getanyapi 0.1.0__tar.gz → 0.7.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 (96) hide show
  1. {getanyapi-0.1.0 → getanyapi-0.7.0}/PKG-INFO +32 -18
  2. {getanyapi-0.1.0 → getanyapi-0.7.0}/README.md +31 -17
  3. {getanyapi-0.1.0 → getanyapi-0.7.0}/pyproject.toml +1 -1
  4. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/__init__.py +19 -1
  5. getanyapi-0.7.0/src/getanyapi/_account.py +144 -0
  6. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/_async_client.py +18 -16
  7. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/_client.py +16 -10
  8. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/__init__.py +5 -0
  9. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/ahrefs.py +33 -32
  10. getanyapi-0.7.0/src/getanyapi/platforms/airbnb.py +198 -0
  11. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/alibaba.py +49 -10
  12. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/amazon.py +198 -68
  13. getanyapi-0.7.0/src/getanyapi/platforms/apollo.py +1095 -0
  14. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/appstore.py +34 -8
  15. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/bluesky.py +47 -24
  16. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/booking.py +25 -15
  17. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/coinmarketcap.py +35 -22
  18. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/congress.py +58 -10
  19. getanyapi-0.7.0/src/getanyapi/platforms/dexscreener.py +187 -0
  20. getanyapi-0.7.0/src/getanyapi/platforms/douyin.py +549 -0
  21. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/ebay.py +70 -30
  22. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/email.py +18 -16
  23. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/facebook.py +665 -237
  24. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/fiverr.py +14 -10
  25. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/github.py +211 -78
  26. getanyapi-0.7.0/src/getanyapi/platforms/glassdoor.py +155 -0
  27. getanyapi-0.7.0/src/getanyapi/platforms/google.py +696 -0
  28. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/google_ads.py +119 -30
  29. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/google_finance.py +15 -11
  30. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/google_shopping.py +62 -8
  31. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/hackernews.py +61 -29
  32. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/indeed.py +20 -10
  33. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/instagram.py +418 -147
  34. getanyapi-0.7.0/src/getanyapi/platforms/linkedin.py +2818 -0
  35. getanyapi-0.7.0/src/getanyapi/platforms/maps.py +578 -0
  36. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/pandaexpress.py +45 -23
  37. getanyapi-0.7.0/src/getanyapi/platforms/person.py +180 -0
  38. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/pinterest.py +6 -4
  39. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/playstore.py +49 -11
  40. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/polymarket.py +52 -8
  41. getanyapi-0.7.0/src/getanyapi/platforms/realtor.py +214 -0
  42. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/reddit.py +118 -55
  43. getanyapi-0.7.0/src/getanyapi/platforms/redfin.py +156 -0
  44. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/rednote.py +197 -83
  45. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/sec.py +34 -16
  46. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/semrush.py +20 -16
  47. getanyapi-0.7.0/src/getanyapi/platforms/seo.py +1293 -0
  48. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/snapchat.py +31 -40
  49. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/social.py +7 -7
  50. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/spotify.py +170 -64
  51. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/substack.py +45 -21
  52. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/threads.py +133 -54
  53. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/tiktok.py +415 -98
  54. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/tiktok_shop.py +98 -34
  55. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/tripadvisor.py +97 -19
  56. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/trustpilot.py +18 -9
  57. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/truthsocial.py +69 -33
  58. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/twitter.py +380 -92
  59. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/upwork.py +49 -21
  60. getanyapi-0.7.0/src/getanyapi/platforms/walmart.py +146 -0
  61. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/web.py +72 -32
  62. getanyapi-0.7.0/src/getanyapi/platforms/weibo.py +699 -0
  63. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/whatsapp.py +20 -10
  64. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/yahoo_finance.py +48 -12
  65. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/yelp.py +15 -9
  66. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/platforms/youtube.py +253 -80
  67. getanyapi-0.7.0/src/getanyapi/platforms/zhihu.py +555 -0
  68. getanyapi-0.7.0/src/getanyapi/platforms/zillow.py +352 -0
  69. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/types.py +125 -15
  70. getanyapi-0.7.0/tests/test_account.py +510 -0
  71. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_fixture_sweep.py +14 -12
  72. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_pagination.py +3 -3
  73. getanyapi-0.1.0/src/getanyapi/_account.py +0 -132
  74. getanyapi-0.1.0/src/getanyapi/platforms/airbnb.py +0 -120
  75. getanyapi-0.1.0/src/getanyapi/platforms/dexscreener.py +0 -104
  76. getanyapi-0.1.0/src/getanyapi/platforms/glassdoor.py +0 -93
  77. getanyapi-0.1.0/src/getanyapi/platforms/google.py +0 -232
  78. getanyapi-0.1.0/src/getanyapi/platforms/linkedin.py +0 -1064
  79. getanyapi-0.1.0/src/getanyapi/platforms/maps.py +0 -412
  80. getanyapi-0.1.0/src/getanyapi/platforms/person.py +0 -96
  81. getanyapi-0.1.0/src/getanyapi/platforms/realtor.py +0 -104
  82. getanyapi-0.1.0/src/getanyapi/platforms/redfin.py +0 -95
  83. getanyapi-0.1.0/src/getanyapi/platforms/walmart.py +0 -93
  84. getanyapi-0.1.0/src/getanyapi/platforms/zillow.py +0 -218
  85. getanyapi-0.1.0/tests/test_account.py +0 -147
  86. {getanyapi-0.1.0 → getanyapi-0.7.0}/.gitignore +0 -0
  87. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/_errors.py +0 -0
  88. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/_pagination.py +0 -0
  89. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/_transport.py +0 -0
  90. {getanyapi-0.1.0 → getanyapi-0.7.0}/src/getanyapi/py.typed +0 -0
  91. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/conftest.py +0 -0
  92. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_client.py +0 -0
  93. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_envelope.py +0 -0
  94. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_errors.py +0 -0
  95. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_fixture_roundtrip.py +0 -0
  96. {getanyapi-0.1.0 → getanyapi-0.7.0}/tests/test_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getanyapi
