bidkit 0.1.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.
Files changed (62) hide show
  1. bidkit/__init__.py +49 -0
  2. bidkit/auth.py +477 -0
  3. bidkit/client.py +212 -0
  4. bidkit/config.py +216 -0
  5. bidkit/errors.py +68 -0
  6. bidkit/generated/__init__.py +1 -0
  7. bidkit/generated/models/__init__.py +1 -0
  8. bidkit/generated/models/buy_browse.py +3162 -0
  9. bidkit/generated/models/buy_deal.py +604 -0
  10. bidkit/generated/models/buy_feed.py +1377 -0
  11. bidkit/generated/models/buy_marketing.py +891 -0
  12. bidkit/generated/models/buy_marketplace_insights.py +432 -0
  13. bidkit/generated/models/buy_offer.py +310 -0
  14. bidkit/generated/models/buy_order.py +1363 -0
  15. bidkit/generated/models/cancellation.py +6 -0
  16. bidkit/generated/models/case.py +99 -0
  17. bidkit/generated/models/commerce_catalog.py +318 -0
  18. bidkit/generated/models/commerce_charity.py +412 -0
  19. bidkit/generated/models/commerce_feedback.py +756 -0
  20. bidkit/generated/models/commerce_identity.py +511 -0
  21. bidkit/generated/models/commerce_media.py +333 -0
  22. bidkit/generated/models/commerce_message.py +390 -0
  23. bidkit/generated/models/commerce_notification.py +399 -0
  24. bidkit/generated/models/commerce_taxonomy.py +511 -0
  25. bidkit/generated/models/commerce_translation.py +121 -0
  26. bidkit/generated/models/commerce_vero.py +560 -0
  27. bidkit/generated/models/developer_analytics.py +116 -0
  28. bidkit/generated/models/developer_client_registration.py +140 -0
  29. bidkit/generated/models/developer_key_management.py +111 -0
  30. bidkit/generated/models/inquiry.py +95 -0
  31. bidkit/generated/models/return_.py +69 -0
  32. bidkit/generated/models/sell_account_v1.py +1929 -0
  33. bidkit/generated/models/sell_account_v2.py +1020 -0
  34. bidkit/generated/models/sell_analytics.py +629 -0
  35. bidkit/generated/models/sell_compliance.py +235 -0
  36. bidkit/generated/models/sell_edelivery_international_shipping.py +1993 -0
  37. bidkit/generated/models/sell_feed.py +853 -0
  38. bidkit/generated/models/sell_finances.py +1348 -0
  39. bidkit/generated/models/sell_fulfillment.py +2645 -0
  40. bidkit/generated/models/sell_inventory.py +2949 -0
  41. bidkit/generated/models/sell_leads.py +234 -0
  42. bidkit/generated/models/sell_listing.py +169 -0
  43. bidkit/generated/models/sell_logistics.py +1027 -0
  44. bidkit/generated/models/sell_marketing.py +3656 -0
  45. bidkit/generated/models/sell_metadata.py +2552 -0
  46. bidkit/generated/models/sell_negotiation.py +414 -0
  47. bidkit/generated/models/sell_recommendation.py +149 -0
  48. bidkit/generated/models/sell_stores.py +214 -0
  49. bidkit/generated/resources.py +19909 -0
  50. bidkit/models.py +43 -0
  51. bidkit/notifications.py +196 -0
  52. bidkit/pagination.py +137 -0
  53. bidkit/py.typed +0 -0
  54. bidkit/resource.py +166 -0
  55. bidkit/retry.py +81 -0
  56. bidkit/signing.py +162 -0
  57. bidkit/transport.py +499 -0
  58. bidkit-0.1.0.dist-info/METADATA +425 -0
  59. bidkit-0.1.0.dist-info/RECORD +62 -0
  60. bidkit-0.1.0.dist-info/WHEEL +4 -0
  61. bidkit-0.1.0.dist-info/licenses/LICENSE +21 -0
  62. bidkit-0.1.0.dist-info/licenses/NOTICE +15 -0
