getanyapi 0.1.0__tar.gz

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 (78) hide show
  1. getanyapi-0.1.0/.gitignore +28 -0
  2. getanyapi-0.1.0/PKG-INFO +155 -0
  3. getanyapi-0.1.0/README.md +126 -0
  4. getanyapi-0.1.0/pyproject.toml +69 -0
  5. getanyapi-0.1.0/src/getanyapi/__init__.py +76 -0
  6. getanyapi-0.1.0/src/getanyapi/_account.py +132 -0
  7. getanyapi-0.1.0/src/getanyapi/_async_client.py +215 -0
  8. getanyapi-0.1.0/src/getanyapi/_client.py +274 -0
  9. getanyapi-0.1.0/src/getanyapi/_errors.py +115 -0
  10. getanyapi-0.1.0/src/getanyapi/_pagination.py +295 -0
  11. getanyapi-0.1.0/src/getanyapi/_transport.py +239 -0
  12. getanyapi-0.1.0/src/getanyapi/platforms/__init__.py +89 -0
  13. getanyapi-0.1.0/src/getanyapi/platforms/ahrefs.py +384 -0
  14. getanyapi-0.1.0/src/getanyapi/platforms/airbnb.py +120 -0
  15. getanyapi-0.1.0/src/getanyapi/platforms/alibaba.py +95 -0
  16. getanyapi-0.1.0/src/getanyapi/platforms/amazon.py +442 -0
  17. getanyapi-0.1.0/src/getanyapi/platforms/appstore.py +95 -0
  18. getanyapi-0.1.0/src/getanyapi/platforms/bluesky.py +215 -0
  19. getanyapi-0.1.0/src/getanyapi/platforms/booking.py +128 -0
  20. getanyapi-0.1.0/src/getanyapi/platforms/coinmarketcap.py +113 -0
  21. getanyapi-0.1.0/src/getanyapi/platforms/congress.py +106 -0
  22. getanyapi-0.1.0/src/getanyapi/platforms/dexscreener.py +104 -0
  23. getanyapi-0.1.0/src/getanyapi/platforms/ebay.py +215 -0
  24. getanyapi-0.1.0/src/getanyapi/platforms/email.py +166 -0
  25. getanyapi-0.1.0/src/getanyapi/platforms/facebook.py +2324 -0
  26. getanyapi-0.1.0/src/getanyapi/platforms/fiverr.py +122 -0
  27. getanyapi-0.1.0/src/getanyapi/platforms/github.py +954 -0
  28. getanyapi-0.1.0/src/getanyapi/platforms/glassdoor.py +93 -0
  29. getanyapi-0.1.0/src/getanyapi/platforms/google.py +232 -0
  30. getanyapi-0.1.0/src/getanyapi/platforms/google_ads.py +380 -0
  31. getanyapi-0.1.0/src/getanyapi/platforms/google_finance.py +170 -0
  32. getanyapi-0.1.0/src/getanyapi/platforms/google_shopping.py +103 -0
  33. getanyapi-0.1.0/src/getanyapi/platforms/hackernews.py +276 -0
  34. getanyapi-0.1.0/src/getanyapi/platforms/indeed.py +114 -0
  35. getanyapi-0.1.0/src/getanyapi/platforms/instagram.py +1868 -0
  36. getanyapi-0.1.0/src/getanyapi/platforms/linkedin.py +1064 -0
  37. getanyapi-0.1.0/src/getanyapi/platforms/maps.py +412 -0
  38. getanyapi-0.1.0/src/getanyapi/platforms/pandaexpress.py +262 -0
  39. getanyapi-0.1.0/src/getanyapi/platforms/person.py +96 -0
  40. getanyapi-0.1.0/src/getanyapi/platforms/pinterest.py +96 -0
  41. getanyapi-0.1.0/src/getanyapi/platforms/playstore.py +99 -0
  42. getanyapi-0.1.0/src/getanyapi/platforms/polymarket.py +109 -0
  43. getanyapi-0.1.0/src/getanyapi/platforms/realtor.py +104 -0
  44. getanyapi-0.1.0/src/getanyapi/platforms/reddit.py +582 -0
  45. getanyapi-0.1.0/src/getanyapi/platforms/redfin.py +95 -0
  46. getanyapi-0.1.0/src/getanyapi/platforms/rednote.py +807 -0
  47. getanyapi-0.1.0/src/getanyapi/platforms/sec.py +118 -0
  48. getanyapi-0.1.0/src/getanyapi/platforms/semrush.py +358 -0
  49. getanyapi-0.1.0/src/getanyapi/platforms/snapchat.py +146 -0
  50. getanyapi-0.1.0/src/getanyapi/platforms/social.py +105 -0
  51. getanyapi-0.1.0/src/getanyapi/platforms/spotify.py +588 -0
  52. getanyapi-0.1.0/src/getanyapi/platforms/substack.py +142 -0
  53. getanyapi-0.1.0/src/getanyapi/platforms/threads.py +358 -0
  54. getanyapi-0.1.0/src/getanyapi/platforms/tiktok.py +1827 -0
  55. getanyapi-0.1.0/src/getanyapi/platforms/tiktok_shop.py +536 -0
  56. getanyapi-0.1.0/src/getanyapi/platforms/tripadvisor.py +180 -0
  57. getanyapi-0.1.0/src/getanyapi/platforms/trustpilot.py +114 -0
  58. getanyapi-0.1.0/src/getanyapi/platforms/truthsocial.py +226 -0
  59. getanyapi-0.1.0/src/getanyapi/platforms/twitter.py +798 -0
  60. getanyapi-0.1.0/src/getanyapi/platforms/upwork.py +119 -0
  61. getanyapi-0.1.0/src/getanyapi/platforms/walmart.py +93 -0
  62. getanyapi-0.1.0/src/getanyapi/platforms/web.py +264 -0
  63. getanyapi-0.1.0/src/getanyapi/platforms/whatsapp.py +91 -0
  64. getanyapi-0.1.0/src/getanyapi/platforms/yahoo_finance.py +95 -0
  65. getanyapi-0.1.0/src/getanyapi/platforms/yelp.py +141 -0
  66. getanyapi-0.1.0/src/getanyapi/platforms/youtube.py +1452 -0
  67. getanyapi-0.1.0/src/getanyapi/platforms/zillow.py +218 -0
  68. getanyapi-0.1.0/src/getanyapi/py.typed +0 -0
  69. getanyapi-0.1.0/src/getanyapi/types.py +170 -0
  70. getanyapi-0.1.0/tests/conftest.py +103 -0
  71. getanyapi-0.1.0/tests/test_account.py +147 -0
  72. getanyapi-0.1.0/tests/test_client.py +125 -0
  73. getanyapi-0.1.0/tests/test_envelope.py +72 -0
  74. getanyapi-0.1.0/tests/test_errors.py +60 -0
  75. getanyapi-0.1.0/tests/test_fixture_roundtrip.py +52 -0
  76. getanyapi-0.1.0/tests/test_fixture_sweep.py +240 -0
  77. getanyapi-0.1.0/tests/test_pagination.py +190 -0
  78. getanyapi-0.1.0/tests/test_transport.py +231 -0