3
- Version: 0.1.0
3
+ Version: 0.7.0
4
4
  Summary: Official typed Python SDK for AnyAPI: any API, one wallet, USD, no subscriptions.
5
5
  Project-URL: Homepage, https://getanyapi.com
6
6
  Project-URL: Documentation, https://getanyapi.com/docs
@@ -44,8 +44,9 @@ from getanyapi import AnyAPI
44
44
 
45
45
  client = AnyAPI() # reads ANYAPI_API_KEY from the environment
46
46
  res = client.reddit.search(query="mechanical keyboard")
47
- for post in res.output.posts:
48
- print(post.title, post.score)
47
+ if res.output.found:
48
+ for post in res.output.data.posts:
49
+ print(post.title, post.score)
49
50
  print("charged", res.cost_usd, "USD")
50
51
  ```
51
52
 
@@ -82,9 +83,22 @@ except ResultNotFoundError:
82
83
  ```
83
84
 
84
85
  `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.
86
+ HTTP 404 and an empty result; catch `ResultNotFoundError` to handle only empty results. If a
87
+ future committed schema uses a bare output, generated typing returns its data object directly
88
+ rather than relying on a hard-coded SKU list.
89
+
90
+ ## Discovery
91
+
92
+ ```python
93
+ apis = client.catalog(category="search")
94
+ matches = client.search(query="web search", platform="google", limit=10)
95
+ api = client.describe(matches.results[0].slug)
96
+ print(api.pricing.from_offer, api.pricing.failover_max_usd, api.input_schema)
97
+ ```
98
+
99
+ Sync and async clients expose the same category-only `catalog`, dedicated ranked `search`, and
100
+ schema-bearing `describe` methods. Prices are nested USD flat/linear offers, lanes are
101
+ anonymous, and provider is always `"AnyAPI"`.
88
102
 
89
103
  ## Pagination
90
104
 
@@ -115,17 +129,17 @@ res = client.google.search(
115
129
 
116
130
  ## Errors and retries
117
131
 
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) |
132
+ | Class | HTTP | Meaning |
133
+ | -------------------------- | ---- | ------------------------------------------ |
134
+ | `BadRequestError` | 400 | Input failed validation |
135
+ | `AuthenticationError` | 401 | Missing or invalid API key |
136
+ | `InsufficientBalanceError` | 402 | Wallet balance or per-key cap exceeded |
137
+ | `NotFoundError` | 404 | Slug or resource does not exist |
138
+ | `ResultNotFoundError` | - | `unwrap` on an empty found-data result |
139
+ | `RateLimitedError` | 429 | Too many requests (retried automatically) |
140
+ | `UpstreamError` | 502 | An upstream backend failed |
141
+ | `ConnectionError` | 0 | Network or transport failure (retried) |
142
+ | `TimeoutError` | 0 | Request exceeded its timeout (not retried) |
129
143
 
130
144
  All extend `AnyAPIError` (with `.status` and `.request_id`). Retries cover only 429 and network
131
145
  failures, with jittered exponential backoff honoring `Retry-After`. Default `max_retries` is 2
@@ -143,7 +157,7 @@ result = agent_signup(label="my-agent")
143
157
  client = AnyAPI(api_key=result.secret)
144
158
  ```
