getanyapi 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 (68) hide show
  1. getanyapi/__init__.py +76 -0
  2. getanyapi/_account.py +132 -0
  3. getanyapi/_async_client.py +215 -0
  4. getanyapi/_client.py +274 -0
  5. getanyapi/_errors.py +115 -0
  6. getanyapi/_pagination.py +295 -0
  7. getanyapi/_transport.py +239 -0
  8. getanyapi/platforms/__init__.py +89 -0
  9. getanyapi/platforms/ahrefs.py +384 -0
  10. getanyapi/platforms/airbnb.py +120 -0
  11. getanyapi/platforms/alibaba.py +95 -0
  12. getanyapi/platforms/amazon.py +442 -0
  13. getanyapi/platforms/appstore.py +95 -0
  14. getanyapi/platforms/bluesky.py +215 -0
  15. getanyapi/platforms/booking.py +128 -0
  16. getanyapi/platforms/coinmarketcap.py +113 -0
  17. getanyapi/platforms/congress.py +106 -0
  18. getanyapi/platforms/dexscreener.py +104 -0
  19. getanyapi/platforms/ebay.py +215 -0
  20. getanyapi/platforms/email.py +166 -0
  21. getanyapi/platforms/facebook.py +2324 -0
  22. getanyapi/platforms/fiverr.py +122 -0
  23. getanyapi/platforms/github.py +954 -0
  24. getanyapi/platforms/glassdoor.py +93 -0
  25. getanyapi/platforms/google.py +232 -0
  26. getanyapi/platforms/google_ads.py +380 -0
  27. getanyapi/platforms/google_finance.py +170 -0
  28. getanyapi/platforms/google_shopping.py +103 -0
  29. getanyapi/platforms/hackernews.py +276 -0
  30. getanyapi/platforms/indeed.py +114 -0
  31. getanyapi/platforms/instagram.py +1868 -0
  32. getanyapi/platforms/linkedin.py +1064 -0
  33. getanyapi/platforms/maps.py +412 -0
  34. getanyapi/platforms/pandaexpress.py +262 -0
  35. getanyapi/platforms/person.py +96 -0
  36. getanyapi/platforms/pinterest.py +96 -0
  37. getanyapi/platforms/playstore.py +99 -0
  38. getanyapi/platforms/polymarket.py +109 -0
  39. getanyapi/platforms/realtor.py +104 -0
  40. getanyapi/platforms/reddit.py +582 -0
  41. getanyapi/platforms/redfin.py +95 -0
  42. getanyapi/platforms/rednote.py +807 -0
  43. getanyapi/platforms/sec.py +118 -0
  44. getanyapi/platforms/semrush.py +358 -0
  45. getanyapi/platforms/snapchat.py +146 -0
  46. getanyapi/platforms/social.py +105 -0
  47. getanyapi/platforms/spotify.py +588 -0
  48. getanyapi/platforms/substack.py +142 -0
  49. getanyapi/platforms/threads.py +358 -0
  50. getanyapi/platforms/tiktok.py +1827 -0
  51. getanyapi/platforms/tiktok_shop.py +536 -0
  52. getanyapi/platforms/tripadvisor.py +180 -0
  53. getanyapi/platforms/trustpilot.py +114 -0
  54. getanyapi/platforms/truthsocial.py +226 -0
  55. getanyapi/platforms/twitter.py +798 -0
  56. getanyapi/platforms/upwork.py +119 -0
  57. getanyapi/platforms/walmart.py +93 -0
  58. getanyapi/platforms/web.py +264 -0
  59. getanyapi/platforms/whatsapp.py +91 -0
  60. getanyapi/platforms/yahoo_finance.py +95 -0
  61. getanyapi/platforms/yelp.py +141 -0
  62. getanyapi/platforms/youtube.py +1452 -0
  63. getanyapi/platforms/zillow.py +218 -0
  64. getanyapi/py.typed +0 -0
  65. getanyapi/types.py +170 -0
  66. getanyapi-0.1.0.dist-info/METADATA +155 -0
  67. getanyapi-0.1.0.dist-info/RECORD +68 -0
  68. getanyapi-0.1.0.dist-info/WHEEL +4 -0
