checkout-intents 0.1.0__py3-none-any.whl → 0.3.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.3.0" # x-release-please-version
@@ -0,0 +1,89 @@
1
+ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
+
3
+ from typing import List, Generic, TypeVar, Optional
4
+ from typing_extensions import override
5
+
6
+ from pydantic import Field as FieldInfo
7
+
8
+ from ._models import BaseModel
9
+ from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
10
+
11
+ __all__ = ["CursorPaginationPageInfo", "SyncCursorPagination", "AsyncCursorPagination"]
12
+
13
+ _T = TypeVar("_T")
14
+
15
+
16
+ class CursorPaginationPageInfo(BaseModel):
17
+ end_cursor: Optional[str] = FieldInfo(alias="endCursor", default=None)
18
+
19
+ has_next_page: Optional[bool] = FieldInfo(alias="hasNextPage", default=None)
20
+
21
+ has_previous_page: Optional[bool] = FieldInfo(alias="hasPreviousPage", default=None)
22
+
23
+ start_cursor: Optional[str] = FieldInfo(alias="startCursor", default=None)
24
+
25
+
26
+ class SyncCursorPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
27
+ data: List[_T]
28
+ page_info: Optional[CursorPaginationPageInfo] = FieldInfo(alias="pageInfo", default=None)
29
+
30
+ @override
31
+ def _get_page_items(self) -> List[_T]:
32
+ data = self.data
33
+ if not data:
34
+ return []
35
+ return data
36
+
37
+ @override
38
+ def next_page_info(self) -> Optional[PageInfo]:
39
+ if self._options.params.get("before"):
40
+ start_cursor = None
41
+ if self.page_info is not None:
42
+ if self.page_info.start_cursor is not None:
43
+ start_cursor = self.page_info.start_cursor
44
+ if not start_cursor:
45
+ return None
46
+
47
+ return PageInfo(params={"before": start_cursor})
48
+
49
+ end_cursor = None
50
+ if self.page_info is not None:
51
+ if self.page_info.end_cursor is not None:
52
+ end_cursor = self.page_info.end_cursor
53
+ if not end_cursor:
54
+ return None
55
+
56
+ return PageInfo(params={"after": end_cursor})
57
+
58
+
59
+ class AsyncCursorPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
60
+ data: List[_T]
61
+ page_info: Optional[CursorPaginationPageInfo] = FieldInfo(alias="pageInfo", default=None)
62
+
63
+ @override
64
+ def _get_page_items(self) -> List[_T]:
65
+ data = self.data
66
+ if not data:
67
+ return []
68
+ return data
69
+
70
+ @override
71
+ def next_page_info(self) -> Optional[PageInfo]:
72
+ if self._options.params.get("before"):
73
+ start_cursor = None
74
+ if self.page_info is not None:
75
+ if self.page_info.start_cursor is not None:
76
+ start_cursor = self.page_info.start_cursor
77
+ if not start_cursor:
78
+ return None
79
+
80
+ return PageInfo(params={"before": start_cursor})
81
+
82
+ end_cursor = None
83
+ if self.page_info is not None:
84
+ if self.page_info.end_cursor is not None:
85
+ end_cursor = self.page_info.end_cursor
86
+ if not end_cursor:
87
+ return None
88
+
89
+ return PageInfo(params={"after": end_cursor})
@@ -53,8 +53,8 @@ class BrandsResource(SyncAPIResource):
53
53
  """
54
54
  Retrieve brand information by domain name
55
55
 
56
- Look up a brand by its domain name (e.g. "aloyoga.com"). Returns brand
57
- information including the marketplace type if the lookup succeeds.
56
+ Look up a brand by its domain name (e.g. "aloyoga.com" or "www.amazon.com").
57
+ Returns brand information including the marketplace type if the lookup succeeds.
58
58
 
59
59
  Args:
60
60
  domain: Represents a valid domain name string.
@@ -112,8 +112,8 @@ class AsyncBrandsResource(AsyncAPIResource):
112
112
  """
113
113
  Retrieve brand information by domain name
114
114
 
115
- Look up a brand by its domain name (e.g. "aloyoga.com"). Returns brand
116
- information including the marketplace type if the lookup succeeds.
115
+ Look up a brand by its domain name (e.g. "aloyoga.com" or "www.amazon.com").
116
+ Returns brand information including the marketplace type if the lookup succeeds.
117
117
 
118
118
  Args:
119
119
  domain: Represents a valid domain name string.