145
159
 
146
- The key ships with a small starter credit and a per-key spend cap; a human funds it by claiming
160
+ The key ships with a small starter balance and a per-key spend cap; a human funds it by claiming
147
161
  it at `result.claim_url`.
148
162
 
149
163
  ## Docs
@@ -15,8 +15,9 @@ from getanyapi import AnyAPI
15
15
 
16
16
  client = AnyAPI() # reads ANYAPI_API_KEY from the environment
17
17
  res = client.reddit.search(query="mechanical keyboard")
18
- for post in res.output.posts:
19
- print(post.title, post.score)
18
+ if res.output.found:
19
+ for post in res.output.data.posts:
20
+ print(post.title, post.score)
20
21
  print("charged", res.cost_usd, "USD")
21
22
  ```
22
23
 
@@ -53,9 +54,22 @@ except ResultNotFoundError:
53
54
  ```
54
55
 
55
56
  `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.
57
+ HTTP 404 and an empty result; catch `ResultNotFoundError` to handle only empty results. If a
58
+ future committed schema uses a bare output, generated typing returns its data object directly
59
+ rather than relying on a hard-coded SKU list.
60
+
61
+ ## Discovery
62
+
63
+ ```python
64
+ apis = client.catalog(category="search")
65
+ matches = client.search(query="web search", platform="google", limit=10)
66
+ api = client.describe(matches.results[0].slug)
67
+ print(api.pricing.from_offer, api.pricing.failover_max_usd, api.input_schema)
68
+ ```
69
+
70
+ Sync and async clients expose the same category-only `catalog`, dedicated ranked `search`, and
71
+ schema-bearing `describe` methods. Prices are nested USD flat/linear offers, lanes are
72
+ anonymous, and provider is always `"AnyAPI"`.
59
73
 
60
74
  ## Pagination
61
75
 
@@ -86,17 +100,17 @@ res = client.google.search(
86
100
 
87
101
  ## Errors and retries
88
102
 
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) |
103
+ | Class | HTTP | Meaning |
104
+ | -------------------------- | ---- | ------------------------------------------ |
105
+ | `BadRequestError` | 400 | Input failed validation |
106
+ | `AuthenticationError` | 401 | Missing or invalid API key |
107
+ | `InsufficientBalanceError` | 402 | Wallet balance or per-key cap exceeded |
108
+ | `NotFoundError` | 404 | Slug or resource does not exist |
109
+ | `ResultNotFoundError` | - | `unwrap` on an empty found-data result |
110
+ | `RateLimitedError` | 429 | Too many requests (retried automatically) |
111
+ | `UpstreamError` | 502 | An upstream backend failed |
112
+ | `ConnectionError` | 0 | Network or transport failure (retried) |
113
+ | `TimeoutError` | 0 | Request exceeded its timeout (not retried) |
100
114
 
101
115
  All extend `AnyAPIError` (with `.status` and `.request_id`). Retries cover only 429 and network
102
116
  failures, with jittered exponential backoff honoring `Retry-After`. Default `max_retries` is 2
@@ -114,7 +128,7 @@ result = agent_signup(label="my-agent")
114
128
  client = AnyAPI(api_key=result.secret)
115
129
  ```
