Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .fern/metadata.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"cliVersion": "5.50.0",
"generatorName": "fernapi/fern-python-sdk",
"generatorVersion": "5.41.4",
"generatorVersion": "5.41.10",
"generatorConfig": {
"inline_request_params": true,
"client": {
Expand Down Expand Up @@ -52,10 +52,10 @@
},
"pyproject_toml": "[[tool.poetry.include]]\npath = \"src/schematic/datastream/wasm/*.wasm\"\nformat = [\"sdist\", \"wheel\"]\n\n[[tool.mypy.overrides]]\nmodule = \"redis.*\"\nignore_missing_imports = true\n"
},
"originGitCommit": "9c3d404a6ed3ff63811ecbef9e2814ea8607f22d",
"originGitCommit": "6d2cbedf447a3e0916031392e5a8ec1c46d245d3",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
"requestedVersion": "1.4.6",
"requestedVersion": "1.4.7",
"ciProvider": "github",
"sdkVersion": "1.4.6"
"sdkVersion": "1.4.7"
}
8 changes: 7 additions & 1 deletion .fern/replay.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ license-files = ["LICENSE"]

[tool.poetry]
name = "schematichq"
version = "1.4.6"
version = "1.4.7"
description = ""
readme = "README.md"
authors = []
Expand Down
3 changes: 3 additions & 0 deletions src/schematic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@
CreditGrantSortOrder,
CreditLeaseResponseData,
CreditLedgerEntryKind,
CreditLedgerSourceType,
CreditReservationResponseData,
CreditSpendPolicy,
CreditSpendPolicyResponseData,
Expand Down Expand Up @@ -1385,6 +1386,7 @@
"CreditGrantSortOrder": ".types",
"CreditLeaseResponseData": ".types",
"CreditLedgerEntryKind": ".types",
"CreditLedgerSourceType": ".types",
"CreditReservationResponseData": ".types",
"CreditSpendPolicy": ".types",
"CreditSpendPolicyResponseData": ".types",
Expand Down Expand Up @@ -2427,6 +2429,7 @@ def __dir__():
"CreditGrantSortOrder",
"CreditLeaseResponseData",
"CreditLedgerEntryKind",
"CreditLedgerSourceType",
"CreditReservationResponseData",
"CreditSpendPolicy",
"CreditSpendPolicyResponseData",
Expand Down
4 changes: 2 additions & 2 deletions src/schematic/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform

headers: typing.Dict[str, str] = {
"User-Agent": "schematichq/1.4.6",
"User-Agent": "schematichq/1.4.7",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
"X-Fern-SDK-Name": "schematichq",
"X-Fern-SDK-Version": "1.4.6",
"X-Fern-SDK-Version": "1.4.7",
**(self.get_custom_headers() or {}),
}
headers["X-Schematic-Api-Key"] = self.api_key
Expand Down
50 changes: 48 additions & 2 deletions src/schematic/core/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,49 @@ def get_keepalive_socket_options(
return opts


def _get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]:
# httpx skips HTTP(S)_PROXY / ALL_PROXY / NO_PROXY whenever a custom transport is
# passed, so the keepalive default rebuilds those env-derived mounts itself, using
# httpx's own resolver to keep identical semantics (incl. NO_PROXY bypass rules).
try:
from httpx._utils import get_environment_proxies
except (ImportError, AttributeError):
return {}
return get_environment_proxies()


def get_keepalive_proxy_mounts(
idle: int = 60,
intvl: int = 30,
cnt: int = 5,
) -> typing.Dict[str, typing.Optional[httpx.BaseTransport]]:
"""
Build ``httpx.Client(mounts=...)`` entries for the proxies configured in the environment,
each carrying the TCP keepalive socket options. ``None`` entries are NO_PROXY bypasses
that fall back to the client's default transport.
"""
socket_options = get_keepalive_socket_options(idle=idle, intvl=intvl, cnt=cnt)
return {
pattern: None if url is None else httpx.HTTPTransport(proxy=httpx.Proxy(url=url), socket_options=socket_options)
for pattern, url in _get_environment_proxies().items()
}