bidkit/client.py ADDED
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import httpx
8
+
9
+ from .auth import EbayAuth, OAuthTokens, TokenCache
10
+ from .config import DEFAULT_TIMEOUT, EbayConfig
11
+ from .errors import EbayConfigError
12
+ from .transport import AsyncEbayTransport, EbayTransport
13
+
14
+
15
+ def _authorization_url(
16
+ config: EbayConfig,
17
+ *,
18
+ state: str | None,
19
+ prompt: str | None,
20
+ scopes: tuple[str, ...] | None,
21
+ ) -> str:
22
+ if not config.app_id or not config.ru_name:
23
+ raise EbayConfigError("app_id and ru_name are required to build an authorization URL")
24
+ return EbayAuth.authorization_url(
25
+ app_id=config.app_id,
26
+ ru_name=config.ru_name,
27
+ scopes=scopes or config.scopes,
28
+ sandbox=config.sandbox,
29
+ state=state,
30
+ prompt=prompt,
31
+ )
32
+
33
+
34
+ def _overridden_config(config: EbayConfig, overrides: Mapping[str, Any]) -> EbayConfig:
35
+ """Re-validate the explicitly-set fields plus ``overrides`` into a new config."""
36
+ data = {name: getattr(config, name) for name in config.model_fields_set}
37
+ return EbayConfig(**{**data, **overrides})
38
+
39
+
40
+ if TYPE_CHECKING:
41
+ from .generated.resources import (
42
+ AsyncBuyNamespace,
43
+ AsyncCommerceNamespace,
44
+ AsyncDeveloperNamespace,
45
+ AsyncPostOrderNamespace,
46
+ AsyncSellNamespace,
47
+ BuyNamespace,
48
+ CommerceNamespace,
49
+ DeveloperNamespace,
50
+ PostOrderNamespace,
51
+ SellNamespace,
52
+ )
53
+
54
+
55
+ class EbayClient:
56
+ buy: BuyNamespace
57
+ commerce: CommerceNamespace
58
+ developer: DeveloperNamespace
59
+ post_order: PostOrderNamespace
60
+ sell: SellNamespace
61
+
62
+ def __init__(
63
+ self,
64
+ config: EbayConfig | Mapping[str, Any] | None = None,
65
+ *,
66
+ http_client: httpx.Client | None = None,
67
+ token_cache: TokenCache | None = None,
68
+ ) -> None:
69
+ self.config = (
70
+ config if isinstance(config, EbayConfig) else EbayConfig.model_validate(config or {})
71
+ )
72
+ self._owns_http = http_client is None
73
+ self.http = http_client or httpx.Client(timeout=self.config.timeout or DEFAULT_TIMEOUT)
74
+ self.auth = EbayAuth(self.config, token_cache)
75
+ self._transport = EbayTransport(self.config, self.auth, self.http)
76
+
77
+ from .generated.resources import install_sync_namespaces
78
+
79
+ install_sync_namespaces(self)
80
+
81
+ @classmethod
82
+ def from_env(cls) -> EbayClient:
83
+ return cls(EbayConfig.from_env())
84
+
85
+ def with_options(self, **overrides: Any) -> EbayClient:
86
+ """A scoped client with config fields overridden, e.g.::
87
+
88
+ client.with_options(timeout=5.0, max_retries=0).sell.inventory.get_offers(...)
89
+
90
+ The returned client shares this client's HTTP connection pool and token cache
91
+ (no new connections, no re-auth); closing it is a no-op, and any
92
+ :class:`EbayConfig` field (``timeout``, ``max_retries``, ``marketplace_id``,
93
+ ``retry_backoff``, ...) can be overridden.
94
+ """
95
+ return type(self)(
96
+ _overridden_config(self.config, overrides),
97
+ http_client=self.http,
98
+ token_cache=self.auth.cache,
99
+ )
100
+
101
+ def authorization_url(
102
+ self,
103
+ *,
104
+ state: str | None = None,
105
+ prompt: str | None = None,
106
+ scopes: tuple[str, ...] | None = None,
107
+ ) -> str:
108
+ """Build the eBay consent URL to send a user to (authorization-code flow)."""
109
+ return _authorization_url(self.config, state=state, prompt=prompt, scopes=scopes)
110
+
111
+ def exchange_code(self, code: str, *, ru_name: str | None = None) -> OAuthTokens:
112
+ """Exchange a consent ``code`` for tokens and authenticate this client with them.
113
+
114
+ Note: this mutates ``self.config`` — the returned ``refresh_token`` is stored on
115
+ the config so subsequent calls run as the newly authorized user.
116
+ """
117
+ tokens = self.auth.exchange_code(self.http, code, ru_name=ru_name)
118
+ self.auth.seed_tokens(tokens)
119
+ return tokens
120
+
121
+ def close(self) -> None:
122
+ # Only close the transport the SDK created; never an injected, caller-owned client.
123
+ if self._owns_http:
124
+ self.http.close()
125
+
126
+ def __enter__(self) -> EbayClient:
127
+ return self
128
+
129
+ def __exit__(self, *_exc: object) -> None:
130
+ self.close()
131
+
132
+ def _request(self, **kwargs: Any) -> Any:
133
+ return self._transport.request(**kwargs)
134
+
135
+ def _stream(self, **kwargs: Any) -> AbstractContextManager[httpx.Response]:
136
+ return self._transport.stream(**kwargs)
137
+
138
+
139
+ class AsyncEbayClient:
140
+ buy: AsyncBuyNamespace
141
+ commerce: AsyncCommerceNamespace
142
+ developer: AsyncDeveloperNamespace
143
+ post_order: AsyncPostOrderNamespace
144
+ sell: AsyncSellNamespace
145
+
146
+ def __init__(
147
+ self,
148
+ config: EbayConfig | Mapping[str, Any] | None = None,
149
+ *,
150
+ http_client: httpx.AsyncClient | None = None,
151
+ token_cache: TokenCache | None = None,
152
+ ) -> None:
153
+ self.config = (
154
+ config if isinstance(config, EbayConfig) else EbayConfig.model_validate(config or {})
155
+ )
156
+ self._owns_http = http_client is None
157
+ self.http = http_client or httpx.AsyncClient(timeout=self.config.timeout or DEFAULT_TIMEOUT)
158
+ self.auth = EbayAuth(self.config, token_cache)
159
+ self._transport = AsyncEbayTransport(self.config, self.auth, self.http)
160
+
161
+ from .generated.resources import install_async_namespaces
162
+
163
+ install_async_namespaces(self)
164
+
165
+ @classmethod
166
+ def from_env(cls) -> AsyncEbayClient:
167
+ return cls(EbayConfig.from_env())
168
+
169
+ def with_options(self, **overrides: Any) -> AsyncEbayClient:
170
+ """A scoped client with config fields overridden — see :meth:`EbayClient.with_options`."""
171
+ return type(self)(
172
+ _overridden_config(self.config, overrides),
173
+ http_client=self.http,
174
+ token_cache=self.auth.cache,
175
+ )
176
+
177
+ def authorization_url(
178
+ self,
179
+ *,
180
+ state: str | None = None,
181
+ prompt: str | None = None,
182
+ scopes: tuple[str, ...] | None = None,
183
+ ) -> str:
184
+ """Build the eBay consent URL to send a user to (authorization-code flow)."""
185
+ return _authorization_url(self.config, state=state, prompt=prompt, scopes=scopes)
186
+
187
+ async def exchange_code(self, code: str, *, ru_name: str | None = None) -> OAuthTokens:
188
+ """Exchange a consent ``code`` for tokens and authenticate this client with them.
189
+
190
+ Note: this mutates ``self.config`` — the returned ``refresh_token`` is stored on
191
+ the config so subsequent calls run as the newly authorized user.
192
+ """
193
+ tokens = await self.auth.async_exchange_code(self.http, code, ru_name=ru_name)
194
+ self.auth.seed_tokens(tokens)
195
+ return tokens
196
+
197
+ async def close(self) -> None:
198
+ # Only close the transport the SDK created; never an injected, caller-owned client.
199
+ if self._owns_http:
200
+ await self.http.aclose()
201
+
202
+ async def __aenter__(self) -> AsyncEbayClient:
203
+ return self
204
+
205
+ async def __aexit__(self, *_exc: object) -> None:
206
+ await self.close()
207
+
208
+ async def _request(self, **kwargs: Any) -> Any:
209
+ return await self._transport.request(**kwargs)
210
+
211
+ def _stream(self, **kwargs: Any) -> AbstractAsyncContextManager[httpx.Response]:
212
+ return self._transport.stream(**kwargs)
bidkit/config.py ADDED
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, ValidationError
10
+
11
+ SecretValue = SecretStr | str
12
+
13
+ logger = logging.getLogger("bidkit.config")
14
+
15
+ # Timeout for HTTP clients the SDK creates itself when EbayConfig.timeout is unset.
16
+ DEFAULT_TIMEOUT = 30.0
17
+
18
+
19
+ class EbaySigningConfig(BaseModel):
20
+ """Credentials for eBay digital-signature (message signing).
21
+
22
+ Required by the Finances API and several payout/refund operations. ``jwe`` is the
23
+ encrypted public key returned by the Key Management API; ``private_key`` is the
24
+ matching PEM (or bare base64 PKCS#8) private key; ``digest`` selects the
25
+ content-digest algorithm.
26
+ """
27
+
28
+ model_config = ConfigDict(extra="forbid")
29
+
30
+ jwe: str
31
+ private_key: SecretValue
32
+ digest: Literal["sha256", "sha512"] = "sha256"
33
+ # Escape hatch: sign every request instead of only the operations eBay
34
+ # requires signatures for, in case eBay expands the signed-API list before
35
+ # the SDK catches up.
36
+ sign_all: bool = False
37
+
38
+ @property
39
+ def private_key_value(self) -> str:
40
+ value = _secret_value(self.private_key)
41
+ if value is None:
42
+ raise ValueError("signing private_key is empty")
43
+ return value
44
+
45
+ @classmethod
46
+ def from_key_file(cls, path: str | Path) -> EbaySigningConfig:
47
+ """Load signing material from an ebay-cli style ``signing-key.json``."""
48
+ data = json.loads(Path(path).expanduser().read_text())
49
+ private_key = data.get("privateKeyPem") or data.get("privateKey")
50
+ if not data.get("jwe") or not private_key:
51
+ raise ValueError(f"Signing key file {path} is missing 'jwe' and/or a private key")
52
+ # ebay-cli files sometimes put the KEY algorithm (e.g. "ED25519") in
53
+ # "cipher"; only sha256/sha512 are content-digest choices, anything else
54
+ # falls back to the default.
55
+ raw_digest = str(data.get("cipher") or "").lower()
56
+ digest: Literal["sha256", "sha512"] = "sha512" if raw_digest == "sha512" else "sha256"
57
+ return cls(jwe=data["jwe"], private_key=private_key, digest=digest)
58
+
59
+
60
+ class EbayConfig(BaseModel):
61
+ model_config = ConfigDict(extra="forbid")
62
+
63
+ app_id: str | None = None
64
+ cert_id: SecretValue | None = None
65
+ dev_id: str | None = None
66
+ ru_name: str | None = None
67
+ sandbox: bool = False
68
+
69
+ marketplace_id: str = "EBAY_US"
70
+ accept_language: str | None = "en-US"
71
+ content_language: str | None = "en-US"
72
+
73
+ access_token: SecretValue | None = None
74
+ refresh_token: SecretValue | None = None
75
+ scopes: tuple[str, ...] = ("https://api.ebay.com/oauth/api_scope",)
76
+ auto_refresh: bool = True
77
+
78
+ signing: EbaySigningConfig | None = None
79
+
80
+ # None means "use the HTTP client's own timeout" (30 s for SDK-created clients);
81
+ # a set value is applied per request and wins over an injected client's default.
82
+ timeout: float | None = Field(default=None, gt=0)
83
+ base_url_override: str | None = None
84
+
85
+ # Retry policy for transient responses (429 + transient 5xx) and connection errors.
86
+ max_retries: int = Field(default=2, ge=0)
87
+ retry_statuses: tuple[int, ...] = (429, 500, 502, 503, 504)
88
+ retry_backoff: float = Field(default=0.5, ge=0)
89
+ retry_max_backoff: float = Field(default=60.0, ge=0)
90
+ respect_retry_after: bool = True
91
+
92
+ @classmethod
93
+ def from_env(cls, prefix: str = "EBAY_") -> EbayConfig:
94
+ def value(name: str) -> str | None:
95
+ raw = os.getenv(prefix + name)
96
+ return raw if raw not in (None, "") else None
97
+
98
+ scopes = value("SCOPES")
99
+ data: dict[str, Any] = {
100
+ "app_id": value("APP_ID"),
101
+ "cert_id": value("CERT_ID"),
102
+ "dev_id": value("DEV_ID"),
103
+ "ru_name": value("RU_NAME"),
104
+ "marketplace_id": value("MARKETPLACE_ID") or "EBAY_US",
105
+ "accept_language": value("ACCEPT_LANGUAGE") or "en-US",
106
+ "content_language": value("CONTENT_LANGUAGE") or "en-US",
107
+ "access_token": value("ACCESS_TOKEN"),
108
+ "refresh_token": value("REFRESH_TOKEN"),
109
+ "base_url_override": value("BASE_URL"),
110
+ }
111
+ sandbox = value("SANDBOX")
112
+ if sandbox is not None:
113
+ data["sandbox"] = sandbox.strip().lower() in {"1", "true", "yes", "on"}
114
+ if scopes:
115
+ data["scopes"] = tuple(scope for scope in scopes.split() if scope)
116
+
117
+ signing_key_file = value("SIGNING_KEY_FILE")
118
+ signing_jwe = value("SIGNING_JWE")
119
+ if signing_key_file:
120
+ data["signing"] = EbaySigningConfig.from_key_file(signing_key_file)
121
+ else:
122
+ signing_private_key = value("SIGNING_PRIVATE_KEY")
123
+ if signing_jwe and signing_private_key:
124
+ data["signing"] = EbaySigningConfig.model_validate(
125
+ {
126
+ "jwe": signing_jwe,
127
+ "private_key": signing_private_key,
128
+ "digest": value("SIGNING_DIGEST") or "sha256",
129
+ }
130
+ )
131
+
132
+ return cls(**{key: val for key, val in data.items() if val is not None})
133
+
134
+ @classmethod
135
+ def from_file(
136
+ cls,
137
+ path: str | Path = "~/.config/ebay-cli/config.json",
138
+ *,
139
+ signing_key_file: str | Path | None = None,
140
+ ) -> EbayConfig:
141
+ """Load an ebay-cli style ``config.json``.
142
+
143
+ Credentials live under a ``credentials`` object (or at the top level) with the
144
+ aliases ebay-cli uses: ``app_id``/``client_id``, ``cert_id``/``client_secret``,
145
+ ``ru_name``/``redirect_uri``, ``granted_scopes``/``scopes``. Top-level
146
+ ``environment`` ("sandbox"/"production") and ``marketplace_default`` map to
147
+ ``sandbox`` and ``marketplace_id``. A ``signing-key.json`` next to the config is
148
+ picked up automatically; pass ``signing_key_file`` to point elsewhere.
149
+ """
150
+ config_path = Path(path).expanduser()
151
+ raw = json.loads(config_path.read_text())
152
+ creds = raw.get("credentials", raw)
153
+ if not isinstance(creds, dict):
154
+ raise ValueError(f"Config file {config_path} has no usable 'credentials' object")
155
+
156
+ def alias(*names: str) -> Any:
157
+ return next((creds[name] for name in names if creds.get(name)), None)
158
+
159
+ scopes = alias("granted_scopes", "scopes")
160
+ data: dict[str, Any] = {
161
+ "app_id": alias("app_id", "client_id"),
162
+ "cert_id": alias("cert_id", "client_secret"),
163
+ "dev_id": alias("dev_id"),
164
+ "ru_name": alias("ru_name", "redirect_uri"),
165
+ "refresh_token": alias("refresh_token"),
166
+ "marketplace_id": raw.get("marketplace_default"),
167
+ }
168
+ if raw.get("environment"):
169
+ data["sandbox"] = raw["environment"] == "sandbox"
170
+ if scopes:
171
+ data["scopes"] = tuple(scopes.split()) if isinstance(scopes, str) else tuple(scopes)
172
+
173
+ if signing_key_file is not None:
174
+ # Explicitly requested: let a broken file raise.
175
+ data["signing"] = EbaySigningConfig.from_key_file(signing_key_file)
176
+ else:
177
+ sibling = config_path.with_name("signing-key.json")
178
+ if sibling.exists():
179
+ # Auto-detected: best effort, since most flows never need signing
180
+ # and a stub or foreign-format file must not break config loading.
181
+ try:
182
+ data["signing"] = EbaySigningConfig.from_key_file(sibling)
183
+ except (ValueError, ValidationError) as exc:
184
+ logger.warning("ignoring unusable signing key file %s: %s", sibling, exc)
185
+
186
+ return cls(**{key: val for key, val in data.items() if val is not None})
187
+
188
+ def api_root(self, subdomain: str = "api") -> str:
189
+ if self.base_url_override:
190
+ return self.base_url_override.rstrip("/")
191
+ sandbox = ".sandbox" if self.sandbox else ""
192
+ return f"https://{subdomain}{sandbox}.ebay.com"
193
+
194
+ @property
195
+ def oauth_token_url(self) -> str:
196
+ return f"{self.api_root('api')}/identity/v1/oauth2/token"
197
+
198
+ @property
199
+ def client_secret(self) -> str | None:
200
+ return _secret_value(self.cert_id)
201
+
202
+ @property
203
+ def bearer_token(self) -> str | None:
204
+ return _secret_value(self.access_token)
205
+
206
+ @property
207
+ def refresh_token_value(self) -> str | None:
208
+ return _secret_value(self.refresh_token)
209
+
210
+
211
+ def _secret_value(value: SecretValue | None) -> str | None:
212
+ if value is None:
213
+ return None
214
+ if isinstance(value, SecretStr):
215
+ return value.get_secret_value()
216
+ return value
bidkit/errors.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+ import orjson
7
+
8
+
9
+ class EbaySDKError(Exception):
10
+ """Base exception for all SDK-level failures."""
11
+
12
+
13
+ class EbayConfigError(EbaySDKError):
14
+ """Raised when SDK configuration is incomplete or invalid."""
15
+
16
+
17
+ class EbayTransportError(EbaySDKError):
18
+ """Raised for network-level failures before eBay returns a response."""
19
+
20
+
21
+ class EbayAuthError(EbaySDKError):
22
+ """Raised when OAuth token acquisition or refresh fails."""
23
+
24
+
25
+ class EbayAPIError(EbaySDKError):
26
+ def __init__(
27
+ self,
28
+ message: str,
29
+ *,
30
+ status_code: int,
31
+ response: httpx.Response | None = None,
32
+ payload: Any = None,
33
+ request_id: str | None = None,
34
+ ) -> None:
35
+ super().__init__(message)
36
+ self.status_code = status_code
37
+ self.response = response
38
+ self.payload = payload
39
+ self.request_id = request_id
40
+
41
+ @classmethod
42
+ def from_response(cls, response: httpx.Response) -> EbayAPIError:
43
+ payload: Any
44
+ try:
45
+ payload = orjson.loads(response.content)
46
+ except orjson.JSONDecodeError:
47
+ payload = response.text
48
+
49
+ message = response.reason_phrase or f"eBay API returned HTTP {response.status_code}"
50
+ if isinstance(payload, dict):
51
+ errors = payload.get("errors")
52
+ if isinstance(errors, list) and errors:
53
+ first = errors[0]
54
+ if isinstance(first, dict):
55
+ message = first.get("message") or first.get("longMessage") or message
56
+ elif payload.get("error_description"):
57
+ message = str(payload["error_description"])
58
+ elif payload.get("message"):
59
+ message = str(payload["message"])
60
+
61
+ return cls(
62
+ message,
63
+ status_code=response.status_code,
64
+ response=response,
65
+ payload=payload,
66
+ request_id=response.headers.get("x-ebay-c-request-id")
67
+ or response.headers.get("x-ebay-request-id"),
68
+ )
@@ -0,0 +1 @@
1
+ """Generated OpenAPI clients and models."""
@@ -0,0 +1 @@
1
+ """Generated Pydantic model modules."""