From 28df611e6d0c3948cefb2ed95f5b931c4da01d7a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 10:21:58 +0000 Subject: [PATCH 1/7] chore(internal): update models test --- tests/test_models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index ef70d7d2..682b85ca 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -492,12 +492,15 @@ class Model(BaseModel): resource_id: Optional[str] = None m = Model.construct() + assert m.resource_id is None assert "resource_id" not in m.model_fields_set m = Model.construct(resource_id=None) + assert m.resource_id is None assert "resource_id" in m.model_fields_set m = Model.construct(resource_id="foo") + assert m.resource_id == "foo" assert "resource_id" in m.model_fields_set From c1a472380f49e766d1ea3adaa8f621112687e93d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:14:55 +0000 Subject: [PATCH 2/7] chore(ci): add timeout thresholds for CI jobs --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81f6dc20..04b083ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: jobs: lint: + timeout-minutes: 10 name: lint runs-on: ubuntu-latest steps: @@ -30,6 +31,7 @@ jobs: run: ./scripts/lint test: + timeout-minutes: 10 name: test runs-on: ubuntu-latest steps: From 3912d5225dce95e20ae41f51342f2dde1f960aca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:38:11 +0000 Subject: [PATCH 3/7] chore(internal): import reformatting --- src/orb/resources/alerts.py | 5 +---- src/orb/resources/coupons/coupons.py | 5 +---- src/orb/resources/credit_notes.py | 5 +---- src/orb/resources/customers/balance_transactions.py | 5 +---- src/orb/resources/customers/costs.py | 5 +---- src/orb/resources/customers/credits/ledger.py | 6 +----- src/orb/resources/customers/credits/top_ups.py | 5 +---- src/orb/resources/customers/customers.py | 5 +---- .../dimensional_price_groups/dimensional_price_groups.py | 5 +---- src/orb/resources/events/backfills.py | 5 +---- src/orb/resources/events/events.py | 5 +---- src/orb/resources/events/volume.py | 5 +---- src/orb/resources/invoice_line_items.py | 5 +---- src/orb/resources/invoices.py | 5 +---- src/orb/resources/items.py | 5 +---- src/orb/resources/metrics.py | 5 +---- src/orb/resources/plans/external_plan_id.py | 5 +---- src/orb/resources/plans/plans.py | 5 +---- src/orb/resources/prices/external_price_id.py | 5 +---- src/orb/resources/prices/prices.py | 6 +----- src/orb/resources/subscription_changes.py | 5 +---- src/orb/resources/subscriptions.py | 5 +---- 22 files changed, 22 insertions(+), 90 deletions(-) diff --git a/src/orb/resources/alerts.py b/src/orb/resources/alerts.py index 8a6ece08..45847cdf 100644 --- a/src/orb/resources/alerts.py +++ b/src/orb/resources/alerts.py @@ -19,10 +19,7 @@ alert_create_for_external_customer_params, ) from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/coupons/coupons.py b/src/orb/resources/coupons/coupons.py index 3deb9fce..876a6d23 100644 --- a/src/orb/resources/coupons/coupons.py +++ b/src/orb/resources/coupons/coupons.py @@ -9,10 +9,7 @@ from ... import _legacy_response from ...types import coupon_list_params, coupon_create_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/credit_notes.py b/src/orb/resources/credit_notes.py index efac90c7..5445c287 100644 --- a/src/orb/resources/credit_notes.py +++ b/src/orb/resources/credit_notes.py @@ -11,10 +11,7 @@ from .. import _legacy_response from ..types import credit_note_list_params, credit_note_create_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/customers/balance_transactions.py b/src/orb/resources/customers/balance_transactions.py index 5da3caf7..d9f4eb2c 100644 --- a/src/orb/resources/customers/balance_transactions.py +++ b/src/orb/resources/customers/balance_transactions.py @@ -10,10 +10,7 @@ from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/customers/costs.py b/src/orb/resources/customers/costs.py index eb349997..a063f27e 100644 --- a/src/orb/resources/customers/costs.py +++ b/src/orb/resources/customers/costs.py @@ -10,10 +10,7 @@ from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/customers/credits/ledger.py b/src/orb/resources/customers/credits/ledger.py index 60462510..1e58d476 100644 --- a/src/orb/resources/customers/credits/ledger.py +++ b/src/orb/resources/customers/credits/ledger.py @@ -10,11 +10,7 @@ from .... import _legacy_response from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ...._utils import ( - required_args, - maybe_transform, - async_maybe_transform, -) +from ...._utils import required_args, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/customers/credits/top_ups.py b/src/orb/resources/customers/credits/top_ups.py index e9792791..269128fb 100644 --- a/src/orb/resources/customers/credits/top_ups.py +++ b/src/orb/resources/customers/credits/top_ups.py @@ -10,10 +10,7 @@ from .... import _legacy_response from ...._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from ...._utils import ( - maybe_transform, - async_maybe_transform, -) +from ...._utils import maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/customers/customers.py b/src/orb/resources/customers/customers.py index 2bfde0fe..471f2011 100644 --- a/src/orb/resources/customers/customers.py +++ b/src/orb/resources/customers/customers.py @@ -24,10 +24,7 @@ customer_update_by_external_id_params, ) from ..._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/dimensional_price_groups/dimensional_price_groups.py b/src/orb/resources/dimensional_price_groups/dimensional_price_groups.py index 45ccfd75..034c8617 100644 --- a/src/orb/resources/dimensional_price_groups/dimensional_price_groups.py +++ b/src/orb/resources/dimensional_price_groups/dimensional_price_groups.py @@ -9,10 +9,7 @@ from ... import _legacy_response from ...types import dimensional_price_group_list_params, dimensional_price_group_create_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/events/backfills.py b/src/orb/resources/events/backfills.py index 6171fb5e..ebcce1f8 100644 --- a/src/orb/resources/events/backfills.py +++ b/src/orb/resources/events/backfills.py @@ -9,10 +9,7 @@ from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/events/events.py b/src/orb/resources/events/events.py index 8b5569bb..5b2796bf 100644 --- a/src/orb/resources/events/events.py +++ b/src/orb/resources/events/events.py @@ -18,10 +18,7 @@ ) from ...types import event_ingest_params, event_search_params, event_update_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from .backfills import ( Backfills, diff --git a/src/orb/resources/events/volume.py b/src/orb/resources/events/volume.py index 4571731e..0ce305ee 100644 --- a/src/orb/resources/events/volume.py +++ b/src/orb/resources/events/volume.py @@ -9,10 +9,7 @@ from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/invoice_line_items.py b/src/orb/resources/invoice_line_items.py index 82c25b94..7dade2e1 100644 --- a/src/orb/resources/invoice_line_items.py +++ b/src/orb/resources/invoice_line_items.py @@ -10,10 +10,7 @@ from .. import _legacy_response from ..types import invoice_line_item_create_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/invoices.py b/src/orb/resources/invoices.py index eff301a9..46b1426c 100644 --- a/src/orb/resources/invoices.py +++ b/src/orb/resources/invoices.py @@ -18,10 +18,7 @@ invoice_fetch_upcoming_params, ) from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/items.py b/src/orb/resources/items.py index 8e584391..a150ad4f 100644 --- a/src/orb/resources/items.py +++ b/src/orb/resources/items.py @@ -9,10 +9,7 @@ from .. import _legacy_response from ..types import item_list_params, item_create_params, item_update_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/metrics.py b/src/orb/resources/metrics.py index 8b78b8c4..9e5c4088 100644 --- a/src/orb/resources/metrics.py +++ b/src/orb/resources/metrics.py @@ -10,10 +10,7 @@ from .. import _legacy_response from ..types import metric_list_params, metric_create_params, metric_update_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/plans/external_plan_id.py b/src/orb/resources/plans/external_plan_id.py index 69d7b5b0..dce7e6a4 100644 --- a/src/orb/resources/plans/external_plan_id.py +++ b/src/orb/resources/plans/external_plan_id.py @@ -8,10 +8,7 @@ from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/plans/plans.py b/src/orb/resources/plans/plans.py index c1ad4691..bb036773 100644 --- a/src/orb/resources/plans/plans.py +++ b/src/orb/resources/plans/plans.py @@ -11,10 +11,7 @@ from ... import _legacy_response from ...types import plan_list_params, plan_create_params, plan_update_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/prices/external_price_id.py b/src/orb/resources/prices/external_price_id.py index 4fbfe25e..f9c2a7b2 100644 --- a/src/orb/resources/prices/external_price_id.py +++ b/src/orb/resources/prices/external_price_id.py @@ -8,10 +8,7 @@ from ... import _legacy_response from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - maybe_transform, - async_maybe_transform, -) +from ..._utils import maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/prices/prices.py b/src/orb/resources/prices/prices.py index 608900f9..6e90e846 100644 --- a/src/orb/resources/prices/prices.py +++ b/src/orb/resources/prices/prices.py @@ -11,11 +11,7 @@ from ... import _legacy_response from ...types import price_list_params, price_create_params, price_update_params, price_evaluate_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from ..._utils import ( - required_args, - maybe_transform, - async_maybe_transform, -) +from ..._utils import required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/subscription_changes.py b/src/orb/resources/subscription_changes.py index 645b12dd..ef2a88ff 100644 --- a/src/orb/resources/subscription_changes.py +++ b/src/orb/resources/subscription_changes.py @@ -9,10 +9,7 @@ from .. import _legacy_response from ..types import subscription_change_apply_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper diff --git a/src/orb/resources/subscriptions.py b/src/orb/resources/subscriptions.py index 89c51e21..e4d97710 100644 --- a/src/orb/resources/subscriptions.py +++ b/src/orb/resources/subscriptions.py @@ -25,10 +25,7 @@ subscription_unschedule_fixed_fee_quantity_updates_params, ) from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import ( - maybe_transform, - async_maybe_transform, -) +from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper From 4281d1153244369d4424731fffb29a52d55d91a8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:05:28 +0000 Subject: [PATCH 4/7] chore(internal): fix list file params --- src/orb/_utils/_utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/orb/_utils/_utils.py b/src/orb/_utils/_utils.py index e5811bba..ea3cf3f2 100644 --- a/src/orb/_utils/_utils.py +++ b/src/orb/_utils/_utils.py @@ -72,8 +72,16 @@ def _extract_items( from .._files import assert_is_file_content # We have exhausted the path, return the entry we found. - assert_is_file_content(obj, key=flattened_key) assert flattened_key is not None + + if is_list(obj): + files: list[tuple[str, FileTypes]] = [] + for entry in obj: + assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") + files.append((flattened_key + "[]", cast(FileTypes, entry))) + return files + + assert_is_file_content(obj, key=flattened_key) return [(flattened_key, cast(FileTypes, obj))] index += 1 From dd2a4b7e48c890763c1fddbd4285713f6658ce49 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:57:34 +0000 Subject: [PATCH 5/7] chore(internal): refactor retries to not use recursion --- src/orb/_base_client.py | 414 +++++++++++++++++----------------------- 1 file changed, 175 insertions(+), 239 deletions(-) diff --git a/src/orb/_base_client.py b/src/orb/_base_client.py index 4ff24524..f82fa056 100644 --- a/src/orb/_base_client.py +++ b/src/orb/_base_client.py @@ -438,8 +438,7 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 headers = httpx.Headers(headers_dict) idempotency_header = self._idempotency_header - if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: - options.idempotency_key = options.idempotency_key or self._idempotency_key() + if idempotency_header and options.idempotency_key and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key # Don't set these headers if they were already set or removed by the caller. We check @@ -904,7 +903,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: Literal[True], stream_cls: Type[_StreamT], @@ -915,7 +913,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: Literal[False] = False, ) -> ResponseT: ... @@ -925,7 +922,6 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: bool = False, stream_cls: Type[_StreamT] | None = None, @@ -935,125 +931,109 @@ def request( self, cast_to: Type[ResponseT], options: FinalRequestOptions, - remaining_retries: Optional[int] = None, *, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: - if remaining_retries is not None: - retries_taken = options.get_max_retries(self.max_retries) - remaining_retries - else: - retries_taken = 0 - - return self._request( - cast_to=cast_to, - options=options, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) + cast_to = self._maybe_override_cast_to(cast_to, options) - def _request( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - retries_taken: int, - stream: bool, - stream_cls: type[_StreamT] | None, - ) -> ResponseT | _StreamT: # create a copy of the options we were given so that if the # options are mutated later & we then retry, the retries are # given the original options input_options = model_copy(options) - - cast_to = self._maybe_override_cast_to(cast_to, options) - options = self._prepare_options(options) - - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - self._prepare_request(request) - - if options.idempotency_key: + if input_options.idempotency_key is None and input_options.method.lower() != "get": # ensure the idempotency key is reused between requests - input_options.idempotency_key = options.idempotency_key + input_options.idempotency_key = self._idempotency_key() - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) - log.debug("Sending HTTP Request: %s %s", request.method, request.url) + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) - try: - response = self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) - if remaining_retries > 0: - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + log.debug("Sending HTTP Request: %s %s", request.method, request.url) - if remaining_retries > 0: - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, + response = None + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + err.response.close() + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - err.response.close() - return self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - response_headers=err.response.headers, - stream=stream, - stream_cls=stream_cls, - ) + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - err.response.read() + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None + break + assert response is not None, "could not resolve response (should never happen)" return self._process_response( cast_to=cast_to, options=options, @@ -1063,37 +1043,20 @@ def _request( retries_taken=retries_taken, ) - def _retry_request( - self, - options: FinalRequestOptions, - cast_to: Type[ResponseT], - *, - retries_taken: int, - response_headers: httpx.Headers | None, - stream: bool, - stream_cls: type[_StreamT] | None, - ) -> ResponseT | _StreamT: - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken if remaining_retries == 1: log.debug("1 retry left") else: log.debug("%i retries left", remaining_retries) - timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers) + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) log.info("Retrying request to %s in %f seconds", options.url, timeout) - # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a - # different thread if necessary. time.sleep(timeout) - return self._request( - options=options, - cast_to=cast_to, - retries_taken=retries_taken + 1, - stream=stream, - stream_cls=stream_cls, - ) - def _process_response( self, *, @@ -1451,7 +1414,6 @@ async def request( options: FinalRequestOptions, *, stream: Literal[False] = False, - remaining_retries: Optional[int] = None, ) -> ResponseT: ... @overload @@ -1462,7 +1424,6 @@ async def request( *, stream: Literal[True], stream_cls: type[_AsyncStreamT], - remaining_retries: Optional[int] = None, ) -> _AsyncStreamT: ... @overload @@ -1473,7 +1434,6 @@ async def request( *, stream: bool, stream_cls: type[_AsyncStreamT] | None = None, - remaining_retries: Optional[int] = None, ) -> ResponseT | _AsyncStreamT: ... async def request( @@ -1483,120 +1443,111 @@ async def request( *, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, - remaining_retries: Optional[int] = None, - ) -> ResponseT | _AsyncStreamT: - if remaining_retries is not None: - retries_taken = options.get_max_retries(self.max_retries) - remaining_retries - else: - retries_taken = 0 - - return await self._request( - cast_to=cast_to, - options=options, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - async def _request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool, - stream_cls: type[_AsyncStreamT] | None, - retries_taken: int, ) -> ResponseT | _AsyncStreamT: if self._platform is None: # `get_platform` can make blocking IO calls so we # execute it earlier while we are in an async context self._platform = await asyncify(get_platform)() + cast_to = self._maybe_override_cast_to(cast_to, options) + # create a copy of the options we were given so that if the # options are mutated later & we then retry, the retries are # given the original options input_options = model_copy(options) - - cast_to = self._maybe_override_cast_to(cast_to, options) - options = await self._prepare_options(options) - - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - await self._prepare_request(request) - - if options.idempotency_key: + if input_options.idempotency_key is None and input_options.method.lower() != "get": # ensure the idempotency key is reused between requests - input_options.idempotency_key = options.idempotency_key + input_options.idempotency_key = self._idempotency_key() - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) - try: - response = await self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) - if remaining_retries > 0: - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth - if remaining_retries > 0: - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - stream=stream, - stream_cls=stream_cls, - response_headers=None, - ) + log.debug("Sending HTTP Request: %s %s", request.method, request.url) - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err + response = None + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) - log.debug( - 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase - ) + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + await err.response.aclose() + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - await err.response.aclose() - return await self._retry_request( - input_options, - cast_to, - retries_taken=retries_taken, - response_headers=err.response.headers, - stream=stream, - stream_cls=stream_cls, - ) + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - await err.response.aread() + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None + break + assert response is not None, "could not resolve response (should never happen)" return await self._process_response( cast_to=cast_to, options=options, @@ -1606,35 +1557,20 @@ async def _request( retries_taken=retries_taken, ) - async def _retry_request( - self, - options: FinalRequestOptions, - cast_to: Type[ResponseT], - *, - retries_taken: int, - response_headers: httpx.Headers | None, - stream: bool, - stream_cls: type[_AsyncStreamT] | None, - ) -> ResponseT | _AsyncStreamT: - remaining_retries = options.get_max_retries(self.max_retries) - retries_taken + async def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken if remaining_retries == 1: log.debug("1 retry left") else: log.debug("%i retries left", remaining_retries) - timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers) + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) log.info("Retrying request to %s in %f seconds", options.url, timeout) await anyio.sleep(timeout) - return await self._request( - options=options, - cast_to=cast_to, - retries_taken=retries_taken + 1, - stream=stream, - stream_cls=stream_cls, - ) - async def _process_response( self, *, From 57796bf80e447c8ef5687bbfe0c60a2d0038f981 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:30:46 +0000 Subject: [PATCH 6/7] fix(pydantic v1): more robust ModelField.annotation check --- src/orb/_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/orb/_models.py b/src/orb/_models.py index 58b9263e..798956f1 100644 --- a/src/orb/_models.py +++ b/src/orb/_models.py @@ -626,8 +626,8 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, # Note: if one variant defines an alias then they all should discriminator_alias = field_info.alias - if field_info.annotation and is_literal_type(field_info.annotation): - for entry in get_args(field_info.annotation): + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): if isinstance(entry, str): mapping[entry] = variant From 233195517f7b80beeb831a25cf3f04e877e36c15 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:31:06 +0000 Subject: [PATCH 7/7] release: 3.19.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 17 +++++++++++++++++ pyproject.toml | 2 +- src/orb/_version.py | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3362d3a3..ca66de7f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.19.1" + ".": "3.19.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 489c1a22..01082bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 3.19.2 (2025-04-22) + +Full Changelog: [v3.19.1...v3.19.2](https://github.com/orbcorp/orb-python/compare/v3.19.1...v3.19.2) + +### Bug Fixes + +* **pydantic v1:** more robust ModelField.annotation check ([57796bf](https://github.com/orbcorp/orb-python/commit/57796bf80e447c8ef5687bbfe0c60a2d0038f981)) + + +### Chores + +* **ci:** add timeout thresholds for CI jobs ([c1a4723](https://github.com/orbcorp/orb-python/commit/c1a472380f49e766d1ea3adaa8f621112687e93d)) +* **internal:** fix list file params ([4281d11](https://github.com/orbcorp/orb-python/commit/4281d1153244369d4424731fffb29a52d55d91a8)) +* **internal:** import reformatting ([3912d52](https://github.com/orbcorp/orb-python/commit/3912d5225dce95e20ae41f51342f2dde1f960aca)) +* **internal:** refactor retries to not use recursion ([dd2a4b7](https://github.com/orbcorp/orb-python/commit/dd2a4b7e48c890763c1fddbd4285713f6658ce49)) +* **internal:** update models test ([28df611](https://github.com/orbcorp/orb-python/commit/28df611e6d0c3948cefb2ed95f5b931c4da01d7a)) + ## 3.19.1 (2025-04-16) Full Changelog: [v3.19.0...v3.19.1](https://github.com/orbcorp/orb-python/compare/v3.19.0...v3.19.1) diff --git a/pyproject.toml b/pyproject.toml index 1ab6aa12..39e4bcc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "orb-billing" -version = "3.19.1" +version = "3.19.2" description = "The official Python library for the orb API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/orb/_version.py b/src/orb/_version.py index eae10670..93e0c539 100644 --- a/src/orb/_version.py +++ b/src/orb/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "orb" -__version__ = "3.19.1" # x-release-please-version +__version__ = "3.19.2" # x-release-please-version