checkout-intents 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,7 @@ from ._exceptions import (
29
29
  RateLimitError,
30
30
  APITimeoutError,
31
31
  BadRequestError,
32
+ PollTimeoutError,
32
33
  APIConnectionError,
33
34
  AuthenticationError,
34
35
  InternalServerError,
@@ -66,6 +67,7 @@ __all__ = [
66
67
  "UnprocessableEntityError",
67
68
  "RateLimitError",
68
69
  "InternalServerError",
70
+ "PollTimeoutError",
69
71
  "Timeout",
70
72
  "RequestOptions",
71
73
  "Client",
@@ -48,6 +48,23 @@ ENVIRONMENTS: Dict[str, str] = {
48
48
  }
49
49
 
50
50
 
51
+ def _extract_environment_from_api_key(api_key: str) -> Literal["staging", "production"] | None:
52
+ """
53
+ Extracts the environment from a Rye API key.
54
+ API keys follow the format: RYE/{environment}-{key}
55
+
56
+ Args:
57
+ api_key: The API key to parse
58
+
59
+ Returns:
60
+ The extracted environment ('staging' or 'production'), or None if the format doesn't match
61
+ """
62
+ import re
63
+
64
+ match = re.match(r"^RYE/(staging|production)-", api_key)
65
+ return match.group(1) if match else None # type: ignore[return-value]
66
+
67
+
51
68
  class CheckoutIntents(SyncAPIClient):
52
69
  checkout_intents: checkout_intents.CheckoutIntentsResource
53
70
  brands: brands.BrandsResource
@@ -95,7 +112,26 @@ class CheckoutIntents(SyncAPIClient):
95
112
  )
96
113
  self.api_key = api_key
97
114
 
98
- self._environment = environment
115
+ # Auto-infer environment from API key
116
+ inferred_environment = _extract_environment_from_api_key(api_key)
117
+
118
+ # Validate environment option matches API key (if both provided)
119
+ if is_given(environment) and inferred_environment and environment != inferred_environment:
120
+ raise CheckoutIntentsError(
121
+ f"Environment mismatch: API key is for '{inferred_environment}' environment but 'environment' option is set to '{environment}'. Please use an API key that matches your desired environment or omit the 'environment' option to auto-detect from the API key (only auto-detectable with the RYE/{{environment}}-abcdef api key format)."
122
+ )
123
+
124
+ # Use provided environment, or infer from API key, or default to staging
125
+ resolved_environment: Literal["staging", "production"]
126
+ if is_given(environment):
127
+ resolved_environment = cast(Literal["staging", "production"], environment)
128
+ self._environment = cast(Literal["staging", "production"], environment)
129
+ elif inferred_environment:
130
+ resolved_environment = inferred_environment
131
+ self._environment = inferred_environment
132
+ else:
133
+ resolved_environment = "staging"
134
+ self._environment = "staging"
99
135
 
100
136
  base_url_env = os.environ.get("CHECKOUT_INTENTS_BASE_URL")
101
137
  if is_given(base_url) and base_url is not None:
@@ -108,18 +144,16 @@ class CheckoutIntents(SyncAPIClient):
108
144
  )
109
145
 
110
146
  try:
111
- base_url = ENVIRONMENTS[environment]
147
+ base_url = ENVIRONMENTS[resolved_environment]
112
148
  except KeyError as exc:
113
- raise ValueError(f"Unknown environment: {environment}") from exc
149
+ raise ValueError(f"Unknown environment: {resolved_environment}") from exc
114
150
  elif base_url_env is not None:
115
151
  base_url = base_url_env
116
152
  else:
117
- self._environment = environment = "staging"
118
-
119
153
  try:
120
- base_url = ENVIRONMENTS[environment]
154
+ base_url = ENVIRONMENTS[resolved_environment]
121
155
  except KeyError as exc:
122
- raise ValueError(f"Unknown environment: {environment}") from exc
156
+ raise ValueError(f"Unknown environment: {resolved_environment}") from exc
123
157
 
124
158
  super().__init__(
125
159
  version=__version__,
@@ -291,7 +325,26 @@ class AsyncCheckoutIntents(AsyncAPIClient):
291
325
  )
292
326
  self.api_key = api_key
293
327
 
294
- self._environment = environment
328
+ # Auto-infer environment from API key
329
+ inferred_environment = _extract_environment_from_api_key(api_key)
330
+
331
+ # Validate environment option matches API key (if both provided)
332
+ if is_given(environment) and inferred_environment and environment != inferred_environment:
333
+ raise CheckoutIntentsError(
334
+ f"Environment mismatch: API key is for '{inferred_environment}' environment but 'environment' option is set to '{environment}'. Please use an API key that matches your desired environment or omit the 'environment' option to auto-detect from the API key (only auto-detectable with the RYE/{{environment}}-abcdef api key format)."
335
+ )
336
+
337
+ # Use provided environment, or infer from API key, or default to staging
338
+ resolved_environment: Literal["staging", "production"]
339
+ if is_given(environment):
340
+ resolved_environment = cast(Literal["staging", "production"], environment)
341
+ self._environment = cast(Literal["staging", "production"], environment)
342
+ elif inferred_environment:
343
+ resolved_environment = inferred_environment
344
+ self._environment = inferred_environment
345
+ else:
346
+ resolved_environment = "staging"
347
+ self._environment = "staging"
295
348
 
296
349
  base_url_env = os.environ.get("CHECKOUT_INTENTS_BASE_URL")
297
350
  if is_given(base_url) and base_url is not None:
@@ -304,18 +357,16 @@ class AsyncCheckoutIntents(AsyncAPIClient):
304
357
  )
305
358
 
306
359
  try:
307
- base_url = ENVIRONMENTS[environment]
360
+ base_url = ENVIRONMENTS[resolved_environment]
308
361
  except KeyError as exc:
309
- raise ValueError(f"Unknown environment: {environment}") from exc
362
+ raise ValueError(f"Unknown environment: {resolved_environment}") from exc
310
363
  elif base_url_env is not None:
311
364
  base_url = base_url_env
312
365
  else:
313
- self._environment = environment = "staging"
314
-
315
366
  try:
316
- base_url = ENVIRONMENTS[environment]
367
+ base_url = ENVIRONMENTS[resolved_environment]
317
368
  except KeyError as exc:
318
- raise ValueError(f"Unknown environment: {environment}") from exc
369
+ raise ValueError(f"Unknown environment: {resolved_environment}") from exc
319
370
 