getanyapi/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """getanyapi - official typed Python SDK for AnyAPI.
2
+
3
+ from getanyapi import AnyAPI, AsyncAnyAPI, agent_signup, unwrap
4
+ from getanyapi import AnyAPIError, NotFoundError, RateLimitedError # etc.
5
+
6
+ Sync and async clients share generated per-platform namespaces (attached lazily)
7
+ through a single transport seam. Output models are pydantic v2; open provider
8
+ records round-trip unknown fields via ``.model_extra``. See ../../../SPEC.md
9
+ section 3 for the frozen public surface.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ._async_client import AsyncAnyAPI
15
+ from ._client import AnyAPI, agent_signup
16
+ from ._errors import (
17
+ AnyAPIError,
18
+ AuthenticationError,
19
+ BadRequestError,
20
+ ConnectionError,
21
+ InsufficientBalanceError,
22
+ NotFoundError,
23
+ RateLimitedError,
24
+ ResultNotFoundError,
25
+ TimeoutError,
26
+ UpstreamError,
27
+ )
28
+ from ._pagination import AsyncPaginator, Paginator
29
+ from .types import (
30
+ AccountProfile,
31
+ AgentSignupResult,
32
+ Balance,
33
+ BareRunResult,
34
+ CatalogEntry,
35
+ Output,
36
+ OutputFound,
37
+ OutputNotFound,
38
+ RequestOptions,
39
+ RunResult,
40
+ unwrap,
41
+ )
42
+
43
+ __version__ = "0.0.0"
44
+
45
+ __all__ = [
46
+ # clients + top-level functions
47
+ "AnyAPI",
48
+ "AsyncAnyAPI",
49
+ "agent_signup",
50
+ "unwrap",
51
+ # result + data models
52
+ "RunResult",
53
+ "BareRunResult",
54
+ "Output",
55
+ "OutputFound",
56
+ "OutputNotFound",
57
+ "Balance",
58
+ "AccountProfile",
59
+ "CatalogEntry",
60
+ "RequestOptions",
61
+ "AgentSignupResult",
62
+ # pagination
63
+ "Paginator",
64
+ "AsyncPaginator",
65
+ # errors
66
+ "AnyAPIError",
67
+ "BadRequestError",
68
+ "AuthenticationError",
69
+ "InsufficientBalanceError",
70
+ "NotFoundError",
71
+ "ResultNotFoundError",
72
+ "RateLimitedError",
73
+ "UpstreamError",
74
+ "ConnectionError",
75
+ "TimeoutError",
76
+ ]
getanyapi/_account.py ADDED
@@ -0,0 +1,132 @@
1
+ """Account, catalog, and agent-signup request/response mapping (SPEC 2.7, 3.7).
2
+
3
+ Pure functions that build the HTTP request pieces and map raw JSON bodies into
4
+ the public models. The sync and async clients share these so the wire mapping
5
+ lives in one place. Credits never appear in the public surface: the catalog
6
+ price is converted from internal credits to USD here, before any model is built.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, cast
12
+
13
+ from ._errors import AnyAPIError, error_for_status
14
+ from ._transport import as_dict, error_message
15
+ from .types import AccountProfile, AgentSignupResult, Balance, CatalogEntry
16
+
17
+ __all__ = [
18
+ "balance_path",
19
+ "me_path",
20
+ "catalog_request",
21
+ "describe_path",
22
+ "signup_request",
23
+ "parse_balance",
24
+ "parse_me",
25
+ "parse_catalog",
26
+ "parse_describe",
27
+ "parse_signup",
28
+ "map_error",
29
+ ]
30
+
31
+ _CREDIT_USD = 0.00001 # 1 credit = $0.00001 USD (internal unit, never surfaced)
32
+
33
+ balance_path = "/v1/balance"
34
+ me_path = "/v1/me"
35
+
36
+
37
+ def catalog_request(
38
+ query: str | None, category: str | None
39
+ ) -> tuple[str, dict[str, str]]:
40
+ """Path and query params for ``catalog()`` (GET /v1/apis)."""
41
+ params: dict[str, str] = {}
42
+ if query is not None:
43
+ params["query"] = query
44
+ if category is not None:
45
+ params["category"] = category
46
+ return "/v1/apis", params
47
+
48
+
49
+ def describe_path(slug: str) -> str:
50
+ return f"/v1/apis/{slug}"
51
+
52
+
53
+ def signup_request(
54
+ sponsor_email: str | None, label: str | None
55
+ ) -> dict[str, Any]:
56
+ """JSON body for ``agent_signup()`` (POST /agent/signup, no auth)."""
57
+ body: dict[str, Any] = {}
58
+ if sponsor_email is not None:
59
+ body["sponsorEmail"] = sponsor_email
60
+ if label is not None:
61
+ body["label"] = label
62
+ return body
63
+
64
+
65
+ def _split_slug(slug: str) -> tuple[str, str]:
66
+ """Derive (platform, action) from a dotted slug; action "" if no dot."""
67
+ platform, _, action = slug.partition(".")
68
+ return platform, action
69
+
70
+
71
+ def parse_balance(body: Any) -> Balance:
72
+ return Balance.model_validate(body)
73
+
74
+
75
+ def parse_me(body: Any) -> AccountProfile:
76
+ """Map /v1/me, dropping clerkUserId and signupGrantApplied (SPEC 2.7)."""
77
+ return AccountProfile.model_validate(body)
78
+
79
+
80
+ def _str_field(raw: dict[str, object], key: str) -> str:
81
+ value = raw.get(key)
82
+ return value if isinstance(value, str) else ""
83
+
84
+
85
+ def _from_credits(raw: dict[str, object]) -> float:
86
+ value = raw.get("fromCredits")
87
+ if isinstance(value, (int, float)):
88
+ return float(value)
89
+ return 0.0
90
+
91
+
92
+ def _to_catalog_entry(raw: dict[str, object]) -> CatalogEntry:
93
+ slug = _str_field(raw, "slug")
94
+ platform, action = _split_slug(slug)
95
+ return CatalogEntry.model_validate(
96
+ {
97
+ "slug": slug,
98
+ "platform": platform,
99
+ "action": action,
100
+ "name": _str_field(raw, "name"),
101
+ "category": _str_field(raw, "category"),
102
+ "description": _str_field(raw, "description"),
103
+ "priceUsd": _from_credits(raw) * _CREDIT_USD,
104
+ }
105
+ )
106
+
107
+
108
+ def parse_catalog(body: object) -> list[CatalogEntry]:
109
+ """Map {apis:[...]} into CatalogEntry[] (SPEC 2.7)."""
110
+ raw = as_dict(body).get("apis")
111
+ if not isinstance(raw, list):
112
+ return []
113
+ apis: list[object] = list(cast("list[object]", raw))
114
+ return [_to_catalog_entry(as_dict(item)) for item in apis]
115
+
116
+
117
+ def parse_describe(body: object) -> CatalogEntry:
118
+ """Map a single /v1/apis/{slug} entry into one CatalogEntry."""
119
+ return _to_catalog_entry(as_dict(body))
120
+
121
+
122
+ def parse_signup(body: Any) -> AgentSignupResult:
123
+ return AgentSignupResult.model_validate(body)
124
+
125
+
126
+ def map_error(
127
+ status: int, body: object, request_id: str | None
128
+ ) -> AnyAPIError:
129
+ """Map a non-2xx account/catalog response to the frozen error hierarchy."""
130
+ return error_for_status(
131
+ status, error_message(body, status), request_id=request_id
132
+ )
@@ -0,0 +1,215 @@
1
+ """Async AsyncAnyAPI client (SPEC 3.1).
2
+
3
+ Mirrors :class:`getanyapi.AnyAPI` with ``async def`` methods, ``aclose``, and an
4
+ async context manager. Generated async namespaces attach lazily via
5
+ ``__getattr__`` using the same ``getanyapi.platforms.REGISTRY`` table as the sync
6
+ client (see ``_client`` module docstring for the full contract); the async
7
+ client instantiates the registry's async class.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import importlib
14
+ import os
15
+ from typing import Any
16
+
17
+ import httpx
18
+
19
+ from . import _account
20
+ from . import _transport
21
+ from ._client import lookup_namespace
22
+ from ._errors import AnyAPIError, ConnectionError, TimeoutError
23
+ from ._transport import (
24
+ RetryState,
25
+ build_request,
26
+ is_retryable_error,
27
+ parse_raw,
28
+ )
29
+ from .types import (
30
+ AccountProfile,
31
+ Balance,
32
+ CatalogEntry,
33
+ RequestOptions,
34
+ RunResult,
35
+ )
36
+
37
+ __all__ = ["AsyncAnyAPI"]
38
+
39
+ _DEFAULT_BASE_URL = "https://api.getanyapi.com"
40
+
41
+
42
+ def _resolve_api_key(api_key: str | None) -> str:
43
+ key = api_key if api_key is not None else os.environ.get("ANYAPI_API_KEY")
44
+ if not key:
45
+ raise AnyAPIError(
46
+ "no API key: pass api_key= or set ANYAPI_API_KEY", status=0
47
+ )
48
+ return key
49
+
50
+
51
+ class AsyncAnyAPI:
52
+ """Asynchronous AnyAPI client (SPEC 3.1)."""
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ api_key: str | None = None,
58
+ base_url: str = _DEFAULT_BASE_URL,
59
+ timeout: float = 60.0,
60
+ max_retries: int = 2,
61
+ http_client: httpx.AsyncClient | None = None,
62
+ ) -> None:
63
+ self._api_key = _resolve_api_key(api_key)
64
+ self._base_url = base_url.rstrip("/")
65
+ self._timeout = timeout
66
+ self._max_retries = max_retries
67
+ self._owns_client = http_client is None
68
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
69
+ self._namespaces: dict[str, Any] = {}
70
+
71
+ # -- generated namespace attachment -----------------------------------
72
+
73
+ def __getattr__(self, name: str) -> Any:
74
+ if name.startswith("_"):
75
+ raise AttributeError(name)
76
+ cache = self.__dict__.get("_namespaces")
77
+ if cache is not None and name in cache:
78
+ return cache[name]
79
+ entry = lookup_namespace(name)
80
+ if entry is None:
81
+ raise AttributeError(name)
82
+ module_suffix, _sync_class, async_class = entry
83
+ module = importlib.import_module(f"getanyapi.platforms.{module_suffix}")
84
+ namespace = getattr(module, async_class)(self)
85
+ if cache is not None:
86
+ cache[name] = namespace
87
+ return namespace
88
+
89
+ # -- transport seam ---------------------------------------------------
90
+
91
+ async def _arun_raw(
92
+ self,
93
+ slug: str,
94
+ input: dict[str, Any],
95
+ options: RequestOptions | None = None,
96
+ ) -> dict[str, Any]:
97
+ """Execute one SKU run with retries, returning the raw JSON dict.
98
+
99
+ The raw seam generated methods call (SPEC N2): the generated code
100
+ validates this dict into its concrete ``RunResult[XData]`` /
101
+ ``BareRunResult[XData]`` model, so there is no double-parse.
102
+ """
103
+ timeout = self._timeout
104
+ max_retries = self._max_retries
105
+ if options:
106
+ opt_timeout = options.get("timeout")
107
+ if opt_timeout is not None:
108
+ timeout = float(opt_timeout)
109
+ opt_retries = options.get("max_retries")
110
+ if opt_retries is not None:
111
+ max_retries = int(opt_retries)
112
+ request = build_request(
113
+ base_url=self._base_url,
114
+ slug=slug,
115
+ input=input,
116
+ api_key=self._api_key,
117
+ options=options,
118
+ timeout=timeout,
119
+ )
120
+ retry = RetryState(max_retries)
121
+ while True:
122
+ response: httpx.Response | None = None
123
+ try:
124
+ response = await self._http.send(request)
125
+ return parse_raw(response)
126
+ except AnyAPIError as exc:
127
+ if is_retryable_error(exc) and retry.can_retry:
128
+ await asyncio.sleep(retry.next_delay(response))
129
+ continue
130
+ raise
131
+ except httpx.TimeoutException as exc:
132
+ raise TimeoutError(
133
+ str(exc) or "request timed out", status=0
134
+ ) from exc
135
+ except httpx.HTTPError as exc:
136
+ if retry.can_retry:
137
+ await asyncio.sleep(retry.next_delay(None))
138
+ continue
139
+ raise ConnectionError(
140
+ str(exc) or "connection failed", status=0
141
+ ) from exc
142
+
143
+ async def _arun(
144
+ self,
145
+ slug: str,
146
+ input: dict[str, Any],
147
+ options: RequestOptions | None = None,
148
+ ) -> RunResult[Any]:
149
+ """Execute one SKU run and parse the generic found-data envelope."""
150
+ raw = await self._arun_raw(slug, input, options)
151
+ return _transport.validate_run_result(raw)
152
+
153
+ async def run(
154
+ self,
155
+ slug: str,
156
+ input: dict[str, Any],
157
+ *,
158
+ options: RequestOptions | None = None,
159
+ ) -> RunResult[Any]:
160
+ """Generic typed run for any SKU by slug (SPEC 3.1)."""
161
+ return await self._arun(slug, input, options)
162
+
163
+ # -- account + catalog ------------------------------------------------
164
+
165
+ async def _get(
166
+ self, path: str, params: dict[str, str] | None = None
167
+ ) -> object:
168
+ url = f"{self._base_url}{path}"
169
+ headers = {
170
+ "Authorization": f"Bearer {self._api_key}",
171
+ "Accept": "application/json",
172
+ }
173
+ response = await self._http.get(
174
+ url, params=params or {}, headers=headers
175
+ )
176
+ return self._json_or_raise(response)
177
+
178
+ def _json_or_raise(self, response: httpx.Response) -> object:
179
+ request_id = response.headers.get("x-request-id")
180
+ body: object = None
181
+ try:
182
+ body = response.json()
183
+ except ValueError:
184
+ body = None
185
+ if response.status_code != 200:
186
+ raise _account.map_error(response.status_code, body, request_id)
187
+ return body
188
+
189
+ async def balance(self) -> Balance:
190
+ return _account.parse_balance(await self._get(_account.balance_path))
191
+
192
+ async def me(self) -> AccountProfile:
193
+ return _account.parse_me(await self._get(_account.me_path))
194
+
195
+ async def catalog(
196
+ self, *, query: str | None = None, category: str | None = None
197
+ ) -> list[CatalogEntry]:
198
+ path, params = _account.catalog_request(query, category)
199
+ return _account.parse_catalog(await self._get(path, params))
200
+
201
+ async def describe(self, slug: str) -> CatalogEntry:
202
+ body = await self._get(_account.describe_path(slug))
203
+ return _account.parse_describe(body)
204
+
205
+ # -- lifecycle --------------------------------------------------------
206
+
207
+ async def aclose(self) -> None:
208
+ if self._owns_client:
209
+ await self._http.aclose()
210
+
211
+ async def __aenter__(self) -> AsyncAnyAPI:
212
+ return self
213
+
214
+ async def __aexit__(self, *exc: object) -> None:
215
+ await self.aclose()
getanyapi/_client.py ADDED
@@ -0,0 +1,274 @@
1
+ """Sync AnyAPI client (SPEC 3.1) and the module-level agent_signup helper.
2
+
3
+ Generated per-platform namespaces attach lazily. On first access,
4
+ ``__getattr__(name)`` looks the name up in the generated registry
5
+ ``getanyapi.platforms.REGISTRY`` and imports the platform module, instantiating its
6
+ sync ``Namespace`` class bound to this client.
7
+
8
+ Namespace-attachment contract (target for the py-emitter):
9
+ ``getanyapi.platforms.__init__`` exposes a module-level dict named ``REGISTRY``
10
+ mapping the client attribute name (the snake_case platform, e.g. "amazon")
11
+ to a 3-tuple ``(module_suffix, sync_class_name, async_class_name)``:
12
+
13
+ REGISTRY: dict[str, tuple[str, str, str]] = {
14
+ "amazon": ("amazon", "AmazonNamespace", "AsyncAmazonNamespace"),
15
+ ...
16
+ }
17
+
18
+ Each generated module ``getanyapi.platforms.<module_suffix>`` defines both the
19
+ sync class ``<sync_class_name>`` and the async class ``<async_class_name>``.
20
+ Each namespace class has ``__init__(self, client)`` storing the client, and
21
+ per-SKU methods that call ``client._run(slug, input, options)`` (sync) or
22
+ ``client._arun(...)`` (async), plus ``iter_*`` methods returning a
23
+ ``Paginator``/``AsyncPaginator`` via ``getanyapi._pagination.paginate`` /
24
+ ``apaginate``. The sync client instantiates the sync class; the async client
25
+ instantiates the async class. Both look the attribute up by the SAME key in
26
+ the SAME registry, so one generated table drives both clients.
27
+
28
+ Namespace instances are cached on the client instance after first access.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import importlib
34
+ import os
35
+ from typing import Any
36
+
37
+ import httpx
38
+
39
+ from . import _account, _transport
40
+ from ._errors import AnyAPIError, ConnectionError, TimeoutError
41
+ from ._transport import (
42
+ RetryState,
43
+ build_request,
44
+ is_retryable_error,
45
+ parse_raw,
46
+ )
47
+ from .types import (
48
+ AccountProfile,
49
+ AgentSignupResult,
50
+ Balance,
51
+ CatalogEntry,
52
+ RequestOptions,
53
+ RunResult,
54
+ )
55
+
56
+ __all__ = ["AnyAPI", "agent_signup"]
57
+
58
+ _DEFAULT_BASE_URL = "https://api.getanyapi.com"
59
+
60
+
61
+ def lookup_namespace(name: str) -> tuple[str, str, str] | None:
62
+ """Look ``name`` up in the generated platform registry (typed accessor)."""
63
+ try:
64
+ module = importlib.import_module("getanyapi.platforms")
65
+ except ImportError:
66
+ return None
67
+ registry: dict[str, tuple[str, str, str]] = getattr(module, "REGISTRY", {})
68
+ return registry.get(name)
69
+
70
+
71
+ def _resolve_api_key(api_key: str | None) -> str:
72
+ key = api_key if api_key is not None else os.environ.get("ANYAPI_API_KEY")
73
+ if not key:
74
+ raise AnyAPIError(
75
+ "no API key: pass api_key= or set ANYAPI_API_KEY", status=0
76
+ )
77
+ return key
78
+
79
+
80
+ class AnyAPI:
81
+ """Synchronous AnyAPI client (SPEC 3.1).
82
+
83
+ Generated per-platform namespaces (``client.amazon``, ``client.facebook``,
84
+ ...) attach lazily via ``__getattr__``; see the module docstring for the
85
+ registry contract the emitter targets.
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ *,
91
+ api_key: str | None = None,
92
+ base_url: str = _DEFAULT_BASE_URL,
93
+ timeout: float = 60.0,
94
+ max_retries: int = 2,
95
+ http_client: httpx.Client | None = None,
96
+ ) -> None:
97
+ self._api_key = _resolve_api_key(api_key)
98
+ self._base_url = base_url.rstrip("/")
99
+ self._timeout = timeout
100
+ self._max_retries = max_retries
101
+ self._owns_client = http_client is None
102
+ self._http = http_client or httpx.Client(timeout=timeout)
103
+ self._namespaces: dict[str, Any] = {}
104
+
105
+ # -- generated namespace attachment -----------------------------------
106
+
107
+ def __getattr__(self, name: str) -> Any:
108
+ # Only called for attributes not found normally.
109
+ if name.startswith("_"):
110
+ raise AttributeError(name)
111
+ cache = self.__dict__.get("_namespaces")
112
+ if cache is not None and name in cache:
113
+ return cache[name]
114
+ entry = lookup_namespace(name)
115
+ if entry is None:
116
+ raise AttributeError(name)
117
+ module_suffix, sync_class, _async_class = entry
118
+ module = importlib.import_module(f"getanyapi.platforms.{module_suffix}")
119
+ namespace = getattr(module, sync_class)(self)
120
+ if cache is not None:
121
+ cache[name] = namespace
122
+ return namespace
123
+
124
+ # -- transport seam ---------------------------------------------------
125
+
126
+ def _run_raw(
127
+ self,
128
+ slug: str,
129
+ input: dict[str, Any],
130
+ options: RequestOptions | None = None,
131
+ ) -> dict[str, Any]:
132
+ """Execute one SKU run with retries, returning the raw JSON dict.
133
+
134
+ The raw seam generated methods call (SPEC N2): the generated code
135
+ validates this dict into its concrete ``RunResult[XData]`` /
136
+ ``BareRunResult[XData]`` model, so there is no double-parse.
137
+ """
138
+ timeout = self._timeout
139
+ max_retries = self._max_retries
140
+ if options:
141
+ opt_timeout = options.get("timeout")
142
+ if opt_timeout is not None:
143
+ timeout = float(opt_timeout)
144
+ opt_retries = options.get("max_retries")
145
+ if opt_retries is not None:
146
+ max_retries = int(opt_retries)
147
+ request = build_request(
148
+ base_url=self._base_url,
149
+ slug=slug,
150
+ input=input,
151
+ api_key=self._api_key,
152
+ options=options,
153
+ timeout=timeout,
154
+ )
155
+ retry = RetryState(max_retries)
156
+ while True:
157
+ response: httpx.Response | None = None
158
+ try:
159
+ response = self._http.send(request)
160
+ return parse_raw(response)
161
+ except AnyAPIError as exc:
162
+ if is_retryable_error(exc) and retry.can_retry:
163
+ _transport.sleep(retry.next_delay(response))
164
+ continue
165
+ raise
166
+ except httpx.TimeoutException as exc:
167
+ raise TimeoutError(
168
+ str(exc) or "request timed out", status=0
169
+ ) from exc
170
+ except httpx.HTTPError as exc:
171
+ if retry.can_retry:
172
+ _transport.sleep(retry.next_delay(None))
173
+ continue
174
+ raise ConnectionError(
175
+ str(exc) or "connection failed", status=0
176
+ ) from exc
177
+
178
+ def _run(
179
+ self,
180
+ slug: str,
181
+ input: dict[str, Any],
182
+ options: RequestOptions | None = None,
183
+ ) -> RunResult[Any]:
184
+ """Execute one SKU run and parse the generic found-data envelope."""
185
+ raw = self._run_raw(slug, input, options)
186
+ return _transport.validate_run_result(raw)
187
+
188
+ def run(
189
+ self,
190
+ slug: str,
191
+ input: dict[str, Any],
192
+ *,
193
+ options: RequestOptions | None = None,
194
+ ) -> RunResult[Any]:
195
+ """Generic typed run for any SKU by slug (SPEC 3.1)."""
196
+ return self._run(slug, input, options)
197
+
198
+ # -- account + catalog ------------------------------------------------
199
+
200
+ def _get(self, path: str, params: dict[str, str] | None = None) -> object:
201
+ url = f"{self._base_url}{path}"
202
+ headers = {
203
+ "Authorization": f"Bearer {self._api_key}",
204
+ "Accept": "application/json",
205
+ }
206
+ response = self._http.get(url, params=params or {}, headers=headers)
207
+ return self._json_or_raise(response)
208
+
209
+ def _json_or_raise(self, response: httpx.Response) -> object:
210
+ request_id = response.headers.get("x-request-id")
211
+ body: object = None
212
+ try:
213
+ body = response.json()
214
+ except ValueError:
215
+ body = None
216
+ if response.status_code != 200:
217
+ raise _account.map_error(response.status_code, body, request_id)
218
+ return body
219
+
220
+ def balance(self) -> Balance:
221
+ return _account.parse_balance(self._get(_account.balance_path))
222
+
223
+ def me(self) -> AccountProfile:
224
+ return _account.parse_me(self._get(_account.me_path))
225
+
226
+ def catalog(
227
+ self, *, query: str | None = None, category: str | None = None
228
+ ) -> list[CatalogEntry]:
229
+ path, params = _account.catalog_request(query, category)
230
+ return _account.parse_catalog(self._get(path, params))
231
+
232
+ def describe(self, slug: str) -> CatalogEntry:
233
+ return _account.parse_describe(self._get(_account.describe_path(slug)))
234
+
235
+ # -- lifecycle --------------------------------------------------------
236
+
237
+ def close(self) -> None:
238
+ if self._owns_client:
239
+ self._http.close()
240
+
241
+ def __enter__(self) -> AnyAPI:
242
+ return self
243
+
244
+ def __exit__(self, *exc: object) -> None:
245
+ self.close()
246
+
247
+
248
+ def agent_signup(
249
+ *,
250
+ base_url: str = _DEFAULT_BASE_URL,
251
+ sponsor_email: str | None = None,
252
+ label: str | None = None,
253
+ ) -> AgentSignupResult:
254
+ """Self-signup for an API key with no auth (SPEC 3.1, POST /agent/signup)."""
255
+ body = _account.signup_request(sponsor_email, label)
256
+ with httpx.Client(timeout=60.0) as http:
257
+ response = http.post(
258
+ f"{base_url.rstrip('/')}/agent/signup",
259
+ json=body,
260
+ headers={
261
+ "Content-Type": "application/json",
262
+ "Accept": "application/json",
263
+ },
264
+ )
265
+ request_id = response.headers.get("x-request-id")
266
+ parsed: object = None
267
+ try:
268
+ parsed = response.json()
269
+ except ValueError:
270
+ parsed = None
271
+ # The gateway returns 200 for agent signup; accept 200 only (SPEC S7).
272
+ if response.status_code != 200:
273
+ raise _account.map_error(response.status_code, parsed, request_id)
274
+ return _account.parse_signup(parsed)