diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd143423..7b2acf820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,46 @@ This release changes the pinned API version to 2026-06-24.preview. * Add support for error codes `anomalous_money_movement_request`, `failed_tax_calculation`, `financial_account_balance_does_not_support_currency`, `financial_account_capability_not_enabled`, and `financial_account_capability_restricted` on `QuotePreviewInvoice.LastFinalizationError` * Add support for error code `default_us_bank_account_cannot_be_archived` on `CannotProceedError` +## 15.3.0 - 2026-06-24 +This release changes the pinned API version to 2026-06-24.dahlia. + +* [#1836](https://github.com/stripe/stripe-python/pull/1836) Update generated code + * Add support for `release_details` on `Reserve.Hold` + * ⚠️ Add support for new value `tax_fund` on enum `BalanceTransaction.type` + * Change `Billing.CreditGrant.priority` to be required + * Add support for `buyer_id` on `Charge.PaymentMethodDetail.Bizum`, `ConfirmationToken.PaymentMethodPreview.Bizum`, `ConfirmationToken.PaymentMethodPreview.Blik`, `PaymentAttemptRecord.PaymentMethodDetail.Bizum`, `PaymentMethod.Bizum`, `PaymentMethod.Blik`, and `PaymentRecord.PaymentMethodDetail.Bizum` + * Add support for `transaction_link_id` on `Charge.PaymentMethodDetail.Card` + * ⚠️ Add support for new value `sui` on enums `Charge.PaymentMethodDetail.Crypto.network`, `PaymentAttemptRecord.PaymentMethodDetail.Crypto.network`, and `PaymentRecord.PaymentMethodDetail.Crypto.network` + * ⚠️ Add support for new value `usdsui` on enums `Charge.PaymentMethodDetail.Crypto.token_currency`, `PaymentAttemptRecord.PaymentMethodDetail.Crypto.token_currency`, and `PaymentRecord.PaymentMethodDetail.Crypto.token_currency` + * Add support for `fingerprint` on `Charge.PaymentMethodDetail.Pix`, `ConfirmationToken.PaymentMethodPreview.Pix`, `PaymentMethod.Pix`, and `SetupAttempt.PaymentMethodDetail.Pix` + * Add support for `sunbit` on `Checkout.Session.PaymentMethodOption`, `PaymentIntent.PaymentMethodOption`, `PaymentIntentConfirmParamsPaymentMethodOption`, `PaymentIntentCreateParamsPaymentMethodOption`, `PaymentIntentModifyParamsPaymentMethodOption`, and `checkout.SessionCreateParamsPaymentMethodOption` + * Add support for `billing_cycle_anchor_config` on `checkout.SessionCreateParamsSubscriptionDatum` + * Add support for `wechat_pay` on `Checkout.Session.PaymentMethodOption` + * Add support for `mastercard_compliance` on `Dispute.Evidence.EnhancedEvidence`, `Dispute.EvidenceDetail.EnhancedEligibility`, and `DisputeModifyParamsEvidenceEnhancedEvidence` + * ⚠️ Add support for new value `mastercard_compliance` on enum `Dispute.enhanced_eligibility_types` + * Add support for `status_details` on `FinancialConnections.Account` + * ⚠️ Add support for new value `validated` on enum `Identity.VerificationSession.Redaction.status` + * Add support for new value `satispay` on enums `InvoiceCreateParamsPaymentSetting.payment_method_types`, `InvoiceModifyParamsPaymentSetting.payment_method_types`, `SubscriptionCreateParamsPaymentSetting.payment_method_types`, and `SubscriptionModifyParamsPaymentSetting.payment_method_types` + * ⚠️ Add support for new value `satispay` on enums `Invoice.PaymentSetting.payment_method_types` and `Subscription.PaymentSetting.payment_method_types` + * ⚠️ Remove support for `stored_credential_usage` on `PaymentAttemptRecord.PaymentMethodDetail.Card` and `PaymentRecord.PaymentMethodDetail.Card` + * ⚠️ Change `PaymentAttemptRecord.PaymentMethodDetail.Card.description` and `PaymentRecord.PaymentMethodDetail.Card.description` to be optional + * ⚠️ Change `PaymentAttemptRecord.PaymentMethodDetail.Card.iin` and `PaymentRecord.PaymentMethodDetail.Card.iin` to be optional + * ⚠️ Change `PaymentAttemptRecord.PaymentMethodDetail.Card.issuer` and `PaymentRecord.PaymentMethodDetail.Card.issuer` to be optional + * Add support for `setup_future_usage` on `PaymentIntent.PaymentMethodOption.Satispay`, `PaymentIntentConfirmParamsPaymentMethodOptionSatispay`, `PaymentIntentCreateParamsPaymentMethodOptionSatispay`, and `PaymentIntentModifyParamsPaymentMethodOptionSatispay` + * Change `PaymentRecordReportRefundParams.refunded` to be optional + * Add support for `satispay` on `SetupAttempt.PaymentMethodDetail` + * Add support for `custom_fields`, `description`, and `footer` on `Subscription.InvoiceSetting`, `SubscriptionCreateParamsInvoiceSetting`, and `SubscriptionModifyParamsInvoiceSetting` + * Add support for `payment_method_options` and `payment_method` on `TopupCreateParams` + * Add support for new value `2026-06-24.dahlia` on enum `WebhookEndpointCreateParams.api_version` + * Add support for `mode` on `V2.Commerce.ProductCatalogImport` + * ⚠️ Add support for new value `promotion` on enum `V2.Commerce.ProductCatalogImport.feed_type` + * Add support for `sunbit_payments` on `V2.Core.Account.Configuration.Merchant.Capability`, `v2.core.AccountCreateParamsConfigurationMerchantCapability`, and `v2.core.AccountModifyParamsConfigurationMerchantCapability` + * Add support for `crypto_money_manager` and `money_manager` on `v2.core.AccountModifyParamsIdentityAttestationTermsOfService` + * ⚠️ Remove support for `crypto_storer` and `storer` on `v2.core.AccountModifyParamsIdentityAttestationTermsOfService` + * Add support for new value `promotion` on enum `v2.commerce.ProductCatalogImportCreateParams.feed_type` + * ⚠️ Add support for new value `sunbit_payments` on enum `EventsV2CoreAccountIncludingConfigurationMerchantCapabilityStatusUpdatedEvent.updated_capability` + * Add support for error codes `anomalous_money_movement_request`, `failed_tax_calculation`, `financial_account_balance_does_not_support_currency`, `financial_account_capability_not_enabled`, and `financial_account_capability_restricted` on `Invoice.LastFinalizationError`, `PaymentIntent.LastPaymentError`, `SetupAttempt.SetupError`, `SetupIntent.LastSetupError`, `StripeError`, and `Terminal.Reader.Action.ApiError` + ## 15.3.0b1 - 2026-05-27 This release changes the pinned API version to 2026-05-27.private. diff --git a/CODEGEN_VERSION b/CODEGEN_VERSION index 983636f7b..6824fd081 100644 --- a/CODEGEN_VERSION +++ b/CODEGEN_VERSION @@ -1 +1 @@ -6012b623b1c09ad54d466947da04511a042ee45a \ No newline at end of file +3723bdf073100308cd413efdb4aefbc912f9bf2f \ No newline at end of file diff --git a/OPENAPI_VERSION b/OPENAPI_VERSION index 83c68c9d1..f0f008c21 100644 --- a/OPENAPI_VERSION +++ b/OPENAPI_VERSION @@ -1 +1 @@ -v2324 \ No newline at end of file +v2343 \ No newline at end of file diff --git a/stripe/_api_requestor.py b/stripe/_api_requestor.py index 3d0ceb641..2c0a2afe7 100644 --- a/stripe/_api_requestor.py +++ b/stripe/_api_requestor.py @@ -24,7 +24,6 @@ NoReturn, Unpack, ) -import uuid from urllib.parse import urlsplit, urlunsplit, parse_qs # breaking circular dependency @@ -100,6 +99,11 @@ def is_v2_delete_resp(method: str, api_mode: ApiMode) -> bool: return method == "delete" and api_mode == "V2" +def _generate_idempotency_key() -> str: + b = os.urandom(16) + return f"{b[0:4].hex()}-{b[4:6].hex()}-{b[6:8].hex()}-{b[8:10].hex()}-{b[10:].hex()}" + + class _APIRequestor(object): _instance: ClassVar["_APIRequestor|None"] = None @@ -616,7 +620,7 @@ def request_headers( # IKs should be set for all POST requests and v2 delete requests if method == "post" or (api_mode == "V2" and method == "delete"): - headers.setdefault("Idempotency-Key", str(uuid.uuid4())) + headers.setdefault("Idempotency-Key", _generate_idempotency_key()) if method == "post": if api_mode == "V2": diff --git a/stripe/_error_object.py b/stripe/_error_object.py index 9221218be..4cc563123 100644 --- a/stripe/_error_object.py +++ b/stripe/_error_object.py @@ -5,24 +5,85 @@ from stripe._api_mode import ApiMode if TYPE_CHECKING: + # errorImports: The beginning of the section generated from our OpenAPI spec from stripe._payment_intent import PaymentIntent + from stripe._payment_method import PaymentMethod from stripe._setup_intent import SetupIntent from stripe._source import Source - from stripe._payment_method import PaymentMethod + # errorImports: The end of the section generated from our OpenAPI spec class ErrorObject(StripeObject): + # errorAnnotations: The beginning of the section generated from our OpenAPI spec + advice_code: Optional[str] + """ + For card errors resulting from a card issuer decline, a short string indicating [how to proceed with an error](https://docs.stripe.com/declines#retrying-issuer-declines) if they provide one. + """ charge: Optional[str] + """ + For card errors, the ID of the failed charge. + """ code: Optional[str] + """ + For some errors that could be handled programmatically, a short string indicating the [error code](https://docs.stripe.com/error-codes) reported. + """ decline_code: Optional[str] + """ + For card errors resulting from a card issuer decline, a short string indicating the [card issuer's reason for the decline](https://docs.stripe.com/declines#issuer-declines) if they provide one. + """ doc_url: Optional[str] + """ + A URL to more information about the [error code](https://docs.stripe.com/error-codes) reported. + """ message: Optional[str] + """ + A human-readable message providing more details about the error. For card errors, these messages can be shown to your users. + """ + network_advice_code: Optional[str] + """ + For card errors resulting from a card issuer decline, a 2 digit code which indicates the advice given to merchant by the card network on how to proceed with an error. + """ + network_decline_code: Optional[str] + """ + For payments declined by the network, an alphanumeric code which indicates the reason the payment failed. + """ param: Optional[str] + """ + If the error is parameter-specific, the parameter related to the error. For example, you can use this to display a message near the correct form field. + """ payment_intent: Optional["PaymentIntent"] + """ + The PaymentIntent object for errors returned on a request involving a PaymentIntent. + """ payment_method: Optional["PaymentMethod"] + """ + The PaymentMethod object for errors returned on a request involving a PaymentMethod. + """ + payment_method_type: Optional[str] + """ + If the error is specific to the type of payment method, the payment method type that had a problem. This field is only populated for invoice-related errors. + """ + request_log_url: Optional[str] + """ + A URL to the request log entry in your dashboard. + """ setup_intent: Optional["SetupIntent"] + """ + The SetupIntent object for errors returned on a request involving a SetupIntent. + """ source: Optional["Source"] + """ + The PaymentSource object for errors returned on a request involving a PaymentSource. + """ type: str + """ + The type of error returned. One of `api_error`, `card_error`, `idempotency_error`, or `invalid_request_error` + """ + user_message: Optional[str] + """ + The user message associated with the error. + """ + # errorAnnotations: The end of the section generated from our OpenAPI spec def refresh_from( self, @@ -63,17 +124,25 @@ def _refresh_from( # values here to facilitate generic error handling. values = merge_dicts( { + # errorDefaults: The beginning of the section generated from our OpenAPI spec + "advice_code": None, "charge": None, "code": None, "decline_code": None, "doc_url": None, "message": None, + "network_advice_code": None, + "network_decline_code": None, "param": None, "payment_intent": None, "payment_method": None, + "payment_method_type": None, + "request_log_url": None, "setup_intent": None, "source": None, "type": None, + "user_message": None, + # errorDefaults: The end of the section generated from our OpenAPI spec }, values, ) diff --git a/stripe/_http_client.py b/stripe/_http_client.py index b09b4984f..65c3ed684 100644 --- a/stripe/_http_client.py +++ b/stripe/_http_client.py @@ -65,61 +65,11 @@ def _now_ms(): def new_default_http_client(*args: Any, **kwargs: Any) -> "HTTPClient": - """ - This method creates and returns a new HTTPClient based on what libraries are available. It uses the following precedence rules: - - 1. Urlfetch (this is provided by Google App Engine, so if it's present you probably want it) - 2. Requests (popular library, the top priority for all environments outside Google App Engine, but not always present) - 3. Pycurl (another library, not always present, not as preferred as Requests but at least it verifies SSL certs) - 4. urllib with a warning (basically always present, a reasonable final default) - - For performance, it only imports what it's actually going to use. But, it re-calculates every time its called, so probably save its result instead of calling it multiple times. - """ - try: - from google.appengine.api import urlfetch # type: ignore # noqa: F401 - except ImportError: - pass - else: - return UrlFetchClient(*args, **kwargs) - - try: - import requests # noqa: F401 - except ImportError: - pass - else: - return RequestsClient(*args, **kwargs) - - try: - import pycurl # type: ignore # noqa: F401 - except ImportError: - pass - else: - return PycurlClient(*args, **kwargs) - - return UrllibClient(*args, **kwargs) + return _default_sync_client(*args, **kwargs) def new_http_client_async_fallback(*args: Any, **kwargs: Any) -> "HTTPClient": - """ - Similar to `new_default_http_client` above, this returns a client that can handle async HTTP requests, if available. - """ - - try: - import httpx # noqa: F401 - import anyio # noqa: F401 - except ImportError: - pass - else: - return HTTPXClient(*args, **kwargs) - - try: - import aiohttp # noqa: F401 - except ImportError: - pass - else: - return AIOHTTPClient(*args, **kwargs) - - return NoImportFoundAsyncClient(*args, **kwargs) + return _default_async_client(*args, **kwargs) class HTTPClient(object): @@ -135,7 +85,6 @@ class _Proxy(TypedDict): MAX_DELAY = 5 INITIAL_DELAY = 0.5 - MAX_RETRY_AFTER = 60 _proxy: Optional[_Proxy] _verify_ssl_certs: bool @@ -212,23 +161,7 @@ def _should_retry( return False - def _retry_after_header( - self, response: Optional[Tuple[Any, Any, Mapping[str, str]]] = None - ): - if response is None: - return None - _, _, rheaders = response - - try: - return int(rheaders["retry-after"]) - except (KeyError, ValueError): - return None - - def _sleep_time_seconds( - self, - num_retries: int, - response: Optional[Tuple[Any, Any, Mapping[str, str]]] = None, - ) -> float: + def _sleep_time_seconds(self, num_retries: int) -> float: """ Apply exponential backoff with initial_network_retry_delay on the number of num_retries so far as inputs. Do not allow the number to exceed `max_network_retry_delay`. @@ -243,11 +176,6 @@ def _sleep_time_seconds( # But never sleep less than the base sleep seconds. sleep_seconds = max(HTTPClient.INITIAL_DELAY, sleep_seconds) - # And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask. - retry_after = self._retry_after_header(response) or 0 - if retry_after <= HTTPClient.MAX_RETRY_AFTER: - sleep_seconds = max(retry_after, sleep_seconds) - return sleep_seconds def _add_jitter_time(self, sleep_seconds: float) -> float: @@ -361,7 +289,7 @@ def _request_with_retries_internal( % connection_error.user_message ) num_retries += 1 - sleep_time = self._sleep_time_seconds(num_retries, response) + sleep_time = self._sleep_time_seconds(num_retries) _util.log_info( ( "Initiating retry %i for request %s %s after " @@ -519,7 +447,7 @@ async def _request_with_retries_internal_async( % connection_error.user_message ) num_retries += 1 - sleep_time = self._sleep_time_seconds(num_retries, response) + sleep_time = self._sleep_time_seconds(num_retries) _util.log_info( ( "Initiating retry %i for request %s %s after " @@ -1550,3 +1478,76 @@ async def request_stream_async( async def close_async(self): self.raise_async_client_import_error() + + +# --- Client resolution --- +# Detect available HTTP libraries at module load time so the expensive imports +# (e.g. requests, httpx) happen during Python's init phase rather than when +# StripeClient() is constructed. This matters in environments like AWS Lambda +# where module loading has a generous timeout (10s) but handler invocation +# does not (often 3s). +# +# Sync client precedence: +# 1. Urlfetch (Google App Engine — if present, you probably want it) +# 2. Requests (popular, top priority outside GAE) +# 3. Pycurl (verifies SSL certs, but less preferred than Requests) +# 4. urllib (stdlib fallback, basically always present) +# +# Async client precedence: +# 1. httpx + anyio (both required) +# 2. aiohttp +# 3. NoImportFoundAsyncClient (raises on use) +# +# To add a new client: define the class above, then add it to the appropriate +# cascade below. The resolved class is stored directly — new_default_http_client() +# and new_http_client_async_fallback() just call it. + + +def _resolve_sync_client(): + try: + from google.appengine.api import urlfetch # type: ignore # noqa: F401 + + return UrlFetchClient + except ImportError: + pass + + try: + import requests # noqa: F401 + + return RequestsClient + except ImportError: + pass + + try: + import pycurl # type: ignore # noqa: F401 + + return PycurlClient + except ImportError: + pass + + return UrllibClient + + +def _resolve_async_client(): + try: + import httpx # noqa: F401 + import anyio # noqa: F401 + + return HTTPXClient + except ImportError: + pass + + try: + import aiohttp # noqa: F401 + + return AIOHTTPClient + except ImportError: + pass + + return NoImportFoundAsyncClient + + +# Called at module load time so HTTP library imports happen during Python's init +# phase rather than inside StripeClient.__init__(). +_default_sync_client = _resolve_sync_client() +_default_async_client = _resolve_async_client() diff --git a/tests/test_error.py b/tests/test_error.py index f8cfc404f..73de812ac 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -54,6 +54,38 @@ def test_error_object(self): assert err.error.code == "some_error" assert err.error.charge is None + def test_error_object_network_advice_code(self): + err = StripeError( + "message", + json_body={ + "error": { + "type": "card_error", + "network_advice_code": "02", + } + }, + ) + assert err.error is not None + assert err.error.network_advice_code == "02" + + def test_error_object_payment_intent(self): + err = StripeError( + "message", + json_body={ + "error": { + "type": "card_error", + "payment_intent": { + "id": "pi_123", + "object": "payment_intent", + "amount": 1000, + }, + } + }, + ) + assert err.error is not None + assert err.error.payment_intent is not None + assert err.error.payment_intent.id == "pi_123" + assert err.error.payment_intent.amount == 1000 + def test_error_object_not_dict(self): err = StripeError("message", json_body={"error": "not a dict"}) assert err.error is None diff --git a/tests/test_http_client.py b/tests/test_http_client.py index be0f8400f..2d1101f9f 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -101,8 +101,8 @@ def test_default_httpclient_from_imports( with patch("builtins.__import__") as mocked_import_fn: mocked_import_fn.side_effect = mock_import(available_libs) - client = _http_client.new_default_http_client() - assert isinstance(client, expected) + resolved_class = _http_client._resolve_sync_client() + assert resolved_class is expected @pytest.mark.parametrize( ["available_libs", "expected"], @@ -123,8 +123,8 @@ def test_default_async_httpclient_from_imports( with patch("builtins.__import__") as mocked_import_fn: mocked_import_fn.side_effect = mock_import(available_libs) - client = _http_client.new_http_client_async_fallback() - assert isinstance(client, expected) + resolved_class = _http_client._resolve_async_client() + assert resolved_class is expected MakeReqFunc = Callable[[str, str, Dict[str, str], Optional[str]], Any] @@ -168,23 +168,6 @@ def test_maximum_delay(self): expected = [0.5, 1.0, 2.0, 4.0, max_delay, max_delay, max_delay] self.assert_sleep_times(client, expected) - def test_retry_after_header(self): - client = _http_client.new_default_http_client() - client._add_jitter_time = lambda sleep_seconds: sleep_seconds - - # Prefer retry-after if it's bigger - assert 30 == client._sleep_time_seconds( - 2, (None, 409, {"retry-after": "30"}) - ) - # Prefer default if it's bigger - assert 2 == client._sleep_time_seconds( - 3, (None, 409, {"retry-after": "1"}) - ) - # Ignore crazy-big values - assert 1 == client._sleep_time_seconds( - 2, (None, 409, {"retry-after": "300"}) - ) - def test_randomness_added(self): client = _http_client.new_default_http_client() random_value = 0.8