@@ -0,0 +1,28 @@
1
+ # dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # build output
6
+ dist/
7
+ *.tsbuildinfo
8
+
9
+ # python
10
+ __pycache__/
11
+ *.egg-info/
12
+ .venv/
13
+ venv/
14
+ .mypy_cache/
15
+ .pyright/
16
+ .pytest_cache/
17
+ .ruff_cache/
18
+ build/
19
+
20
+ # generator outputs: ir.json and fixtures.json ARE committed (byte-determinism is guarded
21
+ # by `pnpm generate:check`; the fixture integration suites read fixtures.json directly).
22
+ # ir.sample.json remains the hand-built 3-SKU reference.
23
+
24
+ # os / editor
25
+ .DS_Store
26
+ *.log
27
+ .idea/
28
+ .vscode/
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: getanyapi
3
+ Version: 0.1.0
4
+ Summary: Official typed Python SDK for AnyAPI: any API, one wallet, USD, no subscriptions.
5
+ Project-URL: Homepage, https://getanyapi.com
6
+ Project-URL: Documentation, https://getanyapi.com/docs
7
+ Project-URL: Repository, https://github.com/getanyapi-com/sdks
8
+ Project-URL: Issues, https://github.com/getanyapi-com/sdks/issues
9
+ Author-email: AnyAPI <support@getanyapi.com>
10
+ License-Expression: MIT
11
+ Keywords: anyapi,api,getanyapi,scraping,sdk
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: pydantic>=2.5
21
+ Requires-Dist: typing-extensions>=4.7
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.11; extra == 'dev'
24
+ Requires-Dist: pyright>=1.1.380; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.6; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # getanyapi
31
+
32
+ Official typed Python SDK for [AnyAPI](https://getanyapi.com): any API, one wallet, USD, no
33
+ subscriptions. Reach hundreds of scraping and data APIs through one interface and one key; pay
34
+ per request in real US dollars. httpx + pydantic v2, Python 3.10+, sync and async clients.
35
+
36
+ ```bash
37
+ pip install getanyapi
38
+ ```
39
+
40
+ ## Quickstart
41
+
42
+ ```python
43
+ from getanyapi import AnyAPI
44
+
45
+ client = AnyAPI() # reads ANYAPI_API_KEY from the environment
46
+ res = client.reddit.search(query="mechanical keyboard")
47
+ for post in res.output.posts:
48
+ print(post.title, post.score)
49
+ print("charged", res.cost_usd, "USD")
50
+ ```
51
+
52
+ Async:
53
+
54
+ ```python
55
+ from getanyapi import AsyncAnyAPI
56
+
57
+ async with AsyncAnyAPI() as client:
58
+ res = await client.google.search(query="best coffee maker")
59
+ ```
60
+
61
+ ## Inputs vs outputs (naming asymmetry)
62
+
63
+ Input keyword arguments mirror the wire API verbatim (camelCase where the API uses it), because
64
+ they are sent as-is. Output models are Pythonic: attributes are snake_case with a wire alias
65
+ (`item.reviews_count` reads the wire `reviewsCount`), and `model_dump(by_alias=True)` reproduces
66
+ the wire shape. Open provider records round-trip unknown fields via `.model_extra`.
67
+
68
+ ## Not found vs error
69
+
70
+ A successful call always returns. For most SKUs the payload is wrapped in a `found` flag:
71
+ `res.output.found` is `False` when the upstream had no matching entity (not an error). Use
72
+ `unwrap` to get the data or raise `ResultNotFoundError` when empty:
73
+
74
+ ```python
75
+ from getanyapi import unwrap, ResultNotFoundError
76
+
77
+ res = client.amazon.reviews(product="B07FZ8S74R")
78
+ try:
79
+ data = unwrap(res) # the typed data payload, or raises
80
+ except ResultNotFoundError:
81
+ ... # empty result (found: False), not an HTTP failure
82
+ ```
83
+
84
+ `ResultNotFoundError` subclasses `NotFoundError`, so `except NotFoundError` catches both an
85
+ HTTP 404 and an empty result; catch `ResultNotFoundError` to handle only empty results. A few
86
+ SKUs (e.g. `reddit.search`) return their data object directly as `output` with no `found`
87
+ wrapper; `unwrap` returns it as-is and never raises for those.
88
+
89
+ ## Pagination
90
+
91
+ Paginated SKUs expose an `iter_*` method that yields validated item models across pages and
92
+ follows the cursor for you. Call `.pages()` on it to walk whole results (each has its own
93
+ `cost_usd`).
94
+
95
+ ```python
96
+ for post in client.reddit.iter_search(query="coffee", options={"max_items": 100}):
97
+ print(post.title) # a validated model, not a dict
98
+
99
+ for page in client.reddit.iter_search(query="coffee").pages():
100
+ print(page.cost_usd)
101
+ ```
102
+
103
+ ## Request options (context-cost savers)
104
+
105
+ Pass `options=` to shape the response. These trim what comes back but do NOT change the price:
106
+
107
+ ```python
108
+ res = client.google.search(
109
+ query="coffee",
110
+ options={"fields": ["title", "link"], "max_items": 5, "summary": True},
111
+ )
112
+ ```
113
+
114
+ `options` also carries per-call `timeout` and `max_retries` overrides.
115
+
116
+ ## Errors and retries
117
+
118
+ | Class | HTTP | Meaning |
119
+ | --- | --- | --- |
120
+ | `BadRequestError` | 400 | Input failed validation |
121
+ | `AuthenticationError` | 401 | Missing or invalid API key |
122
+ | `InsufficientBalanceError` | 402 | Wallet balance or per-key cap exceeded |
123
+ | `NotFoundError` | 404 | Slug or resource does not exist |
124
+ | `ResultNotFoundError` | - | `unwrap` on an empty found-data result |
125
+ | `RateLimitedError` | 429 | Too many requests (retried automatically) |
126
+ | `UpstreamError` | 502 | An upstream backend failed |
127
+ | `ConnectionError` | 0 | Network or transport failure (retried) |
128
+ | `TimeoutError` | 0 | Request exceeded its timeout (not retried) |
129
+
130
+ All extend `AnyAPIError` (with `.status` and `.request_id`). Retries cover only 429 and network
131
+ failures, with jittered exponential backoff honoring `Retry-After`. Default `max_retries` is 2
132
+ (up to 3 attempts); set it on the client (`AnyAPI(max_retries=...)`) or per request via
133
+ `options`. Timeouts are never retried.
134
+
135
+ ## Agent signup
136
+
137
+ Bootstrap a key with no account (for autonomous agents):
138
+
139
+ ```python
140
+ from getanyapi import agent_signup, AnyAPI
141
+
142
+ result = agent_signup(label="my-agent")
143
+ client = AnyAPI(api_key=result.secret)
144
+ ```
145
+
146
+ The key ships with a small starter credit and a per-key spend cap; a human funds it by claiming
147
+ it at `result.claim_url`.
148
+
149
+ ## Docs
150
+
151
+ Full API reference and catalog: [getanyapi.com/docs](https://getanyapi.com/docs).
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,126 @@
1
+ # getanyapi
2
+
3
+ Official typed Python SDK for [AnyAPI](https://getanyapi.com): any API, one wallet, USD, no
4
+ subscriptions. Reach hundreds of scraping and data APIs through one interface and one key; pay
5
+ per request in real US dollars. httpx + pydantic v2, Python 3.10+, sync and async clients.
6
+
7
+ ```bash
8
+ pip install getanyapi
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from getanyapi import AnyAPI
15
+
16
+ client = AnyAPI() # reads ANYAPI_API_KEY from the environment
17
+ res = client.reddit.search(query="mechanical keyboard")
18
+ for post in res.output.posts:
19
+ print(post.title, post.score)
20
+ print("charged", res.cost_usd, "USD")
21
+ ```
22
+
23
+ Async:
24
+
25
+ ```python
26
+ from getanyapi import AsyncAnyAPI
27
+
28
+ async with AsyncAnyAPI() as client:
29
+ res = await client.google.search(query="best coffee maker")
30
+ ```
31
+
32
+ ## Inputs vs outputs (naming asymmetry)
33
+
34
+ Input keyword arguments mirror the wire API verbatim (camelCase where the API uses it), because
35
+ they are sent as-is. Output models are Pythonic: attributes are snake_case with a wire alias
36
+ (`item.reviews_count` reads the wire `reviewsCount`), and `model_dump(by_alias=True)` reproduces
37
+ the wire shape. Open provider records round-trip unknown fields via `.model_extra`.
38
+
39
+ ## Not found vs error
40
+
41
+ A successful call always returns. For most SKUs the payload is wrapped in a `found` flag:
42
+ `res.output.found` is `False` when the upstream had no matching entity (not an error). Use
43
+ `unwrap` to get the data or raise `ResultNotFoundError` when empty:
44
+
45
+ ```python
46
+ from getanyapi import unwrap, ResultNotFoundError
47
+
48
+ res = client.amazon.reviews(product="B07FZ8S74R")
49
+ try:
50
+ data = unwrap(res) # the typed data payload, or raises
51
+ except ResultNotFoundError:
52
+ ... # empty result (found: False), not an HTTP failure
53
+ ```
54
+
55
+ `ResultNotFoundError` subclasses `NotFoundError`, so `except NotFoundError` catches both an
56
+ HTTP 404 and an empty result; catch `ResultNotFoundError` to handle only empty results. A few
57
+ SKUs (e.g. `reddit.search`) return their data object directly as `output` with no `found`
58
+ wrapper; `unwrap` returns it as-is and never raises for those.
59
+
60
+ ## Pagination
61
+
62
+ Paginated SKUs expose an `iter_*` method that yields validated item models across pages and
63
+ follows the cursor for you. Call `.pages()` on it to walk whole results (each has its own
64
+ `cost_usd`).
65
+
66
+ ```python
67
+ for post in client.reddit.iter_search(query="coffee", options={"max_items": 100}):
68
+ print(post.title) # a validated model, not a dict
69
+
70
+ for page in client.reddit.iter_search(query="coffee").pages():
71
+ print(page.cost_usd)
72
+ ```
73
+
74
+ ## Request options (context-cost savers)
75
+
76
+ Pass `options=` to shape the response. These trim what comes back but do NOT change the price:
77
+
78
+ ```python
79
+ res = client.google.search(
80
+ query="coffee",
81
+ options={"fields": ["title", "link"], "max_items": 5, "summary": True},
82
+ )
83
+ ```
84
+
85
+ `options` also carries per-call `timeout` and `max_retries` overrides.
86
+
87
+ ## Errors and retries
88
+
89
+ | Class | HTTP | Meaning |
90
+ | --- | --- | --- |
91
+ | `BadRequestError` | 400 | Input failed validation |
92
+ | `AuthenticationError` | 401 | Missing or invalid API key |
93
+ | `InsufficientBalanceError` | 402 | Wallet balance or per-key cap exceeded |
94
+ | `NotFoundError` | 404 | Slug or resource does not exist |
95
+ | `ResultNotFoundError` | - | `unwrap` on an empty found-data result |
96
+ | `RateLimitedError` | 429 | Too many requests (retried automatically) |
97
+ | `UpstreamError` | 502 | An upstream backend failed |
98
+ | `ConnectionError` | 0 | Network or transport failure (retried) |
99
+ | `TimeoutError` | 0 | Request exceeded its timeout (not retried) |
100
+
101
+ All extend `AnyAPIError` (with `.status` and `.request_id`). Retries cover only 429 and network
102
+ failures, with jittered exponential backoff honoring `Retry-After`. Default `max_retries` is 2
103
+ (up to 3 attempts); set it on the client (`AnyAPI(max_retries=...)`) or per request via
104
+ `options`. Timeouts are never retried.
105
+
106
+ ## Agent signup
107
+
108
+ Bootstrap a key with no account (for autonomous agents):
109
+
110
+ ```python
111
+ from getanyapi import agent_signup, AnyAPI
112
+
113
+ result = agent_signup(label="my-agent")
114
+ client = AnyAPI(api_key=result.secret)
115
+ ```
116
+
117
+ The key ships with a small starter credit and a per-key spend cap; a human funds it by claiming
118
+ it at `result.claim_url`.
119
+
120
+ ## Docs
121
+
122
+ Full API reference and catalog: [getanyapi.com/docs](https://getanyapi.com/docs).
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "getanyapi"
7
+ version = "0.1.0"
8
+ description = "Official typed Python SDK for AnyAPI: any API, one wallet, USD, no subscriptions."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "AnyAPI", email = "support@getanyapi.com" }]
13
+ keywords = ["getanyapi", "anyapi", "api", "scraping", "sdk"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Typing :: Typed",
20
+ "License :: OSI Approved :: MIT License",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.27",
24
+ "pydantic>=2.5",
25
+ "typing_extensions>=4.7",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://getanyapi.com"
30
+ Documentation = "https://getanyapi.com/docs"
31
+ Repository = "https://github.com/getanyapi-com/sdks"
32
+ Issues = "https://github.com/getanyapi-com/sdks/issues"
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8.0",
37
+ "pytest-asyncio>=0.23",
38
+ "pyright>=1.1.380",
39
+ "mypy>=1.11",
40
+ "ruff>=0.6",
41
+ ]
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/getanyapi"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ pythonpath = ["tests"]
49
+ asyncio_mode = "auto"
50
+
51
+ [tool.mypy]
52
+ strict = true
53
+ python_version = "3.10"
54
+ files = ["src/getanyapi"]
55
+
56
+ [tool.pyright]
57
+ include = ["src/getanyapi"]
58
+ pythonVersion = "3.10"
59
+ typeCheckingMode = "strict"
60
+ venvPath = "."
61
+ venv = ".venv"
62
+
63
+ [tool.ruff]
64
+ line-length = 88
65
+ target-version = "py310"
66
+ src = ["src", "tests"]
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "I", "UP", "B"]
@@ -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
+ ]
@@ -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
+ )