116
130
 
117
- The key ships with a small starter credit and a per-key spend cap; a human funds it by claiming
131
+ The key ships with a small starter balance and a per-key spend cap; a human funds it by claiming
118
132
  it at `result.claim_url`.
119
133
 
120
134
  ## Docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getanyapi"
7
- version = "0.1.0"
7
+ version = "0.7.0"
8
8
  description = "Official typed Python SDK for AnyAPI: any API, one wallet, USD, no subscriptions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -32,15 +32,24 @@ from .types import (
32
32
  Balance,
33
33
  BareRunResult,
34
34
  CatalogEntry,
35
+ CatalogSearchResult,
36
+ CatalogSearchResults,
37
+ DiscoveryLane,
38
+ DiscoveryPricing,
39
+ FlatPricingOffer,
40
+ HighlightField,
41
+ LaneHealth,
42
+ LinearPricingOffer,
35
43
  Output,
36
44
  OutputFound,
37
45
  OutputNotFound,
46
+ PricingOffer,
38
47
  RequestOptions,
39
48
  RunResult,
40
49
  unwrap,
41
50
  )
42
51
 
43
- __version__ = "0.0.0"
52
+ __version__ = "0.7.0"
44
53
 
45
54
  __all__ = [
46
55
  # clients + top-level functions
@@ -57,6 +66,15 @@ __all__ = [
57
66
  "Balance",
58
67
  "AccountProfile",
59
68
  "CatalogEntry",
69
+ "FlatPricingOffer",
70
+ "LinearPricingOffer",
71
+ "PricingOffer",
72
+ "DiscoveryPricing",
73
+ "LaneHealth",
74
+ "DiscoveryLane",
75
+ "HighlightField",
76
+ "CatalogSearchResult",
77
+ "CatalogSearchResults",
60
78
  "RequestOptions",
61
79
  "AgentSignupResult",
62
80
  # pagination
@@ -0,0 +1,144 @@
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. Discovery bodies are validated against the customer-safe
6
+ nested USD contract before any public model is returned.
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 (
16
+ AccountProfile,
17
+ AgentSignupResult,
18
+ Balance,
19
+ CatalogEntry,
20
+ CatalogSearchResults,
21
+ )
22
+
23
+ __all__ = [
24
+ "balance_path",
25
+ "me_path",
26
+ "catalog_request",
27
+ "search_request",
28
+ "describe_path",
29
+ "signup_request",
30
+ "parse_balance",
31
+ "parse_me",
32
+ "parse_catalog",
33
+ "parse_describe",
34
+ "parse_search",
35
+ "parse_signup",
36
+ "map_error",
37
+ ]
38
+
39
+ balance_path = "/v1/balance"
40
+ me_path = "/v1/me"
41
+
42
+
43
+ def catalog_request(category: str | None) -> tuple[str, dict[str, str]]:
44
+ """Path and query params for ``catalog()`` (GET /v1/apis)."""
45
+ params: dict[str, str] = {}
46
+ if category is not None:
47
+ params["category"] = category
48
+ return "/v1/apis", params
49
+
50
+
51
+ def search_request(
52
+ query: str,
53
+ category: str | None,
54
+ platform: str | None,
55
+ limit: int | None,
56
+ ) -> tuple[str, dict[str, str]]:
57
+ """Path and query params for dedicated ranked discovery search."""
58
+ params = {"q": query}
59
+ if category is not None:
60
+ params["category"] = category
61
+ if platform is not None:
62
+ params["platform"] = platform
63
+ if limit is not None:
64
+ params["limit"] = str(limit)
65
+ return "/catalog/search", params
66
+
67
+
68
+ def describe_path(slug: str) -> str:
69
+ return f"/v1/apis/{slug}"
70
+
71
+
72
+ def signup_request(sponsor_email: str | None, label: str | None) -> dict[str, Any]:
73
+ """JSON body for ``agent_signup()`` (POST /agent/signup, no auth)."""
74
+ body: dict[str, Any] = {}
75
+ if sponsor_email is not None:
76
+ body["sponsorEmail"] = sponsor_email
77
+ if label is not None:
78
+ body["label"] = label
79
+ return body
80
+
81
+
82
+ def parse_balance(body: Any) -> Balance:
83
+ return Balance.model_validate(body)
84
+
85
+
86
+ def parse_me(body: Any) -> AccountProfile:
87
+ """Map /v1/me, dropping clerkUserId and signupGrantApplied (SPEC 2.7)."""
88
+ return AccountProfile.model_validate(body)
89
+
90
+
91
+ def _reject_internal_keys(value: object, path: str) -> None:
92
+ if isinstance(value, list):
93
+ for index, item in enumerate(cast("list[object]", value)):
94
+ _reject_internal_keys(item, f"{path}[{index}]")
95
+ return
96
+ if not isinstance(value, dict):
97
+ return
98
+ for key, item in cast("dict[object, object]", value).items():
99
+ if isinstance(key, str) and "credit" in key.lower():
100
+ raise ValueError(f"malformed discovery response: {path}.{key}")
101
+ _reject_internal_keys(item, f"{path}.{key}")
102
+
103
+
104
+ def _require_exact_keys(value: dict[str, object], allowed: set[str], path: str) -> None:
105
+ unexpected = set(value) - allowed
106
+ if unexpected:
107
+ key = sorted(unexpected)[0]
108
+ raise ValueError(f"malformed discovery response: {path}.{key}")
109
+
110
+
111
+ def parse_catalog(body: object) -> list[CatalogEntry]:
112
+ """Map {apis:[...]} into CatalogEntry[] (SPEC 2.7)."""
113
+ _reject_internal_keys(body, "catalog")
114
+ envelope = as_dict(body)
115
+ _require_exact_keys(envelope, {"apis"}, "catalog")
116
+ raw = envelope.get("apis")
117
+ if not isinstance(raw, list):
118
+ raise ValueError("malformed discovery response: catalog.apis")
119
+ apis: list[object] = list(cast("list[object]", raw))
120
+ return [CatalogEntry.model_validate(item, strict=True) for item in apis]
121
+
122
+
123
+ def parse_describe(body: object) -> CatalogEntry:
124
+ """Map a single /v1/apis/{slug} entry into one CatalogEntry."""
125
+ _reject_internal_keys(body, "api")
126
+ entry = CatalogEntry.model_validate(body, strict=True)
127
+ if entry.input_schema is None or entry.output_schema is None:
128
+ raise ValueError("malformed discovery response: detail schemas are required")
129
+ return entry
130
+
131
+
132
+ def parse_search(body: object) -> CatalogSearchResults:
133
+ """Validate the dedicated search result envelope."""
134
+ _reject_internal_keys(body, "search")
135
+ return CatalogSearchResults.model_validate(body, strict=True)
136
+
137
+
138
+ def parse_signup(body: Any) -> AgentSignupResult:
139
+ return AgentSignupResult.model_validate(body)
140
+
141
+
142
+ def map_error(status: int, body: object, request_id: str | None) -> AnyAPIError:
143
+ """Map a non-2xx account/catalog response to the frozen error hierarchy."""
144
+ return error_for_status(status, error_message(body, status), request_id=request_id)
@@ -30,6 +30,7 @@ from .types import (
30
30
  AccountProfile,
31
31
  Balance,
32
32
  CatalogEntry,
33
+ CatalogSearchResults,
33
34
  RequestOptions,
34
35
  RunResult,
35
36
  )
@@ -42,9 +43,7 @@ _DEFAULT_BASE_URL = "https://api.getanyapi.com"
42
43
  def _resolve_api_key(api_key: str | None) -> str:
43
44
  key = api_key if api_key is not None else os.environ.get("ANYAPI_API_KEY")
44
45
  if not key:
45
- raise AnyAPIError(
46
- "no API key: pass api_key= or set ANYAPI_API_KEY", status=0
47
- )
46
+ raise AnyAPIError("no API key: pass api_key= or set ANYAPI_API_KEY", status=0)
48
47
  return key
49
48
 
50
49
 
@@ -129,9 +128,7 @@ class AsyncAnyAPI:
129
128
  continue
130
129
  raise
131
130
  except httpx.TimeoutException as exc:
132
- raise TimeoutError(
133
- str(exc) or "request timed out", status=0
134
- ) from exc
131
+ raise TimeoutError(str(exc) or "request timed out", status=0) from exc
135
132
  except httpx.HTTPError as exc:
136
133
  if retry.can_retry:
137
134
  await asyncio.sleep(retry.next_delay(None))
@@ -162,17 +159,13 @@ class AsyncAnyAPI:
162
159
 
163
160
  # -- account + catalog ------------------------------------------------
164
161
 
165
- async def _get(
166
- self, path: str, params: dict[str, str] | None = None
167
- ) -> object:
162
+ async def _get(self, path: str, params: dict[str, str] | None = None) -> object:
168
163
  url = f"{self._base_url}{path}"
169
164
  headers = {
170
165
  "Authorization": f"Bearer {self._api_key}",
171
166
  "Accept": "application/json",
172
167
  }
173
- response = await self._http.get(
174
- url, params=params or {}, headers=headers
175
- )
168
+ response = await self._http.get(url, params=params or {}, headers=headers)
176
169
  return self._json_or_raise(response)
177
170
 
178
171
  def _json_or_raise(self, response: httpx.Response) -> object:
@@ -192,12 +185,21 @@ class AsyncAnyAPI:
192
185
  async def me(self) -> AccountProfile:
193
186
  return _account.parse_me(await self._get(_account.me_path))
194
187
 
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)
188
+ async def catalog(self, *, category: str | None = None) -> list[CatalogEntry]:
189
+ path, params = _account.catalog_request(category)
199
190
  return _account.parse_catalog(await self._get(path, params))
200
191
 
192
+ async def search(
193
+ self,
194
+ *,
195
+ query: str,
196
+ category: str | None = None,
197
+ platform: str | None = None,
198
+ limit: int | None = None,
199
+ ) -> CatalogSearchResults:
200
+ path, params = _account.search_request(query, category, platform, limit)
201
+ return _account.parse_search(await self._get(path, params))
202
+
201
203
  async def describe(self, slug: str) -> CatalogEntry:
202
204
  body = await self._get(_account.describe_path(slug))
203
205
  return _account.parse_describe(body)
@@ -49,6 +49,7 @@ from .types import (
49
49
  AgentSignupResult,
50
50
  Balance,
51
51
  CatalogEntry,
52
+ CatalogSearchResults,
52
53
  RequestOptions,
53
54
  RunResult,
54
55
  )
@@ -71,9 +72,7 @@ def lookup_namespace(name: str) -> tuple[str, str, str] | None:
71
72
  def _resolve_api_key(api_key: str | None) -> str:
72
73
  key = api_key if api_key is not None else os.environ.get("ANYAPI_API_KEY")
73
74
  if not key:
74
- raise AnyAPIError(
75
- "no API key: pass api_key= or set ANYAPI_API_KEY", status=0
76
- )
75
+ raise AnyAPIError("no API key: pass api_key= or set ANYAPI_API_KEY", status=0)
77
76
  return key
78
77
 
79
78
 
@@ -164,9 +163,7 @@ class AnyAPI:
164
163
  continue
165
164
  raise
166
165
  except httpx.TimeoutException as exc:
167
- raise TimeoutError(
168
- str(exc) or "request timed out", status=0
169
- ) from exc
166
+ raise TimeoutError(str(exc) or "request timed out", status=0) from exc
170
167
  except httpx.HTTPError as exc:
171
168
  if retry.can_retry:
172
169
  _transport.sleep(retry.next_delay(None))
@@ -223,12 +220,21 @@ class AnyAPI:
223
220
  def me(self) -> AccountProfile:
224
221
  return _account.parse_me(self._get(_account.me_path))
225
222
 
226
- def catalog(
227
- self, *, query: str | None = None, category: str | None = None
228
- ) -> list[CatalogEntry]:
229
- path, params = _account.catalog_request(query, category)
223
+ def catalog(self, *, category: str | None = None) -> list[CatalogEntry]:
224
+ path, params = _account.catalog_request(category)
230
225
  return _account.parse_catalog(self._get(path, params))
231
226
 
227
+ def search(
228
+ self,
229
+ *,
230
+ query: str,
231
+ category: str | None = None,
232
+ platform: str | None = None,
233
+ limit: int | None = None,
234
+ ) -> CatalogSearchResults:
235
+ path, params = _account.search_request(query, category, platform, limit)
236
+ return _account.parse_search(self._get(path, params))
237
+
232
238
  def describe(self, slug: str) -> CatalogEntry:
233
239
  return _account.parse_describe(self._get(_account.describe_path(slug)))
234
240
 
@@ -13,6 +13,7 @@ REGISTRY: dict[str, tuple[str, str, str]] = {
13
13
  "airbnb": ("airbnb", "AirbnbNamespace", "AsyncAirbnbNamespace"),
14
14
  "alibaba": ("alibaba", "AlibabaNamespace", "AsyncAlibabaNamespace"),
15
15
  "amazon": ("amazon", "AmazonNamespace", "AsyncAmazonNamespace"),
16
+ "apollo": ("apollo", "ApolloNamespace", "AsyncApolloNamespace"),
16
17
  "appstore": ("appstore", "AppstoreNamespace", "AsyncAppstoreNamespace"),
17
18
  "bluesky": ("bluesky", "BlueskyNamespace", "AsyncBlueskyNamespace"),
18
19
  "booking": ("booking", "BookingNamespace", "AsyncBookingNamespace"),
@@ -23,6 +24,7 @@ REGISTRY: dict[str, tuple[str, str, str]] = {
23
24
  ),
24
25
  "congress": ("congress", "CongressNamespace", "AsyncCongressNamespace"),
25
26
  "dexscreener": ("dexscreener", "DexscreenerNamespace", "AsyncDexscreenerNamespace"),
27
+ "douyin": ("douyin", "DouyinNamespace", "AsyncDouyinNamespace"),
26
28
  "ebay": ("ebay", "EbayNamespace", "AsyncEbayNamespace"),
27
29
  "email": ("email", "EmailNamespace", "AsyncEmailNamespace"),
28
30
  "facebook": ("facebook", "FacebookNamespace", "AsyncFacebookNamespace"),
@@ -61,6 +63,7 @@ REGISTRY: dict[str, tuple[str, str, str]] = {
61
63
  "rednote": ("rednote", "RednoteNamespace", "AsyncRednoteNamespace"),
62
64
  "sec": ("sec", "SecNamespace", "AsyncSecNamespace"),
63
65
  "semrush": ("semrush", "SemrushNamespace", "AsyncSemrushNamespace"),
66
+ "seo": ("seo", "SeoNamespace", "AsyncSeoNamespace"),
64
67
  "snapchat": ("snapchat", "SnapchatNamespace", "AsyncSnapchatNamespace"),
65
68
  "social": ("social", "SocialNamespace", "AsyncSocialNamespace"),
66
69
  "spotify": ("spotify", "SpotifyNamespace", "AsyncSpotifyNamespace"),
@@ -75,6 +78,7 @@ REGISTRY: dict[str, tuple[str, str, str]] = {
75
78
  "upwork": ("upwork", "UpworkNamespace", "AsyncUpworkNamespace"),
76
79
  "walmart": ("walmart", "WalmartNamespace", "AsyncWalmartNamespace"),
77
80
  "web": ("web", "WebNamespace", "AsyncWebNamespace"),
81
+ "weibo": ("weibo", "WeiboNamespace", "AsyncWeiboNamespace"),
78
82
  "whatsapp": ("whatsapp", "WhatsappNamespace", "AsyncWhatsappNamespace"),
79
83
  "yahoo_finance": (
80
84
  "yahoo_finance",
@@ -83,6 +87,7 @@ REGISTRY: dict[str, tuple[str, str, str]] = {
83
87
  ),
84
88
  "yelp": ("yelp", "YelpNamespace", "AsyncYelpNamespace"),
85
89
  "youtube": ("youtube", "YoutubeNamespace", "AsyncYoutubeNamespace"),
90
+ "zhihu": ("zhihu", "ZhihuNamespace", "AsyncZhihuNamespace"),
86
91
  "zillow": ("zillow", "ZillowNamespace", "AsyncZillowNamespace"),
87
92
  }
88
93