def get_async_keepalive_proxy_mounts(
idle: int = 60,
intvl: int = 30,
cnt: int = 5,
) -> typing.Dict[str, typing.Optional[httpx.AsyncBaseTransport]]:
"""Async counterpart of ``get_keepalive_proxy_mounts`` for ``httpx.AsyncClient(mounts=...)``."""
socket_options = get_keepalive_socket_options(idle=idle, intvl=intvl, cnt=cnt)
return {
pattern: None
if url is None
else httpx.AsyncHTTPTransport(proxy=httpx.Proxy(url=url), socket_options=socket_options)
for pattern, url in _get_environment_proxies().items()
}


def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
"""
This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait.
Expand All @@ -66,9 +109,12 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float
retry_after_ms = response_headers.get("retry-after-ms")
if retry_after_ms is not None:
try:
return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0
except Exception:
retry_after_ms_value = float(retry_after_ms)
except ValueError:
pass
else:
if 0 < retry_after_ms_value < float("inf"):
return retry_after_ms_value / 1000

retry_after = response_headers.get("retry-after")
if retry_after is None:
Expand Down
2 changes: 1 addition & 1 deletion src/schematic/core/pydantic_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ def encode_by_type(o: Any) -> Any:

def update_forward_refs(model: Type["Model"], **localns: Any) -> None:
if IS_PYDANTIC_V2:
model.model_rebuild(raise_errors=False) # type: ignore[attr-defined]
model.model_rebuild(raise_errors=False, _types_namespace=localns or None) # type: ignore[attr-defined]
else:
model.update_forward_refs(**localns)

Expand Down
3 changes: 3 additions & 0 deletions src/schematic/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
from .credit_grant_sort_order import CreditGrantSortOrder
from .credit_lease_response_data import CreditLeaseResponseData
from .credit_ledger_entry_kind import CreditLedgerEntryKind
from .credit_ledger_source_type import CreditLedgerSourceType
from .credit_reservation_response_data import CreditReservationResponseData
from .credit_spend_policy import CreditSpendPolicy
from .credit_spend_policy_response_data import CreditSpendPolicyResponseData
Expand Down Expand Up @@ -802,6 +803,7 @@
"CreditGrantSortOrder": ".credit_grant_sort_order",
"CreditLeaseResponseData": ".credit_lease_response_data",
"CreditLedgerEntryKind": ".credit_ledger_entry_kind",
"CreditLedgerSourceType": ".credit_ledger_source_type",
"CreditReservationResponseData": ".credit_reservation_response_data",
"CreditSpendPolicy": ".credit_spend_policy",
"CreditSpendPolicyResponseData": ".credit_spend_policy_response_data",
Expand Down Expand Up @@ -1395,6 +1397,7 @@ def __dir__():
"CreditGrantSortOrder",
"CreditLeaseResponseData",
"CreditLedgerEntryKind",
"CreditLedgerSourceType",
"CreditReservationResponseData",
"CreditSpendPolicy",
"CreditSpendPolicyResponseData",
Expand Down
6 changes: 5 additions & 1 deletion src/schematic/types/audit_log_list_response_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ class AuditLogListResponseData(UniversalBaseModel):
environment_id: typing.Optional[str] = None
id: str
method: str
resource_id: typing.Optional[int] = None
resource_id: typing.Optional[int] = pydantic.Field(default=None)
"""
Always null. Use resource_id_string.
"""

resource_id_string: typing.Optional[str] = None
resource_name: typing.Optional[str] = None
resource_type: typing.Optional[str] = None
Expand Down
6 changes: 5 additions & 1 deletion src/schematic/types/audit_log_response_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ class AuditLogResponseData(UniversalBaseModel):
id: str
method: str
req_body: typing.Optional[str] = None
resource_id: typing.Optional[int] = None
resource_id: typing.Optional[int] = pydantic.Field(default=None)
"""
Always null. Use resource_id_string.
"""

resource_id_string: typing.Optional[str] = None
resource_name: typing.Optional[str] = None
resource_type: typing.Optional[str] = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ class BillingNativeSubscriptionLineResponseData(UniversalBaseModel):

effective_from: dt.datetime
effective_to: typing.Optional[dt.datetime] = None
feature_name: typing.Optional[str] = pydantic.Field(default=None)
"""
The feature the line bills for when its price belongs to a plan entitlement, such as seats. Null on a plan's own price, an add-on's and a credit's.
"""

id: str
kind: LineKind
quantity: int
Expand Down
4 changes: 4 additions & 0 deletions src/schematic/types/catalog_credit_grant_response_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class CatalogCreditGrantResponseData(UniversalBaseModel):
license_id: typing.Optional[str] = None
reset_cadence: typing.Optional[BillingPlanCreditGrantResetCadence] = None
scaling: PlanCreditGrantScaling
visible: bool = pydantic.Field()
"""
Whether the catalog shows this credit on the plan. Display-only: a hidden grant is still granted and priced at checkout.
"""

if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
Expand Down
9 changes: 8 additions & 1 deletion src/schematic/types/credit_event_ledger_response_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .credit_burn_breakdown_line import CreditBurnBreakdownLine
from .credit_event_type import CreditEventType
from .credit_ledger_entry_kind import CreditLedgerEntryKind
from .credit_ledger_source_type import CreditLedgerSourceType
from .credit_transfer_reason import CreditTransferReason
from .credit_usage_reason import CreditUsageReason
from .currency import Currency
Expand Down Expand Up @@ -56,7 +57,13 @@ class CreditEventLedgerResponseData(UniversalBaseModel):
plan_id: typing.Optional[str] = None
quantity_consumed: typing.Optional[float] = None
quantity_remaining_at_zero_out: typing.Optional[float] = None
source_id: int
source_id: int = pydantic.Field()
"""
Always 0. Use source_id_string.
"""

source_id_string: typing.Optional[str] = None
source_type: typing.Optional[CreditLedgerSourceType] = None
to_grant_id: typing.Optional[str] = None
transfer_reason: typing.Optional[CreditTransferReason] = None
usage_event_id: typing.Optional[str] = None
Expand Down
8 changes: 8 additions & 0 deletions src/schematic/types/credit_ledger_source_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This file was auto-generated by Fern from our API Definition.

import typing

CreditLedgerSourceType = typing.Union[
typing.Literal["billing_charge", "billing_credit_grant", "billing_credit_transfer", "billing_settlement"],
typing.Any,
]
54 changes: 54 additions & 0 deletions tests/utils/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
HttpClient,
_build_url,
_merge_headers,
_parse_retry_after,
_redact_headers,
_should_retry,
drop_content_type_without_body,
Expand Down Expand Up @@ -880,3 +881,56 @@ def test_http_client_sends_single_header_for_case_variant_override() -> None:
sent = dummy_client.last_request_kwargs["headers"]
assert [k for k in sent if k.lower() == "plaid-secret"] == ["plaid-secret"]
assert sent["plaid-secret"] == "override"


@pytest.mark.parametrize(
"headers,expected",
[
({"retry-after-ms": "1500", "retry-after": "9"}, 1.5),
({"retry-after-ms": "1"}, 0.001),
({"retry-after-ms": "3000"}, 3.0),
({"retry-after-ms": "0", "retry-after": "9"}, 9.0),
({"retry-after-ms": "-5", "retry-after": "9"}, 9.0),
({"retry-after-ms": "NaN", "retry-after": "9"}, 9.0),
({"retry-after-ms": "Infinity", "retry-after": "9"}, 9.0),
({"retry-after-ms": "1e400", "retry-after": "9"}, 9.0),
({"retry-after-ms": "not-a-number", "retry-after": "9"}, 9.0),
({"retry-after": "9"}, 9.0),
({}, None),
],
)
def test_parse_retry_after(headers: Dict[str, str], expected: Any) -> None:
assert _parse_retry_after(httpx.Headers(headers)) == expected


@patch("schematic.core.http_client.time.sleep", return_value=None)
def test_sync_retry_uses_retry_after_ms_header(mock_sleep: MagicMock) -> None:
mock_client = MagicMock()
mock_client.request.side_effect = [
httpx.Response(429, headers={"retry-after-ms": "1500", "retry-after": "9"}),
_DummyResponse(),
]
http_client = _make_sync_http_client(mock_client)

response = http_client.request(path="/test", method="GET")

assert response.status_code == 200
mock_sleep.assert_called_once_with(1.5)


@pytest.mark.asyncio
async def test_async_retry_uses_retry_after_ms_header() -> None:
mock_client = MagicMock()
mock_client.request = AsyncMock(
side_effect=[
httpx.Response(429, headers={"retry-after-ms": "1500", "retry-after": "9"}),
_DummyResponse(),
]
)
http_client = _make_async_http_client(mock_client)

with patch("schematic.core.http_client.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
response = await http_client.request(path="/test", method="GET")

assert response.status_code == 200
mock_sleep.assert_called_once_with(1.5)
Loading