From d8dd636430d34651c08bb45e03ca9d43308bf955 Mon Sep 17 00:00:00 2001 From: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:04:06 -0700 Subject: [PATCH 1/4] Fix `stripe-context` not being included in subsequent list requests (#1660) * fix stripe-context getting dropped from subsequent requests * rename _replace_options and centralize keys --- stripe/_api_requestor.py | 21 +++-- stripe/_error_object.py | 4 +- stripe/_request_options.py | 12 +++ stripe/_stripe_object.py | 13 ++- tests/api_resources/test_list_object.py | 110 ++++++++++++++++++++++++ tests/test_api_requestor.py | 8 +- 6 files changed, 153 insertions(+), 15 deletions(-) diff --git a/stripe/_api_requestor.py b/stripe/_api_requestor.py index e70018261..4c4e44cb6 100644 --- a/stripe/_api_requestor.py +++ b/stripe/_api_requestor.py @@ -42,7 +42,11 @@ StripeStreamResponse, StripeStreamResponseAsync, ) -from stripe._request_options import RequestOptions, merge_options +from stripe._request_options import ( + PERSISTENT_OPTIONS_KEYS, + RequestOptions, + merge_options, +) from stripe._requestor_options import ( RequestorOptions, _GlobalRequestorOptions, @@ -121,12 +125,15 @@ def _get_http_client(self) -> HTTPClient: return stripe.default_http_client return client - def _replace_options( + def _new_requestor_with_options( self, options: Optional[RequestOptions] ) -> "_APIRequestor": + """ + Returns a new _APIRequestor instance with the same HTTP client but a (potentially) updated set of options. Useful for ensuring the original isn't modified, but any options the original had are still used. + """ options = options or {} new_options = self._options.to_dict() - for key in ["api_key", "stripe_account", "stripe_version"]: + for key in PERSISTENT_OPTIONS_KEYS: if key in options and options[key] is not None: new_options[key] = options[key] return _APIRequestor( @@ -165,7 +172,9 @@ def _global_instance(cls): def _global_with_options( **params: Unpack[RequestOptions], ) -> "_APIRequestor": - return _APIRequestor._global_instance()._replace_options(params) + return _APIRequestor._global_instance()._new_requestor_with_options( + params + ) @classmethod def _format_app_info(cls, info): @@ -187,7 +196,7 @@ def request( usage: Optional[List[str]] = None, ) -> "StripeObject": api_mode = get_api_mode(url) - requestor = self._replace_options(options) + requestor = self._new_requestor_with_options(options) rbody, rcode, rheaders = requestor.request_raw( method.lower(), url, @@ -221,7 +230,7 @@ async def request_async( usage: Optional[List[str]] = None, ) -> "StripeObject": api_mode = get_api_mode(url) - requestor = self._replace_options(options) + requestor = self._new_requestor_with_options(options) rbody, rcode, rheaders = await requestor.request_raw_async( method.lower(), url, diff --git a/stripe/_error_object.py b/stripe/_error_object.py index da2f38918..9221218be 100644 --- a/stripe/_error_object.py +++ b/stripe/_error_object.py @@ -39,7 +39,7 @@ def refresh_from( values=values, partial=partial, last_response=last_response, - requestor=self._requestor._replace_options( + requestor=self._requestor._new_requestor_with_options( { "api_key": api_key, "stripe_version": stripe_version, @@ -102,7 +102,7 @@ def refresh_from( values=values, partial=partial, last_response=last_response, - requestor=self._requestor._replace_options( + requestor=self._requestor._new_requestor_with_options( { "api_key": api_key, "stripe_version": stripe_version, diff --git a/stripe/_request_options.py b/stripe/_request_options.py index 05d8c45a3..b3c00c433 100644 --- a/stripe/_request_options.py +++ b/stripe/_request_options.py @@ -54,6 +54,18 @@ def merge_options( } +PERSISTENT_OPTIONS_KEYS = { + "api_key", + "stripe_version", + "stripe_account", + "stripe_context", +} +""" +These are the keys in RequestOptions that should persist across requests made +by the same requestor. +""" + + def extract_options_from_dict( d: Optional[Mapping[str, Any]], ) -> Tuple[RequestOptions, Dict[str, Any]]: diff --git a/stripe/_stripe_object.py b/stripe/_stripe_object.py index bcf3c69ab..0f44342f4 100644 --- a/stripe/_stripe_object.py +++ b/stripe/_stripe_object.py @@ -27,7 +27,10 @@ StripeStreamResponseAsync, ) from stripe._encode import _encode_datetime # pyright: ignore -from stripe._request_options import extract_options_from_dict +from stripe._request_options import ( + PERSISTENT_OPTIONS_KEYS, + extract_options_from_dict, +) from stripe._api_mode import ApiMode from stripe._base_address import BaseAddress @@ -152,8 +155,10 @@ def update( # pyright: ignore if not TYPE_CHECKING: def __setattr__(self, k, v): - if k in {"api_key", "stripe_account", "stripe_version"}: - self._requestor = self._requestor._replace_options({k: v}) + if k in PERSISTENT_OPTIONS_KEYS: + self._requestor = self._requestor._new_requestor_with_options( + {k: v} + ) return None if k[0] == "_" or k in self.__dict__: @@ -303,7 +308,7 @@ def refresh_from( values=values, partial=partial, last_response=last_response, - requestor=self._requestor._replace_options( # pyright: ignore[reportPrivateUsage] + requestor=self._requestor._new_requestor_with_options( # pyright: ignore[reportPrivateUsage] { "api_key": api_key, "stripe_version": stripe_version, diff --git a/tests/api_resources/test_list_object.py b/tests/api_resources/test_list_object.py index 37ab96fe1..02568c4b7 100644 --- a/tests/api_resources/test_list_object.py +++ b/tests/api_resources/test_list_object.py @@ -5,6 +5,7 @@ import stripe from stripe._util import convert_to_stripe_object from stripe._stripe_object import StripeObject +from tests.http_client_mock import HTTPClientMock class TestListObject(object): @@ -522,6 +523,115 @@ def test_forwards_api_key_to_nested_resources(self, http_client_mock): # assert seen == ["prod_001", "prod_002"] + def test_iter_with_stripe_account(self, http_client_mock: HTTPClientMock): + http_client_mock.stub_request( + "get", + path="/v1/customers", + rbody='{"object": "list", "data": [{"id": "cus_001", "object": "customer"}], "url": "/v1/customers", "has_more": true}', + ) + http_client_mock.stub_request( + "get", + path="/v1/customers", + query_string="starting_after=cus_001", + rbody='{"object": "list", "data": [{"id": "cus_002", "object": "customer"}], "url": "/v1/customers", "has_more": false}', + ) + + cu_list = stripe.Customer.list( + api_key="org_key_abc", + stripe_account="ctx_123", + ) + + customers = [item.id for item in cu_list.auto_paging_iter()] + assert customers == ["cus_001", "cus_002"] + + http_client_mock.assert_requested( + "get", + path="/v1/customers", + api_key="org_key_abc", + stripe_account="ctx_123", + ) + http_client_mock.assert_requested( + "get", + path="/v1/customers", + query_string="starting_after=cus_001", + api_key="org_key_abc", + stripe_account="ctx_123", + ) + + def test_iter_with_stripe_context(self, http_client_mock: HTTPClientMock): + http_client_mock.stub_request( + "get", + path="/v1/customers", + rbody='{"object": "list", "data": [{"id": "cus_001", "object": "customer"}], "url": "/v1/customers", "has_more": true}', + ) + http_client_mock.stub_request( + "get", + path="/v1/customers", + query_string="starting_after=cus_001", + rbody='{"object": "list", "data": [{"id": "cus_002", "object": "customer"}], "url": "/v1/customers", "has_more": false}', + ) + + cu_list = stripe.Customer.list( + api_key="org_key_abc", + stripe_context="ctx_123", + ) + + customers = [item.id for item in cu_list.auto_paging_iter()] + assert customers == ["cus_001", "cus_002"] + + http_client_mock.assert_requested( + "get", + path="/v1/customers", + api_key="org_key_abc", + stripe_context="ctx_123", + ) + http_client_mock.assert_requested( + "get", + path="/v1/customers", + query_string="starting_after=cus_001", + api_key="org_key_abc", + stripe_context="ctx_123", + ) + + def test_iter_with_stripe_context_client( + self, http_client_mock: HTTPClientMock + ): + http_client_mock.stub_request( + "get", + path="/v1/customers", + rbody='{"object": "list", "data": [{"id": "cus_001", "object": "customer"}], "url": "/v1/customers", "has_more": true}', + ) + http_client_mock.stub_request( + "get", + path="/v1/customers", + query_string="starting_after=cus_001", + rbody='{"object": "list", "data": [{"id": "cus_002", "object": "customer"}], "url": "/v1/customers", "has_more": false}', + ) + + client = stripe.StripeClient( + "org_key_abc", http_client=http_client_mock.get_mock_http_client() + ) + cu_list = client.v1.customers.list( + options={"stripe_context": "ctx_123"} + ) + + customers = [item.id for item in cu_list.auto_paging_iter()] + assert customers == ["cus_001", "cus_002"] + + http_client_mock.assert_requested( + "get", + path="/v1/customers", + api_key="org_key_abc", + stripe_context="ctx_123", + ) + http_client_mock.assert_requested( + "get", + path="/v1/customers", + query_string="starting_after=cus_001", + api_key="org_key_abc", + stripe_context="ctx_123", + ) + class TestAutoPagingAsync: @staticmethod diff --git a/tests/test_api_requestor.py b/tests/test_api_requestor.py index 0262f75fb..3e5a80947 100644 --- a/tests/test_api_requestor.py +++ b/tests/test_api_requestor.py @@ -572,7 +572,9 @@ def test_prefers_headers_api_version(self, requestor, http_client_mock): def test_uses_instance_key(self, requestor, http_client_mock): key = "fookey" - requestor = requestor._replace_options(RequestOptions(api_key=key)) + requestor = requestor._new_requestor_with_options( + RequestOptions(api_key=key) + ) http_client_mock.stub_request( "get", path=self.v1_path, rbody="{}", rcode=200 @@ -585,7 +587,7 @@ def test_uses_instance_key(self, requestor, http_client_mock): def test_uses_instance_account(self, requestor, http_client_mock): account = "acct_foo" - requestor = requestor._replace_options( + requestor = requestor._new_requestor_with_options( RequestOptions(stripe_account=account) ) @@ -610,7 +612,7 @@ def test_removes_None_account( in the generated fetch_related_object doesn't actually send the null header """ account = None - requestor = requestor._replace_options( + requestor = requestor._new_requestor_with_options( RequestOptions(stripe_account=account) ) From 3e0df8b2019f577291a2e57da43ba61d20883358 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Fri, 31 Oct 2025 13:06:38 -0700 Subject: [PATCH 2/4] Bump version to 13.1.1 --- CHANGELOG.md | 3 +++ VERSION | 2 +- pyproject.toml | 2 +- stripe/_version.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7694b752..96dac03a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 13.1.1 - 2025-10-31 +* [#1660](https://github.com/stripe/stripe-python/pull/1660) Fix `stripe-context` header not being included in paged list requests + ## 13.1.0 - 2025-10-29 This release changes the pinned API version to `2025-10-29.clover`. diff --git a/VERSION b/VERSION index e6ba35136..21b80e995 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -13.1.0 +13.1.1 diff --git a/pyproject.toml b/pyproject.toml index 3c397cd67..e0ea92d0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stripe" -version = "13.1.0" +version = "13.1.1" readme = "README.md" description = "Python bindings for the Stripe API" authors = [{ name = "Stripe", email = "support@stripe.com" }] diff --git a/stripe/_version.py b/stripe/_version.py index 06b380e3b..7e440d406 100644 --- a/stripe/_version.py +++ b/stripe/_version.py @@ -1 +1 @@ -VERSION = "13.1.0" +VERSION = "13.1.1" From d07f3de9b92dbbf84c681a0d5176c2d816ec9f9d Mon Sep 17 00:00:00 2001 From: "stripe-openapi[bot]" <105521251+stripe-openapi[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:02:28 -0800 Subject: [PATCH 3/4] Update generated code for v2108 and (#1661) Co-authored-by: Stripe OpenAPI <105521251+stripe-openapi[bot]@users.noreply.github.com> --- API_VERSION | 2 +- OPENAPI_VERSION | 2 +- stripe/_invoice_payment.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/API_VERSION b/API_VERSION index 1105d4353..6ab1f382f 100644 --- a/API_VERSION +++ b/API_VERSION @@ -1 +1 @@ -3ccf295957c8cadc88e1463ea3ab4ec683a0314f \ No newline at end of file +07c094f7c1f64823539941252667a6620cc6bb44 \ No newline at end of file diff --git a/OPENAPI_VERSION b/OPENAPI_VERSION index c8ed627ff..93363663d 100644 --- a/OPENAPI_VERSION +++ b/OPENAPI_VERSION @@ -1 +1 @@ -v2102 \ No newline at end of file +v2108 \ No newline at end of file diff --git a/stripe/_invoice_payment.py b/stripe/_invoice_payment.py index 203fec916..a66e08069 100644 --- a/stripe/_invoice_payment.py +++ b/stripe/_invoice_payment.py @@ -47,7 +47,7 @@ class Payment(StripeObject): """ ID of the PaymentRecord associated with this payment when `type` is `payment_record`. """ - type: Literal["charge", "payment_intent"] + type: Literal["charge", "payment_intent", "payment_record"] """ Type of payment object associated with this invoice payment. """ From 2b4c14a146f29591fc8aac899823e19513564db0 Mon Sep 17 00:00:00 2001 From: Ramya Rao Date: Tue, 4 Nov 2025 15:07:15 -0800 Subject: [PATCH 4/4] Bump version to 13.1.2 --- CHANGELOG.md | 3 +++ VERSION | 2 +- pyproject.toml | 2 +- stripe/_version.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dac03a5..02213a97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 13.1.2 - 2025-11-04 +* [#1661](https://github.com/stripe/stripe-python/pull/1661) Add support for value `payment_record` to enum `InvoicePayment.payment.type` + ## 13.1.1 - 2025-10-31 * [#1660](https://github.com/stripe/stripe-python/pull/1660) Fix `stripe-context` header not being included in paged list requests diff --git a/VERSION b/VERSION index 21b80e995..383738180 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -13.1.1 +13.1.2 diff --git a/pyproject.toml b/pyproject.toml index e0ea92d0e..bee351afe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stripe" -version = "13.1.1" +version = "13.1.2" readme = "README.md" description = "Python bindings for the Stripe API" authors = [{ name = "Stripe", email = "support@stripe.com" }] diff --git a/stripe/_version.py b/stripe/_version.py index 7e440d406..34ea198a5 100644 --- a/stripe/_version.py +++ b/stripe/_version.py @@ -1 +1 @@ -VERSION = "13.1.1" +VERSION = "13.1.2"