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.
- getanyapi-0.1.0/.gitignore +28 -0
- getanyapi-0.1.0/PKG-INFO +155 -0
- getanyapi-0.1.0/README.md +126 -0
- getanyapi-0.1.0/pyproject.toml +69 -0
- getanyapi-0.1.0/src/getanyapi/__init__.py +76 -0
- getanyapi-0.1.0/src/getanyapi/_account.py +132 -0
- getanyapi-0.1.0/src/getanyapi/_async_client.py +215 -0
- getanyapi-0.1.0/src/getanyapi/_client.py +274 -0
- getanyapi-0.1.0/src/getanyapi/_errors.py +115 -0
- getanyapi-0.1.0/src/getanyapi/_pagination.py +295 -0
- getanyapi-0.1.0/src/getanyapi/_transport.py +239 -0
- getanyapi-0.1.0/src/getanyapi/platforms/__init__.py +89 -0
- getanyapi-0.1.0/src/getanyapi/platforms/ahrefs.py +384 -0
- getanyapi-0.1.0/src/getanyapi/platforms/airbnb.py +120 -0
- getanyapi-0.1.0/src/getanyapi/platforms/alibaba.py +95 -0
- getanyapi-0.1.0/src/getanyapi/platforms/amazon.py +442 -0
- getanyapi-0.1.0/src/getanyapi/platforms/appstore.py +95 -0
- getanyapi-0.1.0/src/getanyapi/platforms/bluesky.py +215 -0
- getanyapi-0.1.0/src/getanyapi/platforms/booking.py +128 -0
- getanyapi-0.1.0/src/getanyapi/platforms/coinmarketcap.py +113 -0
- getanyapi-0.1.0/src/getanyapi/platforms/congress.py +106 -0
- getanyapi-0.1.0/src/getanyapi/platforms/dexscreener.py +104 -0
- getanyapi-0.1.0/src/getanyapi/platforms/ebay.py +215 -0
- getanyapi-0.1.0/src/getanyapi/platforms/email.py +166 -0
- getanyapi-0.1.0/src/getanyapi/platforms/facebook.py +2324 -0
- getanyapi-0.1.0/src/getanyapi/platforms/fiverr.py +122 -0
- getanyapi-0.1.0/src/getanyapi/platforms/github.py +954 -0
- getanyapi-0.1.0/src/getanyapi/platforms/glassdoor.py +93 -0
- getanyapi-0.1.0/src/getanyapi/platforms/google.py +232 -0
- getanyapi-0.1.0/src/getanyapi/platforms/google_ads.py +380 -0
- getanyapi-0.1.0/src/getanyapi/platforms/google_finance.py +170 -0
- getanyapi-0.1.0/src/getanyapi/platforms/google_shopping.py +103 -0
- getanyapi-0.1.0/src/getanyapi/platforms/hackernews.py +276 -0
- getanyapi-0.1.0/src/getanyapi/platforms/indeed.py +114 -0
- getanyapi-0.1.0/src/getanyapi/platforms/instagram.py +1868 -0
- getanyapi-0.1.0/src/getanyapi/platforms/linkedin.py +1064 -0
- getanyapi-0.1.0/src/getanyapi/platforms/maps.py +412 -0
- getanyapi-0.1.0/src/getanyapi/platforms/pandaexpress.py +262 -0
- getanyapi-0.1.0/src/getanyapi/platforms/person.py +96 -0
- getanyapi-0.1.0/src/getanyapi/platforms/pinterest.py +96 -0
- getanyapi-0.1.0/src/getanyapi/platforms/playstore.py +99 -0
- getanyapi-0.1.0/src/getanyapi/platforms/polymarket.py +109 -0
- getanyapi-0.1.0/src/getanyapi/platforms/realtor.py +104 -0
- getanyapi-0.1.0/src/getanyapi/platforms/reddit.py +582 -0
- getanyapi-0.1.0/src/getanyapi/platforms/redfin.py +95 -0
- getanyapi-0.1.0/src/getanyapi/platforms/rednote.py +807 -0
- getanyapi-0.1.0/src/getanyapi/platforms/sec.py +118 -0
- getanyapi-0.1.0/src/getanyapi/platforms/semrush.py +358 -0
- getanyapi-0.1.0/src/getanyapi/platforms/snapchat.py +146 -0
- getanyapi-0.1.0/src/getanyapi/platforms/social.py +105 -0
- getanyapi-0.1.0/src/getanyapi/platforms/spotify.py +588 -0
- getanyapi-0.1.0/src/getanyapi/platforms/substack.py +142 -0
- getanyapi-0.1.0/src/getanyapi/platforms/threads.py +358 -0
- getanyapi-0.1.0/src/getanyapi/platforms/tiktok.py +1827 -0
- getanyapi-0.1.0/src/getanyapi/platforms/tiktok_shop.py +536 -0
- getanyapi-0.1.0/src/getanyapi/platforms/tripadvisor.py +180 -0
- getanyapi-0.1.0/src/getanyapi/platforms/trustpilot.py +114 -0
- getanyapi-0.1.0/src/getanyapi/platforms/truthsocial.py +226 -0
- getanyapi-0.1.0/src/getanyapi/platforms/twitter.py +798 -0
- getanyapi-0.1.0/src/getanyapi/platforms/upwork.py +119 -0
- getanyapi-0.1.0/src/getanyapi/platforms/walmart.py +93 -0
- getanyapi-0.1.0/src/getanyapi/platforms/web.py +264 -0
- getanyapi-0.1.0/src/getanyapi/platforms/whatsapp.py +91 -0
- getanyapi-0.1.0/src/getanyapi/platforms/yahoo_finance.py +95 -0
- getanyapi-0.1.0/src/getanyapi/platforms/yelp.py +141 -0
- getanyapi-0.1.0/src/getanyapi/platforms/youtube.py +1452 -0
- getanyapi-0.1.0/src/getanyapi/platforms/zillow.py +218 -0
- getanyapi-0.1.0/src/getanyapi/py.typed +0 -0
- getanyapi-0.1.0/src/getanyapi/types.py +170 -0
- getanyapi-0.1.0/tests/conftest.py +103 -0
- getanyapi-0.1.0/tests/test_account.py +147 -0
- getanyapi-0.1.0/tests/test_client.py +125 -0
- getanyapi-0.1.0/tests/test_envelope.py +72 -0
- getanyapi-0.1.0/tests/test_errors.py +60 -0
- getanyapi-0.1.0/tests/test_fixture_roundtrip.py +52 -0
- getanyapi-0.1.0/tests/test_fixture_sweep.py +240 -0
- getanyapi-0.1.0/tests/test_pagination.py +190 -0
- 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/
|
getanyapi-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
)
|