320
371
  super().__init__(
321
372
  version=__version__,
@@ -15,6 +15,7 @@ __all__ = [
15
15
  "UnprocessableEntityError",
16
16
  "RateLimitError",
17
17
  "InternalServerError",
18
+ "PollTimeoutError",
18
19
  ]
19
20
 
20
21
 
@@ -106,3 +107,36 @@ class RateLimitError(APIStatusError):
106
107
 
107
108
  class InternalServerError(APIStatusError):
108
109
  pass
110
+
111
+
112
+ class PollTimeoutError(CheckoutIntentsError):
113
+ """Raised when a polling operation times out before the condition is met."""
114
+
115
+ intent_id: str
116
+ attempts: int
117
+ poll_interval: float
118
+ max_attempts: int
119
+
120
+ def __init__(
121
+ self,
122
+ *,
123
+ intent_id: str,
124
+ attempts: int,
125
+ poll_interval: float,
126
+ max_attempts: int,
127
+ message: str | None = None,
128
+ ) -> None:
129
+ self.intent_id = intent_id
130
+ self.attempts = attempts
131
+ self.poll_interval = poll_interval
132
+ self.max_attempts = max_attempts
133
+
134
+ if message is None:
135
+ total_time_s = max_attempts * poll_interval
136
+ message = (
137
+ f"Polling timeout for checkout intent '{intent_id}': "
138
+ f"condition not met after {attempts} attempts ({total_time_s}s). "
139
+ f"Consider increasing max_attempts, polling_interval_ms, or checking the intent state manually."
140
+ )
141
+
142
+ super().__init__(message)
@@ -251,21 +251,22 @@ class BaseModel(pydantic.BaseModel):
251
251
  # pydantic version they are currently using
252
252
 
253
253
  @override
254
- def model_dump(
254
+ def model_dump( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
255
255
  self,
256
256
  *,
257
257
  mode: Literal["json", "python"] | str = "python",
258
258
  include: IncEx | None = None,
259
259
  exclude: IncEx | None = None,
260
+ context: Any | None = None,
260
261
  by_alias: bool | None = None,
261
262
  exclude_unset: bool = False,
262
263
  exclude_defaults: bool = False,
263
264
  exclude_none: bool = False,
265
+ exclude_computed_fields: bool = False,
264
266
  round_trip: bool = False,
265
267
  warnings: bool | Literal["none", "warn", "error"] = True,
266
- context: dict[str, Any] | None = None,
267
- serialize_as_any: bool = False,
268
268
  fallback: Callable[[Any], Any] | None = None,
269
+ serialize_as_any: bool = False,
269
270
  ) -> dict[str, Any]:
270
271
  """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
271
272
 
@@ -273,16 +274,24 @@ class BaseModel(pydantic.BaseModel):
273
274
 
274
275
  Args:
275
276
  mode: The mode in which `to_python` should run.
276
- If mode is 'json', the dictionary will only contain JSON serializable types.
277
- If mode is 'python', the dictionary may contain any Python objects.
278
- include: A list of fields to include in the output.
279
- exclude: A list of fields to exclude from the output.
277
+ If mode is 'json', the output will only contain JSON serializable types.
278
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
279
+ include: A set of fields to include in the output.
280
+ exclude: A set of fields to exclude from the output.
281
+ context: Additional context to pass to the serializer.
280
282
  by_alias: Whether to use the field's alias in the dictionary key if defined.
281
- exclude_unset: Whether to exclude fields that are unset or None from the output.
282
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
283
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
284
- round_trip: Whether to enable serialization and deserialization round-trip support.
285
- warnings: Whether to log warnings when invalid fields are encountered.
283
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
284
+ exclude_defaults: Whether to exclude fields that are set to their default value.
285
+ exclude_none: Whether to exclude fields that have a value of `None`.
286
+ exclude_computed_fields: Whether to exclude computed fields.
287
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
288
+ `round_trip` parameter instead.
289
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
290
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
291
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
292
+ fallback: A function to call when an unknown value is encountered. If not provided,
293
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
294
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
286
295
 
287
296
  Returns:
288
297
  A dictionary representation of the model.
@@ -299,6 +308,8 @@ class BaseModel(pydantic.BaseModel):
299
308
  raise ValueError("serialize_as_any is only supported in Pydantic v2")
300
309
  if fallback is not None:
301
310
  raise ValueError("fallback is only supported in Pydantic v2")
311
+ if exclude_computed_fields != False:
312
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
302
313
  dumped = super().dict( # pyright: ignore[reportDeprecated]
303
314
  include=include,
304
315
  exclude=exclude,
@@ -311,19 +322,21 @@ class BaseModel(pydantic.BaseModel):
311
322
  return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped
312
323
 
313
324
  @override
314
- def model_dump_json(
325
+ def model_dump_json( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
315
326
  self,
316
327
  *,
317
328
  indent: int | None = None,
329
+ ensure_ascii: bool = False,
318
330
  include: IncEx | None = None,
319
331
  exclude: IncEx | None = None,
332
+ context: Any | None = None,
320
333
  by_alias: bool | None = None,
321
334
  exclude_unset: bool = False,
322
335
  exclude_defaults: bool = False,
323
336
  exclude_none: bool = False,
337
+ exclude_computed_fields: bool = False,
324
338
  round_trip: bool = False,
325
339
  warnings: bool | Literal["none", "warn", "error"] = True,
326
- context: dict[str, Any] | None = None,
327
340
  fallback: Callable[[Any], Any] | None = None,
328
341
  serialize_as_any: bool = False,
329
342
  ) -> str:
@@ -355,6 +368,10 @@ class BaseModel(pydantic.BaseModel):
355
368
  raise ValueError("serialize_as_any is only supported in Pydantic v2")
356
369
  if fallback is not None:
357
370
  raise ValueError("fallback is only supported in Pydantic v2")
371
+ if ensure_ascii != False:
372
+ raise ValueError("ensure_ascii is only supported in Pydantic v2")
373
+ if exclude_computed_fields != False:
374
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
358
375
  return super().json( # type: ignore[reportDeprecated]
359
376
  indent=indent,
360
377
  include=include,
@@ -34,7 +34,7 @@ def is_typeddict(tp: Type[Any]) -> bool:
34
34
 
35
35
 
36
36
  def is_literal_type(tp: Type[Any]) -> bool:
37
- return get_origin(tp) in _LITERAL_TYPES
37
+ return get_origin(tp) in _LITERAL_TYPES # type: ignore[comparison-overlap]
38
38
 
39
39
 
40
40
  def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
@@ -373,9 +373,9 @@ def get_required_header(headers: HeadersLike, header: str) -> str:
373
373
  lower_header = header.lower()
374
374
  if is_mapping_t(headers):
375
375
  # mypy doesn't understand the type narrowing here
376
- for k, v in headers.items(): # type: ignore
377
- if k.lower() == lower_header and isinstance(v, str):
378
- return v
376
+ for k, v in headers.items(): # type: ignore[misc, has-type, attr-defined]
377
+ if k.lower() == lower_header and isinstance(v, str): # type: ignore[has-type]
378
+ return v # type: ignore[has-type]
379
379
 
380
380
  # to deal with the case where the header looks like Stainless-Event-Id
381
381
  intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize())
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "checkout_intents"
4
- __version__ = "0.1.0" # x-release-please-version
4
+ __version__ = "0.2.0" # x-release-please-version
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Iterable, cast
5
+ import logging
6
+ from typing import Any, Union, TypeVar, Callable, Iterable, cast
7
+ from typing_extensions import TypeGuard
6
8
 
7
9
  import httpx
8
10
 
@@ -21,14 +23,24 @@ from .._response import (
21
23
  async_to_raw_response_wrapper,
22
24
  async_to_streamed_response_wrapper,
23
25
  )
26
+ from .._exceptions import PollTimeoutError
24
27
  from .._base_client import make_request_options
25
28
  from ..types.buyer_param import BuyerParam
26
- from ..types.checkout_intent import CheckoutIntent
29
+ from ..types.checkout_intent import (
30
+ CheckoutIntent,
31
+ FailedCheckoutIntent,
32
+ CompletedCheckoutIntent,
33
+ AwaitingConfirmationCheckoutIntent,
34
+ )
27
35
  from ..types.payment_method_param import PaymentMethodParam
28
36
  from ..types.variant_selection_param import VariantSelectionParam
29
37
 
30
38
  __all__ = ["CheckoutIntentsResource", "AsyncCheckoutIntentsResource"]
31
39
 
40
+ T = TypeVar("T", bound=CheckoutIntent)
41
+
42
+ logger: logging.Logger = logging.getLogger(__name__)
43
+
32
44
 
33
45
  class CheckoutIntentsResource(SyncAPIResource):
34
46
  @cached_property
@@ -218,6 +230,365 @@ class CheckoutIntentsResource(SyncAPIResource):
218
230
  ),
219
231
  )
220
232
 
233
+ def _poll_until(
234
+ self,
235
+ id: str,
236
+ condition: Callable[[CheckoutIntent], TypeGuard[T]],
237
+ *,
238
+ poll_interval: float = 5.0,
239
+ max_attempts: int = 120,
240
+ extra_headers: Headers | None = None,
241
+ extra_query: Query | None = None,
242
+ extra_body: Body | None = None,
243
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
244
+ ) -> T:
245
+ """
246
+ A helper to poll a checkout intent until a specific condition is met.
247
+
248
+ Args:
249
+ id: The checkout intent ID to poll
250
+ condition: A callable that returns True when the desired state is reached
251
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
252
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
253
+ extra_headers: Send extra headers
254
+ extra_query: Add additional query parameters to the request
255
+ extra_body: Add additional JSON properties to the request
256
+ timeout: Override the client-level default timeout for this request, in seconds
257
+
258
+ Returns:
259
+ The checkout intent once the condition is met
260
+
261
+ Raises:
262
+ CheckoutIntentsError: If the maximum number of attempts is reached without the condition being met
263
+ """
264
+ if max_attempts < 1:
265
+ logger.warning(
266
+ "[Checkout Intents SDK] Invalid max_attempts value: %s. max_attempts must be >= 1. "
267
+ "Defaulting to 1 to ensure at least one polling attempt.",
268
+ max_attempts,
269
+ )
270
+ max_attempts = 1
271
+
272
+ attempts = 0
273
+
274
+ # Build headers for polling
275
+ poll_headers: dict[str, str] = {
276
+ "X-Stainless-Poll-Helper": "true",
277
+ "X-Stainless-Custom-Poll-Interval": str(int(poll_interval * 1000)),
278
+ }
279
+ if extra_headers:
280
+ for k, v in extra_headers.items():
281
+ if not isinstance(v, Omit):
282
+ poll_headers[k] = v # type: ignore[assignment]
283
+
284
+ while attempts < max_attempts:
285
+ # Use with_raw_response to access response headers
286
+ response = self.with_raw_response.retrieve(
287
+ id,
288
+ extra_headers=poll_headers,
289
+ extra_query=extra_query,
290
+ extra_body=extra_body,
291
+ timeout=timeout,
292
+ )
293
+
294
+ intent = response.parse()
295
+
296
+ # Check if condition is met
297
+ if condition(intent):
298
+ return intent
299
+
300
+ attempts += 1
301
+
302
+ # If we've reached max attempts, throw an error
303
+ if attempts >= max_attempts:
304
+ raise PollTimeoutError(
305
+ intent_id=id,
306
+ attempts=attempts,
307
+ poll_interval=poll_interval,
308
+ max_attempts=max_attempts,
309
+ )
310
+
311
+ # Check if server suggests a polling interval
312
+ sleep_interval = poll_interval
313
+ header_interval = response.headers.get("retry-after-ms")
314
+ if header_interval:
315
+ try:
316
+ header_interval_ms = int(header_interval)
317
+ sleep_interval = header_interval_ms / 1000.0
318
+ except ValueError:
319
+ pass # Ignore invalid header values
320
+
321
+ # Sleep before next poll
322
+ self._sleep(sleep_interval)
323
+
324
+ # This should never be reached due to the throw above, but TypeScript needs it
325
+ raise PollTimeoutError(
326
+ intent_id=id,
327
+ attempts=attempts,
328
+ poll_interval=poll_interval,
329
+ max_attempts=max_attempts,
330
+ )
331
+
332
+ def poll_until_completed(
333
+ self,
334
+ id: str,
335
+ *,
336
+ poll_interval: float = 5.0,
337
+ max_attempts: int = 120,
338
+ extra_headers: Headers | None = None,
339
+ extra_query: Query | None = None,
340
+ extra_body: Body | None = None,
341
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
342
+ ) -> Union[CompletedCheckoutIntent, FailedCheckoutIntent]:
343
+ """
344
+ A helper to poll a checkout intent until it reaches a completed state
345
+ (completed or failed).
346
+
347
+ Args:
348
+ id: The checkout intent ID to poll
349
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
350
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
351
+ extra_headers: Send extra headers
352
+ extra_query: Add additional query parameters to the request
353
+ extra_body: Add additional JSON properties to the request
354
+ timeout: Override the client-level default timeout for this request, in seconds
355
+
356
+ Returns:
357
+ The checkout intent once it reaches completed or failed state
358
+
359
+ Example:
360
+ ```python
361
+ checkout_intent = client.checkout_intents.poll_until_completed("id")
362
+ if checkout_intent.state == "completed":
363
+ print("Order placed successfully!")
364
+ elif checkout_intent.state == "failed":
365
+ print("Order failed:", checkout_intent.failure_reason)
366
+ ```
367
+ """
368
+
369
+ def is_completed(
370
+ intent: CheckoutIntent,
371
+ ) -> TypeGuard[Union[CompletedCheckoutIntent, FailedCheckoutIntent]]:
372
+ return intent.state in ("completed", "failed")
373
+
374
+ return self._poll_until(
375
+ id,
376
+ is_completed,
377
+ poll_interval=poll_interval,
378
+ max_attempts=max_attempts,
379
+ extra_headers=extra_headers,
380
+ extra_query=extra_query,
381
+ extra_body=extra_body,
382
+ timeout=timeout,
383
+ )
384
+
385
+ def poll_until_awaiting_confirmation(
386
+ self,
387
+ id: str,
388
+ *,
389
+ poll_interval: float = 5.0,
390
+ max_attempts: int = 120,
391
+ extra_headers: Headers | None = None,
392
+ extra_query: Query | None = None,
393
+ extra_body: Body | None = None,
394
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
395
+ ) -> Union[AwaitingConfirmationCheckoutIntent, FailedCheckoutIntent]:
396
+ """
397
+ A helper to poll a checkout intent until it's ready for confirmation
398
+ (awaiting_confirmation state) or has failed. This is typically used after
399
+ creating a checkout intent to wait for the offer to be retrieved from the merchant.
400
+
401
+ The intent can reach awaiting_confirmation (success - ready to confirm) or failed
402
+ (offer retrieval failed). Always check the state after polling.
403
+
404
+ Args:
405
+ id: The checkout intent ID to poll
406
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
407
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
408
+ extra_headers: Send extra headers
409
+ extra_query: Add additional query parameters to the request
410
+ extra_body: Add additional JSON properties to the request
411
+ timeout: Override the client-level default timeout for this request, in seconds
412
+
413
+ Returns:
414
+ The checkout intent once it reaches awaiting_confirmation or failed state
415
+
416
+ Example:
417
+ ```python
418
+ intent = client.checkout_intents.poll_until_awaiting_confirmation("id")
419
+
420
+ if intent.state == "awaiting_confirmation":
421
+ # Review the offer before confirming
422
+ print("Total:", intent.offer.cost.total)
423
+ elif intent.state == "failed":
424
+ # Handle failure (e.g., offer retrieval failed, product out of stock)
425
+ print("Failed:", intent.failure_reason)
426
+ ```
427
+ """
428
+
429
+ def is_awaiting_confirmation(
430
+ intent: CheckoutIntent,
431
+ ) -> TypeGuard[Union[AwaitingConfirmationCheckoutIntent, FailedCheckoutIntent]]:
432
+ return intent.state in ("awaiting_confirmation", "failed")
433
+
434
+ return self._poll_until(
435
+ id,
436
+ is_awaiting_confirmation,
437
+ poll_interval=poll_interval,
438
+ max_attempts=max_attempts,
439
+ extra_headers=extra_headers,
440
+ extra_query=extra_query,
441
+ extra_body=extra_body,
442
+ timeout=timeout,
443
+ )
444
+
445
+ def create_and_poll(
446
+ self,
447
+ *,
448
+ buyer: BuyerParam,
449
+ product_url: str,
450
+ quantity: float,
451
+ variant_selections: Iterable[VariantSelectionParam] | Omit = omit,
452
+ poll_interval: float = 5.0,
453
+ max_attempts: int = 120,
454
+ extra_headers: Headers | None = None,
455
+ extra_query: Query | None = None,
456
+ extra_body: Body | None = None,
457
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
458
+ ) -> Union[AwaitingConfirmationCheckoutIntent, FailedCheckoutIntent]:
459
+ """
460
+ A helper to create a checkout intent and poll until it's ready for confirmation.
461
+ This follows the Rye documented flow: create → poll until awaiting_confirmation.
462
+
463
+ After this method completes, you should review the offer (pricing, shipping, taxes)
464
+ with the user before calling confirm().
465
+
466
+ Args:
467
+ buyer: Buyer information
468
+ product_url: URL of the product to purchase
469
+ quantity: Quantity of the product
470
+ variant_selections: Product variant selections (optional)
471
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
472
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
473
+ extra_headers: Send extra headers
474
+ extra_query: Add additional query parameters to the request
475
+ extra_body: Add additional JSON properties to the request
476
+ timeout: Override the client-level default timeout for this request, in seconds
477
+
478
+ Returns:
479
+ The checkout intent once it reaches awaiting_confirmation or failed state
480
+
481
+ Example:
482
+ ```python
483
+ # Phase 1: Create and wait for offer
484
+ intent = client.checkout_intents.create_and_poll(
485
+ buyer={
486
+ "address1": "123 Main St",
487
+ "city": "New York",
488
+ "country": "United States",
489
+ "email": "john.doe@example.com",
490
+ "first_name": "John",
491
+ "last_name": "Doe",
492
+ "phone": "+1234567890",
493
+ "postal_code": "10001",
494
+ "province": "NY",
495
+ },
496
+ product_url="https://example.com/product",
497
+ quantity=1,
498
+ )
499
+
500
+ # Review the offer with the user
501
+ print("Total:", intent.offer.cost.total)
502
+
503
+ # Phase 2: Confirm with payment
504
+ completed = client.checkout_intents.confirm_and_poll(
505
+ intent.id, payment_method={"type": "stripe_token", "stripe_token": "tok_visa"}
506
+ )
507
+ ```
508
+ """
509
+ intent = self.create(
510
+ buyer=buyer,
511
+ product_url=product_url,
512
+ quantity=quantity,
513
+ variant_selections=variant_selections,
514
+ extra_headers=extra_headers,
515
+ extra_query=extra_query,
516
+ extra_body=extra_body,
517
+ timeout=timeout,
518
+ )
519
+ return self.poll_until_awaiting_confirmation(
520
+ intent.id,
521
+ poll_interval=poll_interval,
522
+ max_attempts=max_attempts,
523
+ extra_headers=extra_headers,
524
+ extra_query=extra_query,
525
+ extra_body=extra_body,
526
+ timeout=timeout,
527
+ )
528
+
529
+ def confirm_and_poll(
530
+ self,
531
+ id: str,
532
+ *,
533
+ payment_method: PaymentMethodParam,
534
+ poll_interval: float = 5.0,
535
+ max_attempts: int = 120,
536
+ extra_headers: Headers | None = None,
537
+ extra_query: Query | None = None,
538
+ extra_body: Body | None = None,
539
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
540
+ ) -> Union[CompletedCheckoutIntent, FailedCheckoutIntent]:
541
+ """
542
+ A helper to confirm a checkout intent and poll until it reaches a completed state
543
+ (completed or failed).
544
+
545
+ Args:
546
+ id: The checkout intent ID to confirm
547
+ payment_method: Payment method information
548
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
549
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
550
+ extra_headers: Send extra headers
551
+ extra_query: Add additional query parameters to the request
552
+ extra_body: Add additional JSON properties to the request
553
+ timeout: Override the client-level default timeout for this request, in seconds
554
+
555
+ Returns:
556
+ The checkout intent once it reaches completed or failed state
557
+
558
+ Example:
559
+ ```python
560
+ checkout_intent = client.checkout_intents.confirm_and_poll(
561
+ "id",
562
+ payment_method={
563
+ "stripe_token": "tok_1RkrWWHGDlstla3f1Fc7ZrhH",
564
+ "type": "stripe_token",
565
+ },
566
+ )
567
+
568
+ if checkout_intent.state == "completed":
569
+ print("Order placed successfully!")
570
+ elif checkout_intent.state == "failed":
571
+ print("Order failed:", checkout_intent.failure_reason)
572
+ ```
573
+ """
574
+ intent = self.confirm(
575
+ id,
576
+ payment_method=payment_method,
577
+ extra_headers=extra_headers,
578
+ extra_query=extra_query,
579
+ extra_body=extra_body,
580
+ timeout=timeout,
581
+ )
582
+ return self.poll_until_completed(
583
+ intent.id,
584
+ poll_interval=poll_interval,
585
+ max_attempts=max_attempts,
586
+ extra_headers=extra_headers,
587
+ extra_query=extra_query,
588
+ extra_body=extra_body,
589
+ timeout=timeout,
590
+ )
591
+
221
592
 
222
593
  class AsyncCheckoutIntentsResource(AsyncAPIResource):
223
594
  @cached_property
@@ -407,6 +778,365 @@ class AsyncCheckoutIntentsResource(AsyncAPIResource):
407
778
  ),
408
779
  )
409
780
 
781
+ async def _poll_until(
782
+ self,
783
+ id: str,
784
+ condition: Callable[[CheckoutIntent], TypeGuard[T]],
785
+ *,
786
+ poll_interval: float = 5.0,
787
+ max_attempts: int = 120,
788
+ extra_headers: Headers | None = None,
789
+ extra_query: Query | None = None,
790
+ extra_body: Body | None = None,
791
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
792
+ ) -> T:
793
+ """
794
+ A helper to poll a checkout intent until a specific condition is met.
795
+
796
+ Args:
797
+ id: The checkout intent ID to poll
798
+ condition: A callable that returns True when the desired state is reached
799
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
800
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
801
+ extra_headers: Send extra headers
802
+ extra_query: Add additional query parameters to the request
803
+ extra_body: Add additional JSON properties to the request
804
+ timeout: Override the client-level default timeout for this request, in seconds
805
+
806
+ Returns:
807
+ The checkout intent once the condition is met
808
+
809
+ Raises:
810
+ CheckoutIntentsError: If the maximum number of attempts is reached without the condition being met
811
+ """
812
+ if max_attempts < 1:
813
+ logger.warning(
814
+ "[Checkout Intents SDK] Invalid max_attempts value: %s. max_attempts must be >= 1. "
815
+ "Defaulting to 1 to ensure at least one polling attempt.",
816
+ max_attempts,
817
+ )
818
+ max_attempts = 1
819
+
820
+ attempts = 0
821
+
822
+ # Build headers for polling
823
+ poll_headers: dict[str, str] = {
824
+ "X-Stainless-Poll-Helper": "true",
825
+ "X-Stainless-Custom-Poll-Interval": str(int(poll_interval * 1000)),
826
+ }
827
+ if extra_headers:
828
+ for k, v in extra_headers.items():
829
+ if not isinstance(v, Omit):
830
+ poll_headers[k] = v # type: ignore[assignment]
831
+
832
+ while attempts < max_attempts:
833
+ # Use with_raw_response to access response headers
834
+ response = await self.with_raw_response.retrieve(
835
+ id,
836
+ extra_headers=poll_headers,
837
+ extra_query=extra_query,
838
+ extra_body=extra_body,
839
+ timeout=timeout,
840
+ )
841
+
842
+ intent = await response.parse()
843
+
844
+ # Check if condition is met
845
+ if condition(intent):
846
+ return intent
847
+
848
+ attempts += 1
849
+
850
+ # If we've reached max attempts, throw an error
851
+ if attempts >= max_attempts:
852
+ raise PollTimeoutError(
853
+ intent_id=id,
854
+ attempts=attempts,
855
+ poll_interval=poll_interval,
856
+ max_attempts=max_attempts,
857
+ )
858
+
859
+ # Check if server suggests a polling interval
860
+ sleep_interval = poll_interval
861
+ header_interval = response.headers.get("retry-after-ms")
862
+ if header_interval:
863
+ try:
864
+ header_interval_ms = int(header_interval)
865
+ sleep_interval = header_interval_ms / 1000.0
866
+ except ValueError:
867
+ pass # Ignore invalid header values
868
+
869
+ # Sleep before next poll
870
+ await self._sleep(sleep_interval)
871
+
872
+ # This should never be reached due to the throw above, but TypeScript needs it
873
+ raise PollTimeoutError(
874
+ intent_id=id,
875
+ attempts=attempts,
876
+ poll_interval=poll_interval,
877
+ max_attempts=max_attempts,
878
+ )
879
+
880
+ async def poll_until_completed(
881
+ self,
882
+ id: str,
883
+ *,
884
+ poll_interval: float = 5.0,
885
+ max_attempts: int = 120,
886
+ extra_headers: Headers | None = None,
887
+ extra_query: Query | None = None,
888
+ extra_body: Body | None = None,
889
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
890
+ ) -> Union[CompletedCheckoutIntent, FailedCheckoutIntent]:
891
+ """
892
+ A helper to poll a checkout intent until it reaches a completed state
893
+ (completed or failed).
894
+
895
+ Args:
896
+ id: The checkout intent ID to poll
897
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
898
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
899
+ extra_headers: Send extra headers
900
+ extra_query: Add additional query parameters to the request
901
+ extra_body: Add additional JSON properties to the request
902
+ timeout: Override the client-level default timeout for this request, in seconds
903
+
904
+ Returns:
905
+ The checkout intent once it reaches completed or failed state
906
+
907
+ Example:
908
+ ```python
909
+ checkout_intent = await client.checkout_intents.poll_until_completed("id")
910
+ if checkout_intent.state == "completed":
911
+ print("Order placed successfully!")
912
+ elif checkout_intent.state == "failed":
913
+ print("Order failed:", checkout_intent.failure_reason)
914
+ ```
915
+ """
916
+
917
+ def is_completed(
918
+ intent: CheckoutIntent,
919
+ ) -> TypeGuard[Union[CompletedCheckoutIntent, FailedCheckoutIntent]]:
920
+ return intent.state in ("completed", "failed")
921
+
922
+ return await self._poll_until(
923
+ id,
924
+ is_completed,
925
+ poll_interval=poll_interval,
926
+ max_attempts=max_attempts,
927
+ extra_headers=extra_headers,
928
+ extra_query=extra_query,
929
+ extra_body=extra_body,
930
+ timeout=timeout,
931
+ )
932
+
933
+ async def poll_until_awaiting_confirmation(
934
+ self,
935
+ id: str,
936
+ *,
937
+ poll_interval: float = 5.0,
938
+ max_attempts: int = 120,
939
+ extra_headers: Headers | None = None,
940
+ extra_query: Query | None = None,
941
+ extra_body: Body | None = None,
942
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
943
+ ) -> Union[AwaitingConfirmationCheckoutIntent, FailedCheckoutIntent]:
944
+ """
945
+ A helper to poll a checkout intent until it's ready for confirmation
946
+ (awaiting_confirmation state) or has failed. This is typically used after
947
+ creating a checkout intent to wait for the offer to be retrieved from the merchant.
948
+
949
+ The intent can reach awaiting_confirmation (success - ready to confirm) or failed
950
+ (offer retrieval failed). Always check the state after polling.
951
+
952
+ Args:
953
+ id: The checkout intent ID to poll
954
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
955
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
956
+ extra_headers: Send extra headers
957
+ extra_query: Add additional query parameters to the request
958
+ extra_body: Add additional JSON properties to the request
959
+ timeout: Override the client-level default timeout for this request, in seconds
960
+
961
+ Returns:
962
+ The checkout intent once it reaches awaiting_confirmation or failed state
963
+
964
+ Example:
965
+ ```python
966
+ intent = await client.checkout_intents.poll_until_awaiting_confirmation("id")
967
+
968
+ if intent.state == "awaiting_confirmation":
969
+ # Review the offer before confirming
970
+ print("Total:", intent.offer.cost.total)
971
+ elif intent.state == "failed":
972
+ # Handle failure (e.g., offer retrieval failed, product out of stock)
973
+ print("Failed:", intent.failure_reason)
974
+ ```
975
+ """
976
+
977
+ def is_awaiting_confirmation(
978
+ intent: CheckoutIntent,
979
+ ) -> TypeGuard[Union[AwaitingConfirmationCheckoutIntent, FailedCheckoutIntent]]:
980
+ return intent.state in ("awaiting_confirmation", "failed")
981
+
982
+ return await self._poll_until(
983
+ id,
984
+ is_awaiting_confirmation,
985
+ poll_interval=poll_interval,
986
+ max_attempts=max_attempts,
987
+ extra_headers=extra_headers,
988
+ extra_query=extra_query,
989
+ extra_body=extra_body,
990
+ timeout=timeout,
991
+ )
992
+
993
+ async def create_and_poll(
994
+ self,
995
+ *,
996
+ buyer: BuyerParam,
997
+ product_url: str,
998
+ quantity: float,
999
+ variant_selections: Iterable[VariantSelectionParam] | Omit = omit,
1000
+ poll_interval: float = 5.0,
1001
+ max_attempts: int = 120,
1002
+ extra_headers: Headers | None = None,
1003
+ extra_query: Query | None = None,
1004
+ extra_body: Body | None = None,
1005
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
1006
+ ) -> Union[AwaitingConfirmationCheckoutIntent, FailedCheckoutIntent]:
1007
+ """
1008
+ A helper to create a checkout intent and poll until it's ready for confirmation.
1009
+ This follows the Rye documented flow: create → poll until awaiting_confirmation.
1010
+
1011
+ After this method completes, you should review the offer (pricing, shipping, taxes)
1012
+ with the user before calling confirm().
1013
+
1014
+ Args:
1015
+ buyer: Buyer information
1016
+ product_url: URL of the product to purchase
1017
+ quantity: Quantity of the product
1018
+ variant_selections: Product variant selections (optional)
1019
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
1020
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
1021
+ extra_headers: Send extra headers
1022
+ extra_query: Add additional query parameters to the request
1023
+ extra_body: Add additional JSON properties to the request
1024
+ timeout: Override the client-level default timeout for this request, in seconds
1025
+
1026
+ Returns:
1027
+ The checkout intent once it reaches awaiting_confirmation or failed state
1028
+
1029
+ Example:
1030
+ ```python
1031
+ # Phase 1: Create and wait for offer
1032
+ intent = await client.checkout_intents.create_and_poll(
1033
+ buyer={
1034
+ "address1": "123 Main St",
1035
+ "city": "New York",
1036
+ "country": "United States",
1037
+ "email": "john.doe@example.com",
1038
+ "first_name": "John",
1039
+ "last_name": "Doe",
1040
+ "phone": "+1234567890",
1041
+ "postal_code": "10001",
1042
+ "province": "NY",
1043
+ },
1044
+ product_url="https://example.com/product",
1045
+ quantity=1,
1046
+ )
1047
+
1048
+ # Review the offer with the user
1049
+ print("Total:", intent.offer.cost.total)
1050
+
1051
+ # Phase 2: Confirm with payment
1052
+ completed = await client.checkout_intents.confirm_and_poll(
1053
+ intent.id, payment_method={"type": "stripe_token", "stripe_token": "tok_visa"}
1054
+ )
1055
+ ```
1056
+ """
1057
+ intent = await self.create(
1058
+ buyer=buyer,
1059
+ product_url=product_url,
1060
+ quantity=quantity,
1061
+ variant_selections=variant_selections,
1062
+ extra_headers=extra_headers,
1063
+ extra_query=extra_query,
1064
+ extra_body=extra_body,
1065
+ timeout=timeout,
1066
+ )
1067
+ return await self.poll_until_awaiting_confirmation(
1068
+ intent.id,
1069
+ poll_interval=poll_interval,
1070
+ max_attempts=max_attempts,
1071
+ extra_headers=extra_headers,
1072
+ extra_query=extra_query,
1073
+ extra_body=extra_body,
1074
+ timeout=timeout,
1075
+ )
1076
+
1077
+ async def confirm_and_poll(
1078
+ self,
1079
+ id: str,
1080
+ *,
1081
+ payment_method: PaymentMethodParam,
1082
+ poll_interval: float = 5.0,
1083
+ max_attempts: int = 120,
1084
+ extra_headers: Headers | None = None,
1085
+ extra_query: Query | None = None,
1086
+ extra_body: Body | None = None,
1087
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
1088
+ ) -> Union[CompletedCheckoutIntent, FailedCheckoutIntent]:
1089
+ """
1090
+ A helper to confirm a checkout intent and poll until it reaches a completed state
1091
+ (completed or failed).
1092
+
1093
+ Args:
1094
+ id: The checkout intent ID to confirm
1095
+ payment_method: Payment method information
1096
+ poll_interval: The interval in seconds between polling attempts (default: 5.0)
1097
+ max_attempts: The maximum number of polling attempts before timing out (default: 120)
1098
+ extra_headers: Send extra headers
1099
+ extra_query: Add additional query parameters to the request
1100
+ extra_body: Add additional JSON properties to the request
1101
+ timeout: Override the client-level default timeout for this request, in seconds
1102
+
1103
+ Returns:
1104
+ The checkout intent once it reaches completed or failed state
1105
+
1106
+ Example:
1107
+ ```python
1108
+ checkout_intent = await client.checkout_intents.confirm_and_poll(
1109
+ "id",
1110
+ payment_method={
1111
+ "stripe_token": "tok_1RkrWWHGDlstla3f1Fc7ZrhH",
1112
+ "type": "stripe_token",
1113
+ },
1114
+ )
1115
+
1116
+ if checkout_intent.state == "completed":
1117
+ print("Order placed successfully!")
1118
+ elif checkout_intent.state == "failed":
1119
+ print("Order failed:", checkout_intent.failure_reason)
1120
+ ```
1121
+ """
1122
+ intent = await self.confirm(
1123
+ id,
1124
+ payment_method=payment_method,
1125
+ extra_headers=extra_headers,
1126
+ extra_query=extra_query,
1127
+ extra_body=extra_body,
1128
+ timeout=timeout,
1129
+ )
1130
+ return await self.poll_until_completed(
1131
+ intent.id,
1132
+ poll_interval=poll_interval,
1133
+ max_attempts=max_attempts,
1134
+ extra_headers=extra_headers,
1135
+ extra_query=extra_query,
1136
+ extra_body=extra_body,
1137
+ timeout=timeout,
1138
+ )
1139
+
410
1140
 
411
1141
  class CheckoutIntentsResourceWithRawResponse:
412
1142
  def __init__(self, checkout_intents: CheckoutIntentsResource) -> None:
@@ -424,6 +1154,18 @@ class CheckoutIntentsResourceWithRawResponse:
424
1154
  self.confirm = to_raw_response_wrapper(
425
1155
  checkout_intents.confirm,
426
1156
  )
1157
+ self.poll_until_completed = to_raw_response_wrapper(
1158
+ checkout_intents.poll_until_completed,
1159
+ )
1160
+ self.poll_until_awaiting_confirmation = to_raw_response_wrapper(
1161
+ checkout_intents.poll_until_awaiting_confirmation,
1162
+ )
1163
+ self.create_and_poll = to_raw_response_wrapper(
1164
+ checkout_intents.create_and_poll,
1165
+ )
1166
+ self.confirm_and_poll = to_raw_response_wrapper(
1167
+ checkout_intents.confirm_and_poll,
1168
+ )
427
1169
 
428
1170
 
429
1171
  class AsyncCheckoutIntentsResourceWithRawResponse:
@@ -442,6 +1184,18 @@ class AsyncCheckoutIntentsResourceWithRawResponse:
442
1184
  self.confirm = async_to_raw_response_wrapper(
443
1185
  checkout_intents.confirm,
444
1186
  )
1187
+ self.poll_until_completed = async_to_raw_response_wrapper(
1188
+ checkout_intents.poll_until_completed,
1189
+ )
1190
+ self.poll_until_awaiting_confirmation = async_to_raw_response_wrapper(
1191
+ checkout_intents.poll_until_awaiting_confirmation,
1192
+ )
1193
+ self.create_and_poll = async_to_raw_response_wrapper(
1194
+ checkout_intents.create_and_poll,
1195
+ )
1196
+ self.confirm_and_poll = async_to_raw_response_wrapper(
1197
+ checkout_intents.confirm_and_poll,
1198
+ )
445
1199
 
446
1200
 
447
1201
  class CheckoutIntentsResourceWithStreamingResponse:
@@ -460,6 +1214,18 @@ class CheckoutIntentsResourceWithStreamingResponse:
460
1214
  self.confirm = to_streamed_response_wrapper(
461
1215
  checkout_intents.confirm,
462
1216
  )
1217
+ self.poll_until_completed = to_streamed_response_wrapper(
1218
+ checkout_intents.poll_until_completed,
1219
+ )
1220
+ self.poll_until_awaiting_confirmation = to_streamed_response_wrapper(
1221
+ checkout_intents.poll_until_awaiting_confirmation,
1222
+ )
1223
+ self.create_and_poll = to_streamed_response_wrapper(
1224
+ checkout_intents.create_and_poll,
1225
+ )
1226
+ self.confirm_and_poll = to_streamed_response_wrapper(
1227
+ checkout_intents.confirm_and_poll,
1228
+ )
463
1229
 
464
1230
 
465
1231
  class AsyncCheckoutIntentsResourceWithStreamingResponse:
@@ -478,3 +1244,15 @@ class AsyncCheckoutIntentsResourceWithStreamingResponse:
478
1244
  self.confirm = async_to_streamed_response_wrapper(
479
1245
  checkout_intents.confirm,
480
1246
  )
1247
+ self.poll_until_completed = async_to_streamed_response_wrapper(
1248
+ checkout_intents.poll_until_completed,
1249
+ )
1250
+ self.poll_until_awaiting_confirmation = async_to_streamed_response_wrapper(
1251
+ checkout_intents.poll_until_awaiting_confirmation,
1252
+ )
1253
+ self.create_and_poll = async_to_streamed_response_wrapper(
1254
+ checkout_intents.create_and_poll,
1255
+ )
1256
+ self.confirm_and_poll = async_to_streamed_response_wrapper(
1257
+ checkout_intents.confirm_and_poll,
1258
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: checkout-intents
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: The official Python library for the Checkout Intents API
5
5
  Project-URL: Homepage, https://github.com/rye-com/checkout-intents-python
6
6
  Project-URL: Repository, https://github.com/rye-com/checkout-intents-python
@@ -88,6 +88,98 @@ we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)
88
88
  to add `CHECKOUT_INTENTS_API_KEY="My API Key"` to your `.env` file
89
89
  so that your API Key is not stored in source control.
90
90
 
91
+ ### Polling Helpers
92
+
93
+ This SDK includes helper methods for the asynchronous checkout flow. The recommended pattern follows Rye's two-phase checkout:
94
+
95
+ ```python
96
+ from checkout_intents import CheckoutIntents
97
+
98
+ client = CheckoutIntents()
99
+
100
+ # Phase 1: Create and wait for offer
101
+ intent = client.checkout_intents.create_and_poll(
102
+ buyer={
103
+ "address1": "123 Main St",
104
+ "city": "New York",
105
+ "country": "US",
106
+ "email": "john.doe@example.com",
107
+ "first_name": "John",
108
+ "last_name": "Doe",
109
+ "phone": "5555555555",
110
+ "postal_code": "10001",
111
+ "province": "NY",
112
+ },
113
+ product_url="https://example.com/product",
114
+ quantity=1,
115
+ )
116
+
117
+ # Handle failure during offer retrieval
118
+ if intent.state == "failed":
119
+ print(f"Failed: {intent.failure_reason}")
120
+ else:
121
+ # Review pricing with user
122
+ print(f"Total: {intent.offer.cost.total}")
123
+
124
+ # Phase 2: Confirm and wait for completion
125
+ completed = client.checkout_intents.confirm_and_poll(
126
+ intent.id,
127
+ payment_method={
128
+ "type": "stripe_token",
129
+ "stripe_token": "tok_visa",
130
+ },
131
+ )
132
+
133
+ print(f"Status: {completed.state}")
134
+ ```
135
+
136
+ For more examples, see the [`examples/`](https://github.com/rye-com/checkout-intents-python/tree/main/./examples) directory:
137
+
138
+ - [`complete-checkout-intent.py`](https://github.com/rye-com/checkout-intents-python/tree/main/./examples/complete-checkout-intent.py) - Recommended two-phase flow
139
+ - [`error-handling.py`](https://github.com/rye-com/checkout-intents-python/tree/main/./examples/error-handling.py) - Timeout and error handling with `PollTimeoutError`
140
+
141
+ Available polling methods:
142
+
143
+ - `create_and_poll()` - Create and poll until offer is ready (awaiting_confirmation or failed)
144
+ - `confirm_and_poll()` - Confirm and poll until completion (completed or failed)
145
+ - `poll_until_completed()` - Poll until completed or failed
146
+ - `poll_until_awaiting_confirmation()` - Poll until offer is ready or failed
147
+
148
+ All polling methods support customizable timeouts:
149
+
150
+ ```python
151
+ # Configure polling behavior
152
+ intent = client.checkout_intents.poll_until_completed(
153
+ intent_id,
154
+ poll_interval=5.0, # Poll every 5 seconds (default)
155
+ max_attempts=120, # Try up to 120 times, ~10 minutes (default)
156
+ )
157
+ ```
158
+
159
+ #### Handling Polling Timeouts
160
+
161
+ When polling operations exceed `max_attempts`, a `PollTimeoutError` is raised with helpful context:
162
+
163
+ ```python
164
+ from checkout_intents import CheckoutIntents, PollTimeoutError
165
+
166
+ client = CheckoutIntents()
167
+
168
+ try:
169
+ intent = client.checkout_intents.poll_until_completed(
170
+ intent_id,
171
+ poll_interval=5.0,
172
+ max_attempts=60,
173
+ )
174
+ except PollTimeoutError as e:
175
+ print(f"Polling timed out for intent: {e.intent_id}")
176
+ print(f"Attempted {e.attempts} times over {(e.attempts * e.poll_interval) / 1000}s")
177
+
178
+ # You can retrieve the current state manually
179
+ current_intent = client.checkout_intents.retrieve(e.intent_id)
180
+ print(f"Current state: {current_intent.state}")
181
+ ```
182
+
91
183
  ## Async usage
92
184
 
93
185
  Simply import `AsyncCheckoutIntents` instead of `CheckoutIntents` and use `await` with each API call:
@@ -258,6 +350,26 @@ Error codes are as follows:
258
350
  | 429 | `RateLimitError` |
259
351
  | >=500 | `InternalServerError` |
260
352
  | N/A | `APIConnectionError` |
353
+ | N/A | `PollTimeoutError` |
354
+
355
+ ### Polling Timeout Errors
356
+
357
+ When using polling helper methods, if the operation exceeds the configured `max_attempts`, a `PollTimeoutError` is raised. This error includes detailed context about the timeout:
358
+
359
+ ```python
360
+ from checkout_intents import CheckoutIntents, PollTimeoutError
361
+
362
+ try:
363
+ intent = client.checkout_intents.poll_until_completed("intent_id")
364
+ except PollTimeoutError as e:
365
+ # Access timeout details
366
+ print(f"Intent ID: {e.intent_id}")
367
+ print(f"Attempts: {e.attempts}")
368
+ print(f"Poll interval: {e.poll_interval}s")
369
+ print(f"Max attempts: {e.max_attempts}")
370
+ ```
371
+
372
+ See the [error-handling.py example](https://github.com/rye-com/checkout-intents-python/tree/main/./examples/error-handling.py) for more detailed timeout handling patterns.
261
373
 
262
374
  ### Retries
263
375
 
@@ -1,20 +1,20 @@
1
- checkout_intents/__init__.py,sha256=_Y4Qxqr-oIONV3M6jqtFF1yMOaH_iOT7Em7EmCroKAY,2787
1
+ checkout_intents/__init__.py,sha256=5m0_Ktnyya4CJ0vdWZbAv7qZMeR1VnXqN_MJtH3RJTM,2833
2
2
  checkout_intents/_base_client.py,sha256=eB43s-6F0_5tyH_e9uX-HAovJJu6D9DdMGVcI2zP4qc,67057
3
- checkout_intents/_client.py,sha256=jzpmFQeUmVwWQRYy5r2q77OAaO74BSghPPfR5FVrOPs,18814
3
+ checkout_intents/_client.py,sha256=px36nDPT8_lNISKkVjZhI_AZvFJ5KwRyVjQjPycia0s,21798
4
4
  checkout_intents/_compat.py,sha256=DQBVORjFb33zch24jzkhM14msvnzY7mmSmgDLaVFUM8,6562
5
5
  checkout_intents/_constants.py,sha256=S14PFzyN9-I31wiV7SmIlL5Ga0MLHxdvegInGdXH7tM,462
6
- checkout_intents/_exceptions.py,sha256=hwQX-kTpgIyHAc2zsdnyNU1h8fxQN_O4nw5srxROooU,3238
6
+ checkout_intents/_exceptions.py,sha256=oQn7Y6LzccgRDOtdg20l68bXtgf2tgVNKIrufJPDsuk,4252
7
7
  checkout_intents/_files.py,sha256=KnEzGi_O756MvKyJ4fOCW_u3JhOeWPQ4RsmDvqihDQU,3545
8
- checkout_intents/_models.py,sha256=_jpXNYoIJGzLCqeD4LcmiD4UgZKYMLg4cZ8TcWUn94I,30559
8
+ checkout_intents/_models.py,sha256=m6hDet9vdfyY7TuzbLCBYhMPoIN6hddOZp9qDZ-zfVU,31995
9
9
  checkout_intents/_qs.py,sha256=craIKyvPktJ94cvf9zn8j8ekG9dWJzhWv0ob34lIOv4,4828
10
10
  checkout_intents/_resource.py,sha256=X-eWffEBAgzTZvakLqNUjwgidMKfBFRmCeeOiuko4lg,1154
11
11
  checkout_intents/_response.py,sha256=sLqxWBxzmVqPsdr2x5z6d87h8jb1U-Huvh2YFg-U6nE,28876
12
12
  checkout_intents/_streaming.py,sha256=XOY20ljJEmOSf-nT8_KvPZipmgllR5sO400ePyMVLIY,10185
13
13
  checkout_intents/_types.py,sha256=4l2pVH65co-2pua9stzq-WV2VcGg9Hl4nRHd0lz5cko,7246
14
- checkout_intents/_version.py,sha256=EAZcX0kAfIydwo7T3jYb_O2Svh_deoksAmQGQqT2ytw,168
14
+ checkout_intents/_version.py,sha256=bLy0NHnQnIHbjqtwiAOnmDFC0CDZ2s-DkgnwlFLRcps,168
15
15
  checkout_intents/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  checkout_intents/_utils/__init__.py,sha256=7fch0GT9zpNnErbciSpUNa-SjTxxjY6kxHxKMOM4AGs,2305
17
- checkout_intents/_utils/_compat.py,sha256=D8gtAvjJQrDWt9upS0XaG9Rr5l1QhiAx_I_1utT_tt0,1195
17
+ checkout_intents/_utils/_compat.py,sha256=rN17SSvjMoQE1GmKFTLniRuG1sKj2WAD5VjdLPeRlF0,1231
18
18
  checkout_intents/_utils/_datetime_parse.py,sha256=bABTs0Bc6rabdFvnIwXjEhWL15TcRgWZ_6XGTqN8xUk,4204
19
19
  checkout_intents/_utils/_logs.py,sha256=dw1slZVPfWp0Z_jBGEQvr8TDY0uu3V7pJuexM_MG4pk,804
20
20
  checkout_intents/_utils/_proxy.py,sha256=aglnj2yBTDyGX9Akk2crZHrl10oqRmceUy2Zp008XEs,1975
@@ -24,11 +24,11 @@ checkout_intents/_utils/_streams.py,sha256=SMC90diFFecpEg_zgDRVbdR3hSEIgVVij4taD
24
24
  checkout_intents/_utils/_sync.py,sha256=HBnZkkBnzxtwOZe0212C4EyoRvxhTVtTrLFDz2_xVCg,1589
25
25
  checkout_intents/_utils/_transform.py,sha256=NjCzmnfqYrsAikUHQig6N9QfuTVbKipuP3ur9mcNF-E,15951
26
26
  checkout_intents/_utils/_typing.py,sha256=N_5PPuFNsaygbtA_npZd98SVN1LQQvFTKL6bkWPBZGU,4786
27
- checkout_intents/_utils/_utils.py,sha256=ugfUaneOK7I8h9b3656flwf5u_kthY0gvNuqvgOLoSU,12252
27
+ checkout_intents/_utils/_utils.py,sha256=g9ftElB09kVT6EVfCIlD_nUfANhDX5_vZO61FDWoIQI,12334
28
28
  checkout_intents/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
29
29
  checkout_intents/resources/__init__.py,sha256=sHRW0nwo1GnFBFPoTNohkEbvOk7au-R3MdfVn_yb8Ds,1120
30
30
  checkout_intents/resources/brands.py,sha256=LylMXefUeP4G540jtG8QHRAKwTMZHKI7vJhWnACS5Dc,6349
31
- checkout_intents/resources/checkout_intents.py,sha256=lqkOZkvAW-dMzcyReRaLllczKQG4WmIDCnZ9uTsUzH0,19054
31
+ checkout_intents/resources/checkout_intents.py,sha256=Q8BIh96n3nh6DbcBW7tSmCrJv7wF12QOTxyoD9Dit8s,49522
32
32
  checkout_intents/types/__init__.py,sha256=QHuqIegrR01arvPOp6GI2C8VUw3o37KNxr2KDtjh0eE,1098
33
33
  checkout_intents/types/base_checkout_intent.py,sha256=56Qp3ssWYmcdu6A9Z0pvL3ewm1LTvQHiboiJ7Aogx2M,644
34
34
  checkout_intents/types/brand_retrieve_response.py,sha256=1Z7yjrXl3NO1u9NFO5lDBJ-sucpEH501MUfUS_XeqbY,550
@@ -44,7 +44,7 @@ checkout_intents/types/payment_method.py,sha256=Mw85dZNa0z7Sr5FoQ9_nOUOjjKEHoE2x
44
44
  checkout_intents/types/payment_method_param.py,sha256=jbqAao2w7wxEHd_nshOthmZ53Ww7I4EiFmddpa_ejis,437
45
45
  checkout_intents/types/variant_selection.py,sha256=M8O6CpcpIlFqfPlQW_wFcJ6rwt459n6dJkjLkLB1klk,493
46
46
  checkout_intents/types/variant_selection_param.py,sha256=CMz4y1bDpuHNpnUmCcyeiYs6uwLBS9CJwo9iSpGMu-Q,589
47
- checkout_intents-0.1.0.dist-info/METADATA,sha256=5IyIauLcYzfpz0EOC7OHMBCH2x-_F9lvP1o7wHWh6hM,17734
48
- checkout_intents-0.1.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
49
- checkout_intents-0.1.0.dist-info/licenses/LICENSE,sha256=dRDmL6lFnLaphTaman8kAc21qY1IQ_qsAETk1F-b6jY,1056
50
- checkout_intents-0.1.0.dist-info/RECORD,,
47
+ checkout_intents-0.2.0.dist-info/METADATA,sha256=YDQk64h64eqBFNJjfG3vO_O4HvHQ686OmaTnzjPcfAY,21534
48
+ checkout_intents-0.2.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
49
+ checkout_intents-0.2.0.dist-info/licenses/LICENSE,sha256=dRDmL6lFnLaphTaman8kAc21qY1IQ_qsAETk1F-b6jY,1056
50
+ checkout_intents-0.2.0.dist-info/RECORD,,