dataxapi 0.2.0__tar.gz → 0.3.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.
- dataxapi-0.3.0/CHANGELOG.md +58 -0
- dataxapi-0.3.0/MANIFEST.in +1 -0
- dataxapi-0.3.0/PKG-INFO +118 -0
- dataxapi-0.3.0/README.md +101 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/pyproject.toml +4 -2
- dataxapi-0.3.0/src/dataxapi/__init__.py +4 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/src/dataxapi/client.py +78 -25
- dataxapi-0.3.0/src/dataxapi/generated.py +15060 -0
- dataxapi-0.3.0/src/dataxapi.egg-info/PKG-INFO +118 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/src/dataxapi.egg-info/SOURCES.txt +2 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/tests/test_client.py +83 -8
- dataxapi-0.2.0/PKG-INFO +0 -34
- dataxapi-0.2.0/README.md +0 -19
- dataxapi-0.2.0/src/dataxapi/__init__.py +0 -4
- dataxapi-0.2.0/src/dataxapi/generated.py +0 -7487
- dataxapi-0.2.0/src/dataxapi.egg-info/PKG-INFO +0 -34
- {dataxapi-0.2.0 → dataxapi-0.3.0}/setup.cfg +0 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/src/dataxapi/py.typed +0 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/src/dataxapi.egg-info/dependency_links.txt +0 -0
- {dataxapi-0.2.0 → dataxapi-0.3.0}/src/dataxapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `dataxapi` Python package are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses [Semantic Versioning](https://semver.org/) (while below 1.0.0, a minor version may change defaults or behavior).
|
|
4
|
+
|
|
5
|
+
## [0.3.0]
|
|
6
|
+
|
|
7
|
+
This release adopts the DataXAPI brand names, changes the default timeout and the automatic retry rules, and adds types for test keys and partial results. No API operation was added, removed or renamed since 0.2.1 (151 operations). See [Upgrading from 0.2.x](#upgrading-from-02x).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `DataXClient`, `DataXError`, `DataXConnectionError` and `DataXPaginationError` are now the primary exports of `dataxapi`.
|
|
12
|
+
- Constants `DEFAULT_TIMEOUT` (60.0), `IN_PROGRESS_WAIT_LIMIT` (90.0) and `NON_RETRYABLE_CODES` in `dataxapi.client`.
|
|
13
|
+
- Test keys: an `sk_test_...` key works with the same client. Calls are validated like live calls, return the API's documented example and are never charged; responses carry `meta["mode"] == "test"`. See [Test keys](https://dataxapi.com/en/docs/authentication/#test-keys).
|
|
14
|
+
- Response `TypedDict`s: optional `meta["mode"]` (`"test"`) on every operation, and `meta["delivered_count"]` / `meta["filtered_count"]` on operations that can return `partial` results (items returned vs. items removed for rights reasons).
|
|
15
|
+
- `Usage`: `mode` (`"live" | "test"`); `status` is now `Literal["reserved", "complete", "empty", "partial", "failed"]` instead of `str`.
|
|
16
|
+
- `Problem`: `limit_scope` (`"workspace" | "api_key" | "listing"`) on `rate_limited`; `request_id` may be `None`.
|
|
17
|
+
- The API can now answer with these problem codes, raised as `DataXError`: `no_results` (404; since 2026-10-08 live calls no longer return it, see Changed), `not_found` (404: the data source states that the requested object does not exist), `route_not_found` (404), `method_not_allowed` (405), `account_suspended` (403) and `invalid_test_scenario` (400, test keys only). See [Errors](https://dataxapi.com/en/docs/errors/).
|
|
18
|
+
- `client.call()` with an unknown operation id raises a `ValueError` that names the id, before any request is sent (0.2.x raised a bare `KeyError`).
|
|
19
|
+
- Package metadata: the "Documentation" link now points to the docs center; the API catalog has its own link.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Empty results are charged on every API (2026-10-08).** When the data source answers normally but has no data, the API returns 200 with `meta.result_status: "empty"` and charges the unit price, because the data source bills that lookup; data-source failures stay free. The 142 APIs that used to answer a free 404 `no_results` now do this, so `no_results` is no longer returned by live calls. The code stays in the types and in `NON_RETRYABLE_CODES` so existing error handling keeps working.
|
|
24
|
+
- **Default `timeout` per network attempt is 60 seconds (was 10 seconds)**, above the slowest data source's own timeout, so slow sources no longer end in `DataXConnectionError` by default.
|
|
25
|
+
- **`409 request_in_progress` is polled with the same idempotency key** for up to about 90 seconds, waiting `retry_after_ms` between polls, and no longer uses up `max_retries`. The original call may still be charged; polling returns its result instead of starting a new call. `max_retries=0` turns this off.
|
|
26
|
+
- **Deterministic error codes are never retried automatically**: `upstream_failed`, `upstream_schema_changed`, `settlement_failed`, `previous_execution_failed`, `no_results`, `not_found`, `route_not_found`, `method_not_allowed`, `listing_paused` and `account_suspended`. In 0.2.x every 5xx except `listing_paused` was retried with the same key, which could only repeat the same answer.
|
|
27
|
+
- Network errors and timeouts are retried with exponential backoff and jitter (about 0.25 s, 0.5 s, 1 s …, at most 8 s and never longer than the timeout) instead of immediately.
|
|
28
|
+
- Default error messages say "DataXAPI" instead of "Exact API".
|
|
29
|
+
|
|
30
|
+
### Deprecated
|
|
31
|
+
|
|
32
|
+
- `ExactApiClient`, `ExactApiError`, `ExactApiConnectionError` and `ExactApiPaginationError` remain as aliases of the new classes (the same class objects, so `except ExactApiError` keeps working) and will be removed in a future major version.
|
|
33
|
+
|
|
34
|
+
### Upgrading from 0.2.x
|
|
35
|
+
|
|
36
|
+
1. `pip install -U "dataxapi>=0.3,<0.4"`. Existing code keeps working: the old names are aliases.
|
|
37
|
+
2. Rename imports when convenient: `ExactApiClient` → `DataXClient`, `ExactApiError` → `DataXError`, `ExactApiConnectionError` → `DataXConnectionError`, `ExactApiPaginationError` → `DataXPaginationError`.
|
|
38
|
+
3. If you relied on the old 10-second timeout to fail fast, pass `timeout=10`. With the defaults, a call that keeps hitting network timeouts can take about three minutes (3 attempts × 60 s) before `DataXConnectionError`.
|
|
39
|
+
4. `upstream_failed`, `upstream_schema_changed`, `settlement_failed` and `previous_execution_failed` are no longer retried for you. They are not charged; retry later yourself with a **new** `idempotency_key` if you still need the data.
|
|
40
|
+
5. A call that meets `request_in_progress` can now wait up to about 90 seconds inside the SDK. Use `max_retries=0` if your code polls on its own.
|
|
41
|
+
6. If you caught `KeyError` for a mistyped operation id, catch `ValueError` instead.
|
|
42
|
+
7. Type checkers: `Usage["status"]` is now a `Literal` union and `Problem["request_id"]` can be `None`.
|
|
43
|
+
|
|
44
|
+
## 0.2.1 and earlier
|
|
45
|
+
|
|
46
|
+
0.1.0 (2026-09-17), 0.2.0 (2026-09-19) and 0.2.1 (2026-09-20) were published before this changelog existed.
|
|
47
|
+
|
|
48
|
+
## 中文摘要
|
|
49
|
+
|
|
50
|
+
0.3.0 主要变化:
|
|
51
|
+
|
|
52
|
+
- 新名称 `DataXClient`、`DataXError`、`DataXConnectionError`、`DataXPaginationError`;旧的 `ExactApi*` 名称继续可用,作为兼容别名保留。
|
|
53
|
+
- 每次网络尝试的默认超时从 10 秒改为 60 秒。
|
|
54
|
+
- 遇到 `request_in_progress` 时,用同一个幂等 Key 继续等待最多约 90 秒,拿到首次调用的结果,不占用 `max_retries`。
|
|
55
|
+
- `upstream_failed`、`no_results`、`not_found` 等重试也不会变的错误不再自动重试;网络错误改为逐步加长、带随机抖动的等待后重试。
|
|
56
|
+
- 支持测试 Key(`sk_test_`):返回示例数据、不扣费,响应里 `meta["mode"]` 为 `"test"`;新增 `delivered_count` / `filtered_count` 等类型和 5 个新错误码。
|
|
57
|
+
- 写错接口名时抛出 `ValueError`(原来是 `KeyError`)。
|
|
58
|
+
- 接口本身没有增删或改名。升级方法见上面的 "Upgrading from 0.2.x"。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include CHANGELOG.md
|
dataxapi-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataxapi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: DataXAPI Python SDK: exact public social data endpoints with one CNY balance
|
|
5
|
+
Author-email: "DataXAPI (Aurevant LLC)" <support@dataxapi.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://dataxapi.com
|
|
8
|
+
Project-URL: Documentation, https://dataxapi.com/en/docs/
|
|
9
|
+
Project-URL: API catalog, https://dataxapi.com/en/apis/
|
|
10
|
+
Project-URL: Changelog, https://dataxapi.com/en/docs/sdks/#upgrade
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# DataXAPI Python SDK
|
|
19
|
+
|
|
20
|
+
Typed client for [DataXAPI](https://dataxapi.com): fixed, per-call-priced public social data APIs (X, Reddit, Xiaohongshu, Douyin, TikTok, Instagram, YouTube, Bilibili, Weibo, Kuaishou, Zhihu) paid from one CNY balance.
|
|
21
|
+
|
|
22
|
+
The input, output and error `TypedDict`s are generated from the current API catalog / [OpenAPI document](https://dataxapi.com/openapi.json). The runtime only handles authentication, timeouts, safe retries with one idempotency key, pagination and Usage lookups. It has no third-party dependencies.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install dataxapi
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Python 3.11 or later.
|
|
31
|
+
|
|
32
|
+
## Authenticate
|
|
33
|
+
|
|
34
|
+
Create an API key in the [Console](https://dataxapi.com/en/console/) and keep it in an environment variable:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
export DATAXAPI_API_KEY="sk_live_..."
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For development and tests, use a test key (`sk_test_...`, created with the Console's "Test key" switch) with the same client: calls are validated like live calls but return each API's documented example with `meta["mode"] == "test"` and are never charged. See [Test keys](https://dataxapi.com/en/docs/authentication/#test-keys).
|
|
41
|
+
|
|
42
|
+
## Call an API
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
import uuid
|
|
47
|
+
|
|
48
|
+
from dataxapi import DataXClient
|
|
49
|
+
|
|
50
|
+
client = DataXClient("https://api.dataxapi.com", os.environ["DATAXAPI_API_KEY"])
|
|
51
|
+
|
|
52
|
+
# Every API is available by its operationId (the OpenAPI operationId).
|
|
53
|
+
result = client.call(
|
|
54
|
+
"x_posts_search_v1",
|
|
55
|
+
{"query": "OpenAI min_faves:100"},
|
|
56
|
+
# Optional. Store the key with your job and reuse it when you retry the same
|
|
57
|
+
# logical call: the retry returns the original result and is not charged again.
|
|
58
|
+
idempotency_key=str(uuid.uuid4()),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
print(len(result["data"]["items"]), result["meta"]["charged_amount"], result["meta"]["balance_after"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When you do not pass `idempotency_key`, the SDK generates one per call and reuses it for its own automatic retries. Unknown operation ids raise a `ValueError` that names the id.
|
|
65
|
+
|
|
66
|
+
## Errors and retries
|
|
67
|
+
|
|
68
|
+
- `DataXError`: the API answered with a non-2xx problem. Attributes: `status`, `code`, `detail`, `request_id`, `charge_status`, `charged_amount`, `retry_after_ms`, `retry_after`, `idempotency_key`.
|
|
69
|
+
- `DataXConnectionError`: the outcome is unknown (network error or timeout). Retry later with `error.idempotency_key` so the call cannot run or charge twice.
|
|
70
|
+
|
|
71
|
+
Built-in behavior (defaults: `timeout=60` seconds per attempt, `max_retries=2`):
|
|
72
|
+
|
|
73
|
+
- `429 rate_limited` and retryable `5xx` errors are retried with the same key after the server's `retry_after_ms` / `Retry-After`.
|
|
74
|
+
- `409 request_in_progress` means the original call with that key is still running (`charge_status` is `unknown` until it settles). The SDK waits `retry_after_ms` and polls with the same key for up to about 90 seconds; this does not use up `max_retries`. Set `max_retries=0` to disable all automatic retries.
|
|
75
|
+
- Network errors are retried with exponential backoff and jitter, with the same key.
|
|
76
|
+
- Codes that repeat deterministically are never retried: `upstream_failed`, `upstream_schema_changed`, `settlement_failed`, `previous_execution_failed`, `no_results`, `not_found`, `route_not_found`, `method_not_allowed`, `listing_paused`, `account_suspended`, and other 4xx errors. (`no_results` is no longer returned since 2026-10-08: an empty result is a charged 200 with `meta.result_status` `"empty"`.)
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from dataxapi import DataXConnectionError, DataXError
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
client.call("x_posts_search_v1", {"query": "OpenAI"})
|
|
83
|
+
except DataXError as error:
|
|
84
|
+
print(error.status, error.code, error.request_id, error.charge_status)
|
|
85
|
+
except DataXConnectionError as error:
|
|
86
|
+
print("retry later with", error.idempotency_key)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`DataXPaginationError` is raised when a page iterator sees a repeated cursor. `ExactApiClient`, `ExactApiError`, `ExactApiConnectionError` and `ExactApiPaginationError` remain as deprecated aliases of these classes.
|
|
90
|
+
|
|
91
|
+
## Upgrading from 0.2.x
|
|
92
|
+
|
|
93
|
+
0.3.0 renames the classes (old names still work), raises the default timeout from 10 to 60 seconds, polls `request_in_progress` with the same key, and stops retrying errors that a retry would only repeat. See `CHANGELOG.md` in the source distribution and the [upgrade notes](https://dataxapi.com/en/docs/sdks/#upgrade).
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- Docs: <https://dataxapi.com/en/docs/> (中文:<https://dataxapi.com/zh/docs/>)
|
|
98
|
+
- SDK guide: <https://dataxapi.com/en/docs/sdks/>
|
|
99
|
+
- Upgrade notes: <https://dataxapi.com/en/docs/sdks/#upgrade>
|
|
100
|
+
- Errors: <https://dataxapi.com/en/docs/errors/>
|
|
101
|
+
- API catalog and prices: <https://dataxapi.com/en/apis/>
|
|
102
|
+
- OpenAPI: <https://dataxapi.com/openapi.json>
|
|
103
|
+
- MCP server for AI agents: <https://dataxapi.com/en/docs/mcp/>
|
|
104
|
+
|
|
105
|
+
## 中文说明
|
|
106
|
+
|
|
107
|
+
`dataxapi` 是 DataXAPI 的 Python SDK:用一个 API Key、一个人民币余额按次调用各平台的公开数据接口。
|
|
108
|
+
|
|
109
|
+
- 安装:`pip install dataxapi`(Python 3.11+,无第三方依赖)。
|
|
110
|
+
- 用法:`DataXClient("https://api.dataxapi.com", api_key)`,再用 `client.call("<operationId>", 参数, idempotency_key=...)` 调用任意接口。
|
|
111
|
+
- 重试:同一个逻辑调用复用同一个 `idempotency_key`,重试不会重复执行或扣费;SDK 自动重试 429 和可重试的 5xx,遇到 `request_in_progress` 会用同一个 Key 等待最多约 90 秒;`upstream_failed`、`no_results`、`not_found` 等确定性错误不自动重试。
|
|
112
|
+
- 默认每次尝试超时 60 秒,`max_retries` 默认 2。
|
|
113
|
+
- 文档:<https://dataxapi.com/zh/docs/>,SDK 说明:<https://dataxapi.com/zh/docs/sdks/>。
|
|
114
|
+
- 从 0.2.x 升级:旧名称仍可用,默认超时由 10 秒改为 60 秒,重试规则有调整,见源码包中的 `CHANGELOG.md` 和 <https://dataxapi.com/zh/docs/sdks/#upgrade>。
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
dataxapi-0.3.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# DataXAPI Python SDK
|
|
2
|
+
|
|
3
|
+
Typed client for [DataXAPI](https://dataxapi.com): fixed, per-call-priced public social data APIs (X, Reddit, Xiaohongshu, Douyin, TikTok, Instagram, YouTube, Bilibili, Weibo, Kuaishou, Zhihu) paid from one CNY balance.
|
|
4
|
+
|
|
5
|
+
The input, output and error `TypedDict`s are generated from the current API catalog / [OpenAPI document](https://dataxapi.com/openapi.json). The runtime only handles authentication, timeouts, safe retries with one idempotency key, pagination and Usage lookups. It has no third-party dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install dataxapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python 3.11 or later.
|
|
14
|
+
|
|
15
|
+
## Authenticate
|
|
16
|
+
|
|
17
|
+
Create an API key in the [Console](https://dataxapi.com/en/console/) and keep it in an environment variable:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
export DATAXAPI_API_KEY="sk_live_..."
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For development and tests, use a test key (`sk_test_...`, created with the Console's "Test key" switch) with the same client: calls are validated like live calls but return each API's documented example with `meta["mode"] == "test"` and are never charged. See [Test keys](https://dataxapi.com/en/docs/authentication/#test-keys).
|
|
24
|
+
|
|
25
|
+
## Call an API
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
import uuid
|
|
30
|
+
|
|
31
|
+
from dataxapi import DataXClient
|
|
32
|
+
|
|
33
|
+
client = DataXClient("https://api.dataxapi.com", os.environ["DATAXAPI_API_KEY"])
|
|
34
|
+
|
|
35
|
+
# Every API is available by its operationId (the OpenAPI operationId).
|
|
36
|
+
result = client.call(
|
|
37
|
+
"x_posts_search_v1",
|
|
38
|
+
{"query": "OpenAI min_faves:100"},
|
|
39
|
+
# Optional. Store the key with your job and reuse it when you retry the same
|
|
40
|
+
# logical call: the retry returns the original result and is not charged again.
|
|
41
|
+
idempotency_key=str(uuid.uuid4()),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
print(len(result["data"]["items"]), result["meta"]["charged_amount"], result["meta"]["balance_after"])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When you do not pass `idempotency_key`, the SDK generates one per call and reuses it for its own automatic retries. Unknown operation ids raise a `ValueError` that names the id.
|
|
48
|
+
|
|
49
|
+
## Errors and retries
|
|
50
|
+
|
|
51
|
+
- `DataXError`: the API answered with a non-2xx problem. Attributes: `status`, `code`, `detail`, `request_id`, `charge_status`, `charged_amount`, `retry_after_ms`, `retry_after`, `idempotency_key`.
|
|
52
|
+
- `DataXConnectionError`: the outcome is unknown (network error or timeout). Retry later with `error.idempotency_key` so the call cannot run or charge twice.
|
|
53
|
+
|
|
54
|
+
Built-in behavior (defaults: `timeout=60` seconds per attempt, `max_retries=2`):
|
|
55
|
+
|
|
56
|
+
- `429 rate_limited` and retryable `5xx` errors are retried with the same key after the server's `retry_after_ms` / `Retry-After`.
|
|
57
|
+
- `409 request_in_progress` means the original call with that key is still running (`charge_status` is `unknown` until it settles). The SDK waits `retry_after_ms` and polls with the same key for up to about 90 seconds; this does not use up `max_retries`. Set `max_retries=0` to disable all automatic retries.
|
|
58
|
+
- Network errors are retried with exponential backoff and jitter, with the same key.
|
|
59
|
+
- Codes that repeat deterministically are never retried: `upstream_failed`, `upstream_schema_changed`, `settlement_failed`, `previous_execution_failed`, `no_results`, `not_found`, `route_not_found`, `method_not_allowed`, `listing_paused`, `account_suspended`, and other 4xx errors. (`no_results` is no longer returned since 2026-10-08: an empty result is a charged 200 with `meta.result_status` `"empty"`.)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from dataxapi import DataXConnectionError, DataXError
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
client.call("x_posts_search_v1", {"query": "OpenAI"})
|
|
66
|
+
except DataXError as error:
|
|
67
|
+
print(error.status, error.code, error.request_id, error.charge_status)
|
|
68
|
+
except DataXConnectionError as error:
|
|
69
|
+
print("retry later with", error.idempotency_key)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`DataXPaginationError` is raised when a page iterator sees a repeated cursor. `ExactApiClient`, `ExactApiError`, `ExactApiConnectionError` and `ExactApiPaginationError` remain as deprecated aliases of these classes.
|
|
73
|
+
|
|
74
|
+
## Upgrading from 0.2.x
|
|
75
|
+
|
|
76
|
+
0.3.0 renames the classes (old names still work), raises the default timeout from 10 to 60 seconds, polls `request_in_progress` with the same key, and stops retrying errors that a retry would only repeat. See `CHANGELOG.md` in the source distribution and the [upgrade notes](https://dataxapi.com/en/docs/sdks/#upgrade).
|
|
77
|
+
|
|
78
|
+
## Links
|
|
79
|
+
|
|
80
|
+
- Docs: <https://dataxapi.com/en/docs/> (中文:<https://dataxapi.com/zh/docs/>)
|
|
81
|
+
- SDK guide: <https://dataxapi.com/en/docs/sdks/>
|
|
82
|
+
- Upgrade notes: <https://dataxapi.com/en/docs/sdks/#upgrade>
|
|
83
|
+
- Errors: <https://dataxapi.com/en/docs/errors/>
|
|
84
|
+
- API catalog and prices: <https://dataxapi.com/en/apis/>
|
|
85
|
+
- OpenAPI: <https://dataxapi.com/openapi.json>
|
|
86
|
+
- MCP server for AI agents: <https://dataxapi.com/en/docs/mcp/>
|
|
87
|
+
|
|
88
|
+
## 中文说明
|
|
89
|
+
|
|
90
|
+
`dataxapi` 是 DataXAPI 的 Python SDK:用一个 API Key、一个人民币余额按次调用各平台的公开数据接口。
|
|
91
|
+
|
|
92
|
+
- 安装:`pip install dataxapi`(Python 3.11+,无第三方依赖)。
|
|
93
|
+
- 用法:`DataXClient("https://api.dataxapi.com", api_key)`,再用 `client.call("<operationId>", 参数, idempotency_key=...)` 调用任意接口。
|
|
94
|
+
- 重试:同一个逻辑调用复用同一个 `idempotency_key`,重试不会重复执行或扣费;SDK 自动重试 429 和可重试的 5xx,遇到 `request_in_progress` 会用同一个 Key 等待最多约 90 秒;`upstream_failed`、`no_results`、`not_found` 等确定性错误不自动重试。
|
|
95
|
+
- 默认每次尝试超时 60 秒,`max_retries` 默认 2。
|
|
96
|
+
- 文档:<https://dataxapi.com/zh/docs/>,SDK 说明:<https://dataxapi.com/zh/docs/sdks/>。
|
|
97
|
+
- 从 0.2.x 升级:旧名称仍可用,默认超时由 10 秒改为 60 秒,重试规则有调整,见源码包中的 `CHANGELOG.md` 和 <https://dataxapi.com/zh/docs/sdks/#upgrade>。
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dataxapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
requires-python = ">=3.11"
|
|
5
5
|
description = "DataXAPI Python SDK: exact public social data endpoints with one CNY balance"
|
|
6
6
|
readme = "README.md"
|
|
@@ -15,7 +15,9 @@ classifiers = [
|
|
|
15
15
|
|
|
16
16
|
[project.urls]
|
|
17
17
|
Homepage = "https://dataxapi.com"
|
|
18
|
-
Documentation = "https://dataxapi.com/en/
|
|
18
|
+
Documentation = "https://dataxapi.com/en/docs/"
|
|
19
|
+
"API catalog" = "https://dataxapi.com/en/apis/"
|
|
20
|
+
Changelog = "https://dataxapi.com/en/docs/sdks/#upgrade"
|
|
19
21
|
|
|
20
22
|
[build-system]
|
|
21
23
|
requires = ["setuptools>=77"]
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Generated by scripts/generate-exact-api-sdks.mjs. Do not edit.
|
|
2
|
+
from .client import DataXClient, DataXConnectionError, DataXError, DataXPaginationError, ExactApiClient, ExactApiConnectionError, ExactApiError, ExactApiPaginationError
|
|
3
|
+
from .generated import Problem, Usage, OperationId, OperationInput, OperationResponse, OPERATIONS, USAGE_OPERATION, EXAMPLES, XPostsSearchV1Input, XPostsSearchV1Response, XPostsDetailV1Input, XPostsDetailV1Response, XUsersProfileV1Input, XUsersProfileV1Response, XUsersPostsV1Input, XUsersPostsV1Response, RedditPostsDetailV1Input, RedditPostsDetailV1Response, RedditPostsSearchV1Input, RedditPostsSearchV1Response, RedditPostsCommentsV1Input, RedditPostsCommentsV1Response, XiaohongshuPgyNotesDetailV1Input, XiaohongshuPgyNotesDetailV1Response, XiaohongshuPgyBloggersDetailV1Input, XiaohongshuPgyBloggersDetailV1Response, XiaohongshuPgyBloggersNotesV1Input, XiaohongshuPgyBloggersNotesV1Response, XiaohongshuPgyBloggersCoredataV1Input, XiaohongshuPgyBloggersCoredataV1Response, XiaohongshuPgyBloggersDatasummaryV1Input, XiaohongshuPgyBloggersDatasummaryV1Response, XiaohongshuNotesDetailV2Input, XiaohongshuNotesDetailV2Response, XiaohongshuNotesSearchV1Input, XiaohongshuNotesSearchV1Response, DouyinVideosDetailV2Input, DouyinVideosDetailV2Response, DouyinVideosSearchV1Input, DouyinVideosSearchV1Response, XPostsRepliesV1Input, XPostsRepliesV1Response, XPostsQuotesV1Input, XPostsQuotesV1Response, XUsersSearchV1Input, XUsersSearchV1Response, XUsersFollowersV1Input, XUsersFollowersV1Response, XUsersFollowingV1Input, XUsersFollowingV1Response, XiaohongshuNotesCommentsV1Input, XiaohongshuNotesCommentsV1Response, XiaohongshuNotesCommentsRepliesV1Input, XiaohongshuNotesCommentsRepliesV1Response, XiaohongshuUsersProfileV1Input, XiaohongshuUsersProfileV1Response, XiaohongshuUsersNotesV1Input, XiaohongshuUsersNotesV1Response, XiaohongshuUsersSearchV1Input, XiaohongshuUsersSearchV1Response, XiaohongshuPgyBloggersPerformanceV1Input, XiaohongshuPgyBloggersPerformanceV1Response, XiaohongshuPgyBloggersFansSummaryV1Input, XiaohongshuPgyBloggersFansSummaryV1Response, XiaohongshuPgyBloggersFansProfileV1Input, XiaohongshuPgyBloggersFansProfileV1Response, XiaohongshuPgyBloggersFansHistoryV1Input, XiaohongshuPgyBloggersFansHistoryV1Response, XiaohongshuPgyBloggersSearchV1Input, XiaohongshuPgyBloggersSearchV1Response, DouyinVideosCommentsV1Input, DouyinVideosCommentsV1Response, DouyinVideosCommentsRepliesV1Input, DouyinVideosCommentsRepliesV1Response, DouyinUsersProfileV1Input, DouyinUsersProfileV1Response, DouyinUsersVideosV2Input, DouyinUsersVideosV2Response, DouyinUsersSearchV1Input, DouyinUsersSearchV1Response, DouyinTrendsHotsearchV1Input, DouyinTrendsHotsearchV1Response, DouyinTrendsRisingV1Input, DouyinTrendsRisingV1Response, DouyinRankingsLowfanV1Input, DouyinRankingsLowfanV1Response, DouyinRankingsHighcompletionV1Input, DouyinRankingsHighcompletionV1Response, DouyinMixesDetailV1Input, DouyinMixesDetailV1Response, DouyinMixesVideosV1Input, DouyinMixesVideosV1Response, XiaohongshuTopicsDetailV1Input, XiaohongshuTopicsDetailV1Response, XiaohongshuTopicsNotesV1Input, XiaohongshuTopicsNotesV1Response, XiaohongshuProductsSearchV1Input, XiaohongshuProductsSearchV1Response, XiaohongshuProductsDetailV1Input, XiaohongshuProductsDetailV1Response, XPostsRetweetersV1Input, XPostsRetweetersV1Response, XTrendsListV1Input, XTrendsListV1Response, XUsersRepliesV1Input, XUsersRepliesV1Response, XUsersMediaV1Input, XUsersMediaV1Response, RedditPostsBatchV1Input, RedditPostsBatchV1Response, RedditSubredditsProfileV1Input, RedditSubredditsProfileV1Response, RedditSubredditsPostsV1Input, RedditSubredditsPostsV1Response, RedditUsersProfileV1Input, RedditUsersProfileV1Response, RedditSearchTrendingV1Input, RedditSearchTrendingV1Response, XPostsThreadContextV1Input, XPostsThreadContextV1Response, XArticlesDetailV1Input, XArticlesDetailV1Response, XUsersMentionsV1Input, XUsersMentionsV1Response, XUsersBatchDetailV1Input, XUsersBatchDetailV1Response, XUsersVerifiedFollowersV1Input, XUsersVerifiedFollowersV1Response, RedditUsersPostsV1Input, RedditUsersPostsV1Response, XiaohongshuProductsReviewsV1Input, XiaohongshuProductsReviewsV1Response, XiaohongshuCreatorInspirationRecommendedV1Input, XiaohongshuCreatorInspirationRecommendedV1Response, XiaohongshuCreatorInspirationHotV1Input, XiaohongshuCreatorInspirationHotV1Response, DouyinVideosBatchDetailV1Input, DouyinVideosBatchDetailV1Response, DouyinVideosRelatedV1Input, DouyinVideosRelatedV1Response, DouyinSearchGeneralV1Input, DouyinSearchGeneralV1Response, DouyinMusicDetailV1Input, DouyinMusicDetailV1Response, DouyinMusicVideosV1Input, DouyinMusicVideosV1Response, DouyinRankingsHighlikeV1Input, DouyinRankingsHighlikeV1Response, DouyinRankingsHighfanV1Input, DouyinRankingsHighfanV1Response, XListsTweetsV1Input, XListsTweetsV1Response, XUsersFollowerIdsV1Input, XUsersFollowerIdsV1Response, XCommunitiesMembersV1Input, XCommunitiesMembersV1Response, XCommunitiesDetailV1Input, XCommunitiesDetailV1Response, XCommunitiesTweetsV1Input, XCommunitiesTweetsV1Response, RedditSearchSuggestionsV1Input, RedditSearchSuggestionsV1Response, RedditSubredditsHighlightsV1Input, RedditSubredditsHighlightsV1Response, RedditSubredditsPostFlairsV1Input, RedditSubredditsPostFlairsV1Response, RedditUsersCommentsV1Input, RedditUsersCommentsV1Response, RedditUsersActiveSubredditsV1Input, RedditUsersActiveSubredditsV1Response, XiaohongshuUsersFavedNotesV1Input, XiaohongshuUsersFavedNotesV1Response, XiaohongshuImagesSearchV1Input, XiaohongshuImagesSearchV1Response, DouyinHashtagsDetailV1Input, DouyinHashtagsDetailV1Response, DouyinHashtagsVideosV1Input, DouyinHashtagsVideosV1Response, DouyinCreatorsSimilarV1Input, DouyinCreatorsSimilarV1Response, DouyinCreatorsTrendsCompareV1Input, DouyinCreatorsTrendsCompareV1Response, DouyinXingtuCreatorsSearchV1Input, DouyinXingtuCreatorsSearchV1Response, DouyinRankingsTopicsV1Input, DouyinRankingsTopicsV1Response, DouyinRankingsSearchesV1Input, DouyinRankingsSearchesV1Response, DouyinXingtuCreatorsBatchDetailV1Input, DouyinXingtuCreatorsBatchDetailV1Response, DouyinXingtuCreatorsAudienceV1Input, DouyinXingtuCreatorsAudienceV1Response, DouyinXingtuCreatorsVideosV1Input, DouyinXingtuCreatorsVideosV1Response, TiktokUsersProfileV1Input, TiktokUsersProfileV1Response, TiktokUsersVideosV1Input, TiktokUsersVideosV1Response, TiktokVideosDetailV1Input, TiktokVideosDetailV1Response, TiktokVideosCommentsV1Input, TiktokVideosCommentsV1Response, TiktokVideosCommentsRepliesV1Input, TiktokVideosCommentsRepliesV1Response, TiktokVideosSearchV1Input, TiktokVideosSearchV1Response, TiktokUsersSearchV1Input, TiktokUsersSearchV1Response, TiktokHashtagsDetailV1Input, TiktokHashtagsDetailV1Response, TiktokHashtagsVideosV1Input, TiktokHashtagsVideosV1Response, TiktokShopProductsSearchV1Input, TiktokShopProductsSearchV1Response, TiktokShopProductsReviewsV1Input, TiktokShopProductsReviewsV1Response, TiktokShopSellersProductsV1Input, TiktokShopSellersProductsV1Response, TiktokAdsSearchV1Input, TiktokAdsSearchV1Response, TiktokAdsDetailV1Input, TiktokAdsDetailV1Response, TiktokAdsSpotlightV1Input, TiktokAdsSpotlightV1Response, TiktokAdsPercentileV1Input, TiktokAdsPercentileV1Response, TiktokTrendsHashtagsV1Input, TiktokTrendsHashtagsV1Response, InstagramUsersProfileV1Input, InstagramUsersProfileV1Response, InstagramUsersPostsV1Input, InstagramUsersPostsV1Response, InstagramUsersReelsV1Input, InstagramUsersReelsV1Response, InstagramPostsDetailV1Input, InstagramPostsDetailV1Response, InstagramPostsCommentsV1Input, InstagramPostsCommentsV1Response, InstagramPostsCommentsRepliesV1Input, InstagramPostsCommentsRepliesV1Response, InstagramUsersSearchV1Input, InstagramUsersSearchV1Response, InstagramHashtagsPostsV1Input, InstagramHashtagsPostsV1Response, YoutubeVideosDetailV1Input, YoutubeVideosDetailV1Response, YoutubeVideosCommentsV1Input, YoutubeVideosCommentsV1Response, YoutubeChannelsProfileV1Input, YoutubeChannelsProfileV1Response, YoutubeChannelsVideosV1Input, YoutubeChannelsVideosV1Response, YoutubeChannelsShortsV1Input, YoutubeChannelsShortsV1Response, YoutubeVideosSearchV1Input, YoutubeVideosSearchV1Response, YoutubeChannelsSearchV1Input, YoutubeChannelsSearchV1Response, YoutubeVideosRelatedV1Input, YoutubeVideosRelatedV1Response, YoutubeVideosCaptionsV1Input, YoutubeVideosCaptionsV1Response, BilibiliVideosDetailV1Input, BilibiliVideosDetailV1Response, BilibiliVideosCommentsV1Input, BilibiliVideosCommentsV1Response, BilibiliVideosCommentsRepliesV1Input, BilibiliVideosCommentsRepliesV1Response, BilibiliUsersProfileV1Input, BilibiliUsersProfileV1Response, BilibiliUsersStatsV1Input, BilibiliUsersStatsV1Response, BilibiliUsersVideosV1Input, BilibiliUsersVideosV1Response, BilibiliVideosSearchV1Input, BilibiliVideosSearchV1Response, BilibiliVideosPopularV1Input, BilibiliVideosPopularV1Response, BilibiliVideosSubtitlesV1Input, BilibiliVideosSubtitlesV1Response, BilibiliHuahuoCreatorsSearchV1Input, BilibiliHuahuoCreatorsSearchV1Response, BilibiliHuahuoCreatorsDetailV1Input, BilibiliHuahuoCreatorsDetailV1Response, BilibiliHuahuoCreatorsFollowersTrendV1Input, BilibiliHuahuoCreatorsFollowersTrendV1Response, BilibiliHuahuoCreatorsVideosTrendV1Input, BilibiliHuahuoCreatorsVideosTrendV1Response, BilibiliHuahuoCreatorsVideosPerformanceV1Input, BilibiliHuahuoCreatorsVideosPerformanceV1Response, BilibiliHuahuoCreatorsSimilarV1Input, BilibiliHuahuoCreatorsSimilarV1Response, BilibiliHuahuoCreatorsShowcaseV1Input, BilibiliHuahuoCreatorsShowcaseV1Response, BilibiliHuahuoRankingsCreatorsV1Input, BilibiliHuahuoRankingsCreatorsV1Response, BilibiliHuahuoRankingsVideosV1Input, BilibiliHuahuoRankingsVideosV1Response, BilibiliHuahuoRankingsSeedingV1Input, BilibiliHuahuoRankingsSeedingV1Response, BilibiliHuahuoRankingsGoodsV1Input, BilibiliHuahuoRankingsGoodsV1Response, BilibiliHuahuoGoodsCreatorsV1Input, BilibiliHuahuoGoodsCreatorsV1Response, WeiboPostsDetailV1Input, WeiboPostsDetailV1Response, WeiboUsersProfileV1Input, WeiboUsersProfileV1Response, WeiboUsersPostsV1Input, WeiboUsersPostsV1Response, WeiboPostsCommentsV1Input, WeiboPostsCommentsV1Response, WeiboCommentsRepliesV1Input, WeiboCommentsRepliesV1Response, WeiboPostsSearchV1Input, WeiboPostsSearchV1Response, WeiboPostsHotV1Input, WeiboPostsHotV1Response, WeiboTrendsHotsearchV1Input, WeiboTrendsHotsearchV1Response, KuaishouVideosDetailV1Input, KuaishouVideosDetailV1Response, KuaishouVideosCommentsV1Input, KuaishouVideosCommentsV1Response, KuaishouVideosCommentsRepliesV1Input, KuaishouVideosCommentsRepliesV1Response, KuaishouUsersProfileV1Input, KuaishouUsersProfileV1Response, KuaishouUsersVideosV1Input, KuaishouUsersVideosV1Response, KuaishouVideosSearchV2Input, KuaishouVideosSearchV2Response, KuaishouUsersSearchV1Input, KuaishouUsersSearchV1Response, KuaishouTrendsHotV1Input, KuaishouTrendsHotV1Response, ZhihuTrendsHotV1Input, ZhihuTrendsHotV1Response, ZhihuSearchContentV1Input, ZhihuSearchContentV1Response, ZhihuUsersSearchV1Input, ZhihuUsersSearchV1Response, ZhihuQuestionsDetailV1Input, ZhihuQuestionsDetailV1Response, ZhihuQuestionsAnswersV1Input, ZhihuQuestionsAnswersV1Response, ZhihuAnswersDetailV1Input, ZhihuAnswersDetailV1Response, ZhihuAnswersCommentsV1Input, ZhihuAnswersCommentsV1Response, ZhihuCommentsRepliesV1Input, ZhihuCommentsRepliesV1Response, ZhihuUsersProfileV1Input, ZhihuUsersProfileV1Response, DouyinIndexHottopicsV1Input, DouyinIndexHottopicsV1Response, DouyinIndexVideosSuggestV1Input, DouyinIndexVideosSuggestV1Response, DouyinIndexCreatorsMetricsV1Input, DouyinIndexCreatorsMetricsV1Response, DouyinIndexCreatorsAudienceV1Input, DouyinIndexCreatorsAudienceV1Response, DouyinIndexCreatorsTopvideosV1Input, DouyinIndexCreatorsTopvideosV1Response, DouyinIndexKeywordsAudienceV1Input, DouyinIndexKeywordsAudienceV1Response, DouyinIndexKeywordsTrendsV1Input, DouyinIndexKeywordsTrendsV1Response, DouyinIndexKeywordsInterpretationV1Input, DouyinIndexKeywordsInterpretationV1Response, DouyinXingtuCreatorsDetailV1Input, DouyinXingtuCreatorsDetailV1Response, DouyinXingtuCreatorsPricesV1Input, DouyinXingtuCreatorsPricesV1Response, DouyinXingtuCreatorsContentkeywordsV1Input, DouyinXingtuCreatorsContentkeywordsV1Response, DouyinXingtuCreatorsSpreadV1Input, DouyinXingtuCreatorsSpreadV1Response, DouyinXingtuCreatorsConversionV1Input, DouyinXingtuCreatorsConversionV1Response, DouyinXingtuCreatorsLinkindexV1Input, DouyinXingtuCreatorsLinkindexV1Response, DouyinXingtuCreatorsContentmixV1Input, DouyinXingtuCreatorsContentmixV1Response, DouyinXingtuRankingsCatalogV1Input, DouyinXingtuRankingsCatalogV1Response, DouyinXingtuRankingsCreatorsV1Input, DouyinXingtuRankingsCreatorsV1Response, DouyinXingtuVideosStatsV1Input, DouyinXingtuVideosStatsV1Response, DouyinXingtuCampaignsV1Input, DouyinXingtuCampaignsV1Response, WechatMpAccountsProfileV1Input, WechatMpAccountsProfileV1Response, WechatMpAccountsArticlesV1Input, WechatMpAccountsArticlesV1Response, WechatMpArticlesDetailV1Input, WechatMpArticlesDetailV1Response, WechatMpArticlesStatsV1Input, WechatMpArticlesStatsV1Response, WechatChannelsUsersProfileV1Input, WechatChannelsUsersProfileV1Response, WechatChannelsUsersVideosV1Input, WechatChannelsUsersVideosV1Response, WechatChannelsVideosDetailV1Input, WechatChannelsVideosDetailV1Response, WechatChannelsVideosCommentsV1Input, WechatChannelsVideosCommentsV1Response, WechatChannelsUsersCollectionsV1Input, WechatChannelsUsersCollectionsV1Response, WechatChannelsCollectionsVideosV1Input, WechatChannelsCollectionsVideosV1Response, WechatChannelsUsersSearchV1Input, WechatChannelsUsersSearchV1Response, WechatSearchGeneralV1Input, WechatSearchGeneralV1Response, WechatSearchVideosV1Input, WechatSearchVideosV1Response, ToutiaoArticlesDetailV1Input, ToutiaoArticlesDetailV1Response, ToutiaoVideosDetailV1Input, ToutiaoVideosDetailV1Response, ToutiaoPostsCommentsV1Input, ToutiaoPostsCommentsV1Response, XiguaVideosSearchV1Input, XiguaVideosSearchV1Response, XiguaVideosCommentsV1Input, XiguaVideosCommentsV1Response, XiguaUsersProfileV1Input, XiguaUsersProfileV1Response, XiguaUsersVideosV1Input, XiguaUsersVideosV1Response, ThreadsUsersProfileV1Input, ThreadsUsersProfileV1Response, ThreadsUsersPostsV1Input, ThreadsUsersPostsV1Response, ThreadsPostsDetailV1Input, ThreadsPostsDetailV1Response, ThreadsPostsSearchV1Input, ThreadsPostsSearchV1Response, LinkedinCompaniesProfileV1Input, LinkedinCompaniesProfileV1Response, LinkedinCompaniesPostsV1Input, LinkedinCompaniesPostsV1Response, LinkedinPostsDetailV1Input, LinkedinPostsDetailV1Response, LinkedinPostsCommentsV1Input, LinkedinPostsCommentsV1Response, LinkedinJobsSearchV1Input, LinkedinJobsSearchV1Response, LinkedinJobsDetailV1Input, LinkedinJobsDetailV1Response, Lemon8UsersProfileV1Input, Lemon8UsersProfileV1Response, Lemon8PostsDetailV1Input, Lemon8PostsDetailV1Response, Lemon8PostsCommentsV1Input, Lemon8PostsCommentsV1Response, Lemon8TrendsHotV1Input, Lemon8TrendsHotV1Response, Lemon8TopicsDetailV1Input, Lemon8TopicsDetailV1Response, Lemon8TopicsPostsV1Input, Lemon8TopicsPostsV1Response, TelegramChannelsProfileV1Input, TelegramChannelsProfileV1Response, TelegramChannelsBatchV1Input, TelegramChannelsBatchV1Response, TelegramChannelsMessagesV1Input, TelegramChannelsMessagesV1Response, TelegramMessagesDetailV1Input, TelegramMessagesDetailV1Response, TelegramChannelsMessagesSearchV1Input, TelegramChannelsMessagesSearchV1Response
|
|
4
|
+
__all__ = ["DataXClient", "DataXConnectionError", "DataXError", "DataXPaginationError", "ExactApiClient", "ExactApiConnectionError", "ExactApiError", "ExactApiPaginationError", "Problem", "Usage", "OperationId", "OperationInput", "OperationResponse", "OPERATIONS", "USAGE_OPERATION", "EXAMPLES", "XPostsSearchV1Input", "XPostsSearchV1Response", "XPostsDetailV1Input", "XPostsDetailV1Response", "XUsersProfileV1Input", "XUsersProfileV1Response", "XUsersPostsV1Input", "XUsersPostsV1Response", "RedditPostsDetailV1Input", "RedditPostsDetailV1Response", "RedditPostsSearchV1Input", "RedditPostsSearchV1Response", "RedditPostsCommentsV1Input", "RedditPostsCommentsV1Response", "XiaohongshuPgyNotesDetailV1Input", "XiaohongshuPgyNotesDetailV1Response", "XiaohongshuPgyBloggersDetailV1Input", "XiaohongshuPgyBloggersDetailV1Response", "XiaohongshuPgyBloggersNotesV1Input", "XiaohongshuPgyBloggersNotesV1Response", "XiaohongshuPgyBloggersCoredataV1Input", "XiaohongshuPgyBloggersCoredataV1Response", "XiaohongshuPgyBloggersDatasummaryV1Input", "XiaohongshuPgyBloggersDatasummaryV1Response", "XiaohongshuNotesDetailV2Input", "XiaohongshuNotesDetailV2Response", "XiaohongshuNotesSearchV1Input", "XiaohongshuNotesSearchV1Response", "DouyinVideosDetailV2Input", "DouyinVideosDetailV2Response", "DouyinVideosSearchV1Input", "DouyinVideosSearchV1Response", "XPostsRepliesV1Input", "XPostsRepliesV1Response", "XPostsQuotesV1Input", "XPostsQuotesV1Response", "XUsersSearchV1Input", "XUsersSearchV1Response", "XUsersFollowersV1Input", "XUsersFollowersV1Response", "XUsersFollowingV1Input", "XUsersFollowingV1Response", "XiaohongshuNotesCommentsV1Input", "XiaohongshuNotesCommentsV1Response", "XiaohongshuNotesCommentsRepliesV1Input", "XiaohongshuNotesCommentsRepliesV1Response", "XiaohongshuUsersProfileV1Input", "XiaohongshuUsersProfileV1Response", "XiaohongshuUsersNotesV1Input", "XiaohongshuUsersNotesV1Response", "XiaohongshuUsersSearchV1Input", "XiaohongshuUsersSearchV1Response", "XiaohongshuPgyBloggersPerformanceV1Input", "XiaohongshuPgyBloggersPerformanceV1Response", "XiaohongshuPgyBloggersFansSummaryV1Input", "XiaohongshuPgyBloggersFansSummaryV1Response", "XiaohongshuPgyBloggersFansProfileV1Input", "XiaohongshuPgyBloggersFansProfileV1Response", "XiaohongshuPgyBloggersFansHistoryV1Input", "XiaohongshuPgyBloggersFansHistoryV1Response", "XiaohongshuPgyBloggersSearchV1Input", "XiaohongshuPgyBloggersSearchV1Response", "DouyinVideosCommentsV1Input", "DouyinVideosCommentsV1Response", "DouyinVideosCommentsRepliesV1Input", "DouyinVideosCommentsRepliesV1Response", "DouyinUsersProfileV1Input", "DouyinUsersProfileV1Response", "DouyinUsersVideosV2Input", "DouyinUsersVideosV2Response", "DouyinUsersSearchV1Input", "DouyinUsersSearchV1Response", "DouyinTrendsHotsearchV1Input", "DouyinTrendsHotsearchV1Response", "DouyinTrendsRisingV1Input", "DouyinTrendsRisingV1Response", "DouyinRankingsLowfanV1Input", "DouyinRankingsLowfanV1Response", "DouyinRankingsHighcompletionV1Input", "DouyinRankingsHighcompletionV1Response", "DouyinMixesDetailV1Input", "DouyinMixesDetailV1Response", "DouyinMixesVideosV1Input", "DouyinMixesVideosV1Response", "XiaohongshuTopicsDetailV1Input", "XiaohongshuTopicsDetailV1Response", "XiaohongshuTopicsNotesV1Input", "XiaohongshuTopicsNotesV1Response", "XiaohongshuProductsSearchV1Input", "XiaohongshuProductsSearchV1Response", "XiaohongshuProductsDetailV1Input", "XiaohongshuProductsDetailV1Response", "XPostsRetweetersV1Input", "XPostsRetweetersV1Response", "XTrendsListV1Input", "XTrendsListV1Response", "XUsersRepliesV1Input", "XUsersRepliesV1Response", "XUsersMediaV1Input", "XUsersMediaV1Response", "RedditPostsBatchV1Input", "RedditPostsBatchV1Response", "RedditSubredditsProfileV1Input", "RedditSubredditsProfileV1Response", "RedditSubredditsPostsV1Input", "RedditSubredditsPostsV1Response", "RedditUsersProfileV1Input", "RedditUsersProfileV1Response", "RedditSearchTrendingV1Input", "RedditSearchTrendingV1Response", "XPostsThreadContextV1Input", "XPostsThreadContextV1Response", "XArticlesDetailV1Input", "XArticlesDetailV1Response", "XUsersMentionsV1Input", "XUsersMentionsV1Response", "XUsersBatchDetailV1Input", "XUsersBatchDetailV1Response", "XUsersVerifiedFollowersV1Input", "XUsersVerifiedFollowersV1Response", "RedditUsersPostsV1Input", "RedditUsersPostsV1Response", "XiaohongshuProductsReviewsV1Input", "XiaohongshuProductsReviewsV1Response", "XiaohongshuCreatorInspirationRecommendedV1Input", "XiaohongshuCreatorInspirationRecommendedV1Response", "XiaohongshuCreatorInspirationHotV1Input", "XiaohongshuCreatorInspirationHotV1Response", "DouyinVideosBatchDetailV1Input", "DouyinVideosBatchDetailV1Response", "DouyinVideosRelatedV1Input", "DouyinVideosRelatedV1Response", "DouyinSearchGeneralV1Input", "DouyinSearchGeneralV1Response", "DouyinMusicDetailV1Input", "DouyinMusicDetailV1Response", "DouyinMusicVideosV1Input", "DouyinMusicVideosV1Response", "DouyinRankingsHighlikeV1Input", "DouyinRankingsHighlikeV1Response", "DouyinRankingsHighfanV1Input", "DouyinRankingsHighfanV1Response", "XListsTweetsV1Input", "XListsTweetsV1Response", "XUsersFollowerIdsV1Input", "XUsersFollowerIdsV1Response", "XCommunitiesMembersV1Input", "XCommunitiesMembersV1Response", "XCommunitiesDetailV1Input", "XCommunitiesDetailV1Response", "XCommunitiesTweetsV1Input", "XCommunitiesTweetsV1Response", "RedditSearchSuggestionsV1Input", "RedditSearchSuggestionsV1Response", "RedditSubredditsHighlightsV1Input", "RedditSubredditsHighlightsV1Response", "RedditSubredditsPostFlairsV1Input", "RedditSubredditsPostFlairsV1Response", "RedditUsersCommentsV1Input", "RedditUsersCommentsV1Response", "RedditUsersActiveSubredditsV1Input", "RedditUsersActiveSubredditsV1Response", "XiaohongshuUsersFavedNotesV1Input", "XiaohongshuUsersFavedNotesV1Response", "XiaohongshuImagesSearchV1Input", "XiaohongshuImagesSearchV1Response", "DouyinHashtagsDetailV1Input", "DouyinHashtagsDetailV1Response", "DouyinHashtagsVideosV1Input", "DouyinHashtagsVideosV1Response", "DouyinCreatorsSimilarV1Input", "DouyinCreatorsSimilarV1Response", "DouyinCreatorsTrendsCompareV1Input", "DouyinCreatorsTrendsCompareV1Response", "DouyinXingtuCreatorsSearchV1Input", "DouyinXingtuCreatorsSearchV1Response", "DouyinRankingsTopicsV1Input", "DouyinRankingsTopicsV1Response", "DouyinRankingsSearchesV1Input", "DouyinRankingsSearchesV1Response", "DouyinXingtuCreatorsBatchDetailV1Input", "DouyinXingtuCreatorsBatchDetailV1Response", "DouyinXingtuCreatorsAudienceV1Input", "DouyinXingtuCreatorsAudienceV1Response", "DouyinXingtuCreatorsVideosV1Input", "DouyinXingtuCreatorsVideosV1Response", "TiktokUsersProfileV1Input", "TiktokUsersProfileV1Response", "TiktokUsersVideosV1Input", "TiktokUsersVideosV1Response", "TiktokVideosDetailV1Input", "TiktokVideosDetailV1Response", "TiktokVideosCommentsV1Input", "TiktokVideosCommentsV1Response", "TiktokVideosCommentsRepliesV1Input", "TiktokVideosCommentsRepliesV1Response", "TiktokVideosSearchV1Input", "TiktokVideosSearchV1Response", "TiktokUsersSearchV1Input", "TiktokUsersSearchV1Response", "TiktokHashtagsDetailV1Input", "TiktokHashtagsDetailV1Response", "TiktokHashtagsVideosV1Input", "TiktokHashtagsVideosV1Response", "TiktokShopProductsSearchV1Input", "TiktokShopProductsSearchV1Response", "TiktokShopProductsReviewsV1Input", "TiktokShopProductsReviewsV1Response", "TiktokShopSellersProductsV1Input", "TiktokShopSellersProductsV1Response", "TiktokAdsSearchV1Input", "TiktokAdsSearchV1Response", "TiktokAdsDetailV1Input", "TiktokAdsDetailV1Response", "TiktokAdsSpotlightV1Input", "TiktokAdsSpotlightV1Response", "TiktokAdsPercentileV1Input", "TiktokAdsPercentileV1Response", "TiktokTrendsHashtagsV1Input", "TiktokTrendsHashtagsV1Response", "InstagramUsersProfileV1Input", "InstagramUsersProfileV1Response", "InstagramUsersPostsV1Input", "InstagramUsersPostsV1Response", "InstagramUsersReelsV1Input", "InstagramUsersReelsV1Response", "InstagramPostsDetailV1Input", "InstagramPostsDetailV1Response", "InstagramPostsCommentsV1Input", "InstagramPostsCommentsV1Response", "InstagramPostsCommentsRepliesV1Input", "InstagramPostsCommentsRepliesV1Response", "InstagramUsersSearchV1Input", "InstagramUsersSearchV1Response", "InstagramHashtagsPostsV1Input", "InstagramHashtagsPostsV1Response", "YoutubeVideosDetailV1Input", "YoutubeVideosDetailV1Response", "YoutubeVideosCommentsV1Input", "YoutubeVideosCommentsV1Response", "YoutubeChannelsProfileV1Input", "YoutubeChannelsProfileV1Response", "YoutubeChannelsVideosV1Input", "YoutubeChannelsVideosV1Response", "YoutubeChannelsShortsV1Input", "YoutubeChannelsShortsV1Response", "YoutubeVideosSearchV1Input", "YoutubeVideosSearchV1Response", "YoutubeChannelsSearchV1Input", "YoutubeChannelsSearchV1Response", "YoutubeVideosRelatedV1Input", "YoutubeVideosRelatedV1Response", "YoutubeVideosCaptionsV1Input", "YoutubeVideosCaptionsV1Response", "BilibiliVideosDetailV1Input", "BilibiliVideosDetailV1Response", "BilibiliVideosCommentsV1Input", "BilibiliVideosCommentsV1Response", "BilibiliVideosCommentsRepliesV1Input", "BilibiliVideosCommentsRepliesV1Response", "BilibiliUsersProfileV1Input", "BilibiliUsersProfileV1Response", "BilibiliUsersStatsV1Input", "BilibiliUsersStatsV1Response", "BilibiliUsersVideosV1Input", "BilibiliUsersVideosV1Response", "BilibiliVideosSearchV1Input", "BilibiliVideosSearchV1Response", "BilibiliVideosPopularV1Input", "BilibiliVideosPopularV1Response", "BilibiliVideosSubtitlesV1Input", "BilibiliVideosSubtitlesV1Response", "BilibiliHuahuoCreatorsSearchV1Input", "BilibiliHuahuoCreatorsSearchV1Response", "BilibiliHuahuoCreatorsDetailV1Input", "BilibiliHuahuoCreatorsDetailV1Response", "BilibiliHuahuoCreatorsFollowersTrendV1Input", "BilibiliHuahuoCreatorsFollowersTrendV1Response", "BilibiliHuahuoCreatorsVideosTrendV1Input", "BilibiliHuahuoCreatorsVideosTrendV1Response", "BilibiliHuahuoCreatorsVideosPerformanceV1Input", "BilibiliHuahuoCreatorsVideosPerformanceV1Response", "BilibiliHuahuoCreatorsSimilarV1Input", "BilibiliHuahuoCreatorsSimilarV1Response", "BilibiliHuahuoCreatorsShowcaseV1Input", "BilibiliHuahuoCreatorsShowcaseV1Response", "BilibiliHuahuoRankingsCreatorsV1Input", "BilibiliHuahuoRankingsCreatorsV1Response", "BilibiliHuahuoRankingsVideosV1Input", "BilibiliHuahuoRankingsVideosV1Response", "BilibiliHuahuoRankingsSeedingV1Input", "BilibiliHuahuoRankingsSeedingV1Response", "BilibiliHuahuoRankingsGoodsV1Input", "BilibiliHuahuoRankingsGoodsV1Response", "BilibiliHuahuoGoodsCreatorsV1Input", "BilibiliHuahuoGoodsCreatorsV1Response", "WeiboPostsDetailV1Input", "WeiboPostsDetailV1Response", "WeiboUsersProfileV1Input", "WeiboUsersProfileV1Response", "WeiboUsersPostsV1Input", "WeiboUsersPostsV1Response", "WeiboPostsCommentsV1Input", "WeiboPostsCommentsV1Response", "WeiboCommentsRepliesV1Input", "WeiboCommentsRepliesV1Response", "WeiboPostsSearchV1Input", "WeiboPostsSearchV1Response", "WeiboPostsHotV1Input", "WeiboPostsHotV1Response", "WeiboTrendsHotsearchV1Input", "WeiboTrendsHotsearchV1Response", "KuaishouVideosDetailV1Input", "KuaishouVideosDetailV1Response", "KuaishouVideosCommentsV1Input", "KuaishouVideosCommentsV1Response", "KuaishouVideosCommentsRepliesV1Input", "KuaishouVideosCommentsRepliesV1Response", "KuaishouUsersProfileV1Input", "KuaishouUsersProfileV1Response", "KuaishouUsersVideosV1Input", "KuaishouUsersVideosV1Response", "KuaishouVideosSearchV2Input", "KuaishouVideosSearchV2Response", "KuaishouUsersSearchV1Input", "KuaishouUsersSearchV1Response", "KuaishouTrendsHotV1Input", "KuaishouTrendsHotV1Response", "ZhihuTrendsHotV1Input", "ZhihuTrendsHotV1Response", "ZhihuSearchContentV1Input", "ZhihuSearchContentV1Response", "ZhihuUsersSearchV1Input", "ZhihuUsersSearchV1Response", "ZhihuQuestionsDetailV1Input", "ZhihuQuestionsDetailV1Response", "ZhihuQuestionsAnswersV1Input", "ZhihuQuestionsAnswersV1Response", "ZhihuAnswersDetailV1Input", "ZhihuAnswersDetailV1Response", "ZhihuAnswersCommentsV1Input", "ZhihuAnswersCommentsV1Response", "ZhihuCommentsRepliesV1Input", "ZhihuCommentsRepliesV1Response", "ZhihuUsersProfileV1Input", "ZhihuUsersProfileV1Response", "DouyinIndexHottopicsV1Input", "DouyinIndexHottopicsV1Response", "DouyinIndexVideosSuggestV1Input", "DouyinIndexVideosSuggestV1Response", "DouyinIndexCreatorsMetricsV1Input", "DouyinIndexCreatorsMetricsV1Response", "DouyinIndexCreatorsAudienceV1Input", "DouyinIndexCreatorsAudienceV1Response", "DouyinIndexCreatorsTopvideosV1Input", "DouyinIndexCreatorsTopvideosV1Response", "DouyinIndexKeywordsAudienceV1Input", "DouyinIndexKeywordsAudienceV1Response", "DouyinIndexKeywordsTrendsV1Input", "DouyinIndexKeywordsTrendsV1Response", "DouyinIndexKeywordsInterpretationV1Input", "DouyinIndexKeywordsInterpretationV1Response", "DouyinXingtuCreatorsDetailV1Input", "DouyinXingtuCreatorsDetailV1Response", "DouyinXingtuCreatorsPricesV1Input", "DouyinXingtuCreatorsPricesV1Response", "DouyinXingtuCreatorsContentkeywordsV1Input", "DouyinXingtuCreatorsContentkeywordsV1Response", "DouyinXingtuCreatorsSpreadV1Input", "DouyinXingtuCreatorsSpreadV1Response", "DouyinXingtuCreatorsConversionV1Input", "DouyinXingtuCreatorsConversionV1Response", "DouyinXingtuCreatorsLinkindexV1Input", "DouyinXingtuCreatorsLinkindexV1Response", "DouyinXingtuCreatorsContentmixV1Input", "DouyinXingtuCreatorsContentmixV1Response", "DouyinXingtuRankingsCatalogV1Input", "DouyinXingtuRankingsCatalogV1Response", "DouyinXingtuRankingsCreatorsV1Input", "DouyinXingtuRankingsCreatorsV1Response", "DouyinXingtuVideosStatsV1Input", "DouyinXingtuVideosStatsV1Response", "DouyinXingtuCampaignsV1Input", "DouyinXingtuCampaignsV1Response", "WechatMpAccountsProfileV1Input", "WechatMpAccountsProfileV1Response", "WechatMpAccountsArticlesV1Input", "WechatMpAccountsArticlesV1Response", "WechatMpArticlesDetailV1Input", "WechatMpArticlesDetailV1Response", "WechatMpArticlesStatsV1Input", "WechatMpArticlesStatsV1Response", "WechatChannelsUsersProfileV1Input", "WechatChannelsUsersProfileV1Response", "WechatChannelsUsersVideosV1Input", "WechatChannelsUsersVideosV1Response", "WechatChannelsVideosDetailV1Input", "WechatChannelsVideosDetailV1Response", "WechatChannelsVideosCommentsV1Input", "WechatChannelsVideosCommentsV1Response", "WechatChannelsUsersCollectionsV1Input", "WechatChannelsUsersCollectionsV1Response", "WechatChannelsCollectionsVideosV1Input", "WechatChannelsCollectionsVideosV1Response", "WechatChannelsUsersSearchV1Input", "WechatChannelsUsersSearchV1Response", "WechatSearchGeneralV1Input", "WechatSearchGeneralV1Response", "WechatSearchVideosV1Input", "WechatSearchVideosV1Response", "ToutiaoArticlesDetailV1Input", "ToutiaoArticlesDetailV1Response", "ToutiaoVideosDetailV1Input", "ToutiaoVideosDetailV1Response", "ToutiaoPostsCommentsV1Input", "ToutiaoPostsCommentsV1Response", "XiguaVideosSearchV1Input", "XiguaVideosSearchV1Response", "XiguaVideosCommentsV1Input", "XiguaVideosCommentsV1Response", "XiguaUsersProfileV1Input", "XiguaUsersProfileV1Response", "XiguaUsersVideosV1Input", "XiguaUsersVideosV1Response", "ThreadsUsersProfileV1Input", "ThreadsUsersProfileV1Response", "ThreadsUsersPostsV1Input", "ThreadsUsersPostsV1Response", "ThreadsPostsDetailV1Input", "ThreadsPostsDetailV1Response", "ThreadsPostsSearchV1Input", "ThreadsPostsSearchV1Response", "LinkedinCompaniesProfileV1Input", "LinkedinCompaniesProfileV1Response", "LinkedinCompaniesPostsV1Input", "LinkedinCompaniesPostsV1Response", "LinkedinPostsDetailV1Input", "LinkedinPostsDetailV1Response", "LinkedinPostsCommentsV1Input", "LinkedinPostsCommentsV1Response", "LinkedinJobsSearchV1Input", "LinkedinJobsSearchV1Response", "LinkedinJobsDetailV1Input", "LinkedinJobsDetailV1Response", "Lemon8UsersProfileV1Input", "Lemon8UsersProfileV1Response", "Lemon8PostsDetailV1Input", "Lemon8PostsDetailV1Response", "Lemon8PostsCommentsV1Input", "Lemon8PostsCommentsV1Response", "Lemon8TrendsHotV1Input", "Lemon8TrendsHotV1Response", "Lemon8TopicsDetailV1Input", "Lemon8TopicsDetailV1Response", "Lemon8TopicsPostsV1Input", "Lemon8TopicsPostsV1Response", "TelegramChannelsProfileV1Input", "TelegramChannelsProfileV1Response", "TelegramChannelsBatchV1Input", "TelegramChannelsBatchV1Response", "TelegramChannelsMessagesV1Input", "TelegramChannelsMessagesV1Response", "TelegramMessagesDetailV1Input", "TelegramMessagesDetailV1Response", "TelegramChannelsMessagesSearchV1Input", "TelegramChannelsMessagesSearchV1Response"]
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import io
|
|
3
3
|
import http.client
|
|
4
4
|
import math
|
|
5
|
+
import random
|
|
5
6
|
import re
|
|
6
7
|
import socket
|
|
7
8
|
import time
|
|
@@ -13,9 +14,30 @@ from urllib.parse import quote, urlsplit
|
|
|
13
14
|
from .generated import OPERATIONS, USAGE_OPERATION, GeneratedOperationsClient, OperationId, OperationInput, OperationResponse, RedditPostsDetailV1Input, RedditPostsDetailV1Response, Usage, XiaohongshuPgyBloggersCoredataV1Input, XiaohongshuPgyBloggersCoredataV1Response, XiaohongshuPgyBloggersDatasummaryV1Input, XiaohongshuPgyBloggersDatasummaryV1Response, XiaohongshuPgyBloggersDetailV1Input, XiaohongshuPgyBloggersDetailV1Response, XiaohongshuPgyBloggersNotesV1Input, XiaohongshuPgyBloggersNotesV1Response, XiaohongshuPgyNotesDetailV1Input, XiaohongshuPgyNotesDetailV1Response, XPostsDetailV1Input, XPostsDetailV1Response, XPostsSearchV1Input, XPostsSearchV1Response
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
DEFAULT_TIMEOUT = 60.0
|
|
18
|
+
"""Seconds per network attempt; above the slowest data-source timeout (30 s)."""
|
|
19
|
+
IN_PROGRESS_WAIT_LIMIT = 90.0
|
|
20
|
+
"""Seconds to keep polling a `request_in_progress` call with the same key."""
|
|
21
|
+
NON_RETRYABLE_CODES = frozenset({
|
|
22
|
+
"account_suspended",
|
|
23
|
+
"listing_paused",
|
|
24
|
+
"upstream_failed",
|
|
25
|
+
"upstream_schema_changed",
|
|
26
|
+
"settlement_failed",
|
|
27
|
+
"previous_execution_failed",
|
|
28
|
+
# Not returned by live calls since 2026-10-08 (empty results are a charged
|
|
29
|
+
# 200); kept so older error handling keeps working.
|
|
30
|
+
"no_results",
|
|
31
|
+
"not_found",
|
|
32
|
+
"route_not_found",
|
|
33
|
+
"method_not_allowed",
|
|
34
|
+
})
|
|
35
|
+
"""Codes that repeat deterministically, so an automatic retry cannot help."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DataXError(Exception):
|
|
17
39
|
def __init__(self, status: int, problem: dict[str, Any], idempotency_key: str | None = None, retry_after: str | None = None):
|
|
18
|
-
super().__init__(problem.get("detail", f"
|
|
40
|
+
super().__init__(problem.get("detail", f"DataXAPI request failed with HTTP {status}"))
|
|
19
41
|
self.status = status
|
|
20
42
|
self.code = problem.get("code", "unknown_error")
|
|
21
43
|
self.detail = problem.get("detail", str(self))
|
|
@@ -27,14 +49,14 @@ class ExactApiError(Exception):
|
|
|
27
49
|
self.idempotency_key = idempotency_key
|
|
28
50
|
|
|
29
51
|
|
|
30
|
-
class
|
|
52
|
+
class DataXConnectionError(Exception):
|
|
31
53
|
def __init__(self, message: str, idempotency_key: str | None):
|
|
32
54
|
super().__init__(message)
|
|
33
55
|
self.idempotency_key = idempotency_key
|
|
34
56
|
|
|
35
57
|
|
|
36
|
-
class
|
|
37
|
-
def __init__(self, base_url: str, api_key: str, *, timeout: float =
|
|
58
|
+
class DataXClient(GeneratedOperationsClient):
|
|
59
|
+
def __init__(self, base_url: str, api_key: str, *, timeout: float = DEFAULT_TIMEOUT, max_retries: int = 2):
|
|
38
60
|
if not base_url or not api_key:
|
|
39
61
|
raise ValueError("base_url and api_key are required")
|
|
40
62
|
parsed_base_url = urlsplit(base_url)
|
|
@@ -51,7 +73,12 @@ class ExactApiClient(GeneratedOperationsClient):
|
|
|
51
73
|
self.usage = _Usage(self)
|
|
52
74
|
|
|
53
75
|
def _call_operation(self, operation_id: OperationId, input: OperationInput, *, idempotency_key: str | None = None) -> OperationResponse:
|
|
54
|
-
operation = OPERATIONS
|
|
76
|
+
operation = OPERATIONS.get(operation_id)
|
|
77
|
+
if operation is None:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"unknown operation {operation_id!r}; valid ids are the keys of dataxapi.OPERATIONS "
|
|
80
|
+
"(the OpenAPI operationId, e.g. 'x_posts_search_v1'); see https://dataxapi.com/en/docs/sdks/"
|
|
81
|
+
)
|
|
55
82
|
return cast(OperationResponse, self._execute(operation["method"], operation["path"], dict(input), idempotency_key))
|
|
56
83
|
|
|
57
84
|
def _execute(self, method: str, path: str, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
|
|
@@ -67,7 +94,9 @@ class ExactApiClient(GeneratedOperationsClient):
|
|
|
67
94
|
if idempotency_key:
|
|
68
95
|
headers["Idempotency-Key"] = idempotency_key
|
|
69
96
|
|
|
70
|
-
|
|
97
|
+
attempt = 0
|
|
98
|
+
in_progress_since: float | None = None
|
|
99
|
+
while True:
|
|
71
100
|
try:
|
|
72
101
|
status, response_headers, payload = self._send(method, path, headers, data)
|
|
73
102
|
try:
|
|
@@ -76,28 +105,40 @@ class ExactApiClient(GeneratedOperationsClient):
|
|
|
76
105
|
decoded = None
|
|
77
106
|
if 200 <= status < 300:
|
|
78
107
|
if not isinstance(decoded, dict):
|
|
79
|
-
raise _InvalidResponse("
|
|
108
|
+
raise _InvalidResponse("DataXAPI success response must be a JSON object")
|
|
80
109
|
return cast(dict[str, Any], decoded)
|
|
81
110
|
problem: dict[str, Any] = cast(dict[str, Any], decoded) if isinstance(decoded, dict) else {
|
|
82
111
|
"code": "invalid_error_response",
|
|
83
|
-
"detail": f"
|
|
112
|
+
"detail": f"DataXAPI returned an invalid HTTP {status} error body.",
|
|
84
113
|
}
|
|
85
|
-
error =
|
|
114
|
+
error = DataXError(status, problem, idempotency_key, response_headers.get("Retry-After"))
|
|
115
|
+
delay = _retry_delay(error.retry_after, problem)
|
|
116
|
+
if status == 409 and error.code == "request_in_progress" and idempotency_key and self._max_retries > 0:
|
|
117
|
+
# The original call is still running and may still charge; its
|
|
118
|
+
# outcome arrives by polling with the same key, not a new call.
|
|
119
|
+
now = time.monotonic()
|
|
120
|
+
in_progress_since = now if in_progress_since is None else in_progress_since
|
|
121
|
+
if delay is not None and delay <= self._timeout and now - in_progress_since + delay <= IN_PROGRESS_WAIT_LIMIT:
|
|
122
|
+
if delay > 0:
|
|
123
|
+
time.sleep(delay)
|
|
124
|
+
continue
|
|
125
|
+
raise error
|
|
86
126
|
if attempt < self._max_retries and _retryable(status, error.code):
|
|
87
|
-
delay = _retry_delay(error.retry_after, problem)
|
|
88
127
|
if delay is not None and delay <= self._timeout:
|
|
128
|
+
attempt += 1
|
|
89
129
|
if delay > 0:
|
|
90
130
|
time.sleep(delay)
|
|
91
131
|
continue
|
|
92
132
|
raise error
|
|
93
133
|
except (OSError, TimeoutError, socket.timeout, http.client.HTTPException, _InvalidResponse) as error:
|
|
94
134
|
if attempt < self._max_retries:
|
|
135
|
+
time.sleep(_network_backoff(attempt, self._timeout))
|
|
136
|
+
attempt += 1
|
|
95
137
|
continue
|
|
96
|
-
raise
|
|
138
|
+
raise DataXConnectionError(
|
|
97
139
|
"The request outcome is unknown. Retry the same logical operation with this idempotency key.",
|
|
98
140
|
idempotency_key,
|
|
99
141
|
) from error
|
|
100
|
-
raise AssertionError("unreachable")
|
|
101
142
|
|
|
102
143
|
def get_usage(self, request_id: str) -> Usage:
|
|
103
144
|
path = USAGE_OPERATION["path"].replace("{requestId}", quote(request_id, safe=""))
|
|
@@ -158,12 +199,12 @@ class _DeadlineReader(io.RawIOBase):
|
|
|
158
199
|
|
|
159
200
|
|
|
160
201
|
class _X:
|
|
161
|
-
def __init__(self, client:
|
|
202
|
+
def __init__(self, client: DataXClient):
|
|
162
203
|
self.posts = _XPosts(client)
|
|
163
204
|
|
|
164
205
|
|
|
165
206
|
class _XPosts:
|
|
166
|
-
def __init__(self, client:
|
|
207
|
+
def __init__(self, client: DataXClient):
|
|
167
208
|
self._client = client
|
|
168
209
|
|
|
169
210
|
def search(self, input: XPostsSearchV1Input, *, idempotency_key: str | None = None) -> XPostsSearchV1Response:
|
|
@@ -176,7 +217,7 @@ class _XPosts:
|
|
|
176
217
|
page = self.search(cast(XPostsSearchV1Input, current))
|
|
177
218
|
cursor = page["page"]["next_cursor"]
|
|
178
219
|
if cursor and cursor in seen_cursors:
|
|
179
|
-
raise
|
|
220
|
+
raise DataXPaginationError(cursor)
|
|
180
221
|
if cursor:
|
|
181
222
|
seen_cursors.add(cursor)
|
|
182
223
|
yield page
|
|
@@ -189,12 +230,12 @@ class _XPosts:
|
|
|
189
230
|
|
|
190
231
|
|
|
191
232
|
class _Reddit:
|
|
192
|
-
def __init__(self, client:
|
|
233
|
+
def __init__(self, client: DataXClient):
|
|
193
234
|
self.posts = _RedditPosts(client)
|
|
194
235
|
|
|
195
236
|
|
|
196
237
|
class _RedditPosts:
|
|
197
|
-
def __init__(self, client:
|
|
238
|
+
def __init__(self, client: DataXClient):
|
|
198
239
|
self._client = client
|
|
199
240
|
|
|
200
241
|
def detail(self, input: RedditPostsDetailV1Input, *, idempotency_key: str | None = None) -> RedditPostsDetailV1Response:
|
|
@@ -202,18 +243,18 @@ class _RedditPosts:
|
|
|
202
243
|
|
|
203
244
|
|
|
204
245
|
class _Xiaohongshu:
|
|
205
|
-
def __init__(self, client:
|
|
246
|
+
def __init__(self, client: DataXClient):
|
|
206
247
|
self.pgy = _XiaohongshuPgy(client)
|
|
207
248
|
|
|
208
249
|
|
|
209
250
|
class _XiaohongshuPgy:
|
|
210
|
-
def __init__(self, client:
|
|
251
|
+
def __init__(self, client: DataXClient):
|
|
211
252
|
self.notes = _XiaohongshuPgyNotes(client)
|
|
212
253
|
self.bloggers = _XiaohongshuPgyBloggers(client)
|
|
213
254
|
|
|
214
255
|
|
|
215
256
|
class _XiaohongshuPgyNotes:
|
|
216
|
-
def __init__(self, client:
|
|
257
|
+
def __init__(self, client: DataXClient):
|
|
217
258
|
self._client = client
|
|
218
259
|
|
|
219
260
|
def detail(self, input: XiaohongshuPgyNotesDetailV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyNotesDetailV1Response:
|
|
@@ -221,7 +262,7 @@ class _XiaohongshuPgyNotes:
|
|
|
221
262
|
|
|
222
263
|
|
|
223
264
|
class _XiaohongshuPgyBloggers:
|
|
224
|
-
def __init__(self, client:
|
|
265
|
+
def __init__(self, client: DataXClient):
|
|
225
266
|
self._client = client
|
|
226
267
|
|
|
227
268
|
def detail(self, input: XiaohongshuPgyBloggersDetailV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyBloggersDetailV1Response:
|
|
@@ -238,7 +279,7 @@ class _XiaohongshuPgyBloggers:
|
|
|
238
279
|
|
|
239
280
|
|
|
240
281
|
class _Usage:
|
|
241
|
-
def __init__(self, client:
|
|
282
|
+
def __init__(self, client: DataXClient):
|
|
242
283
|
self._client = client
|
|
243
284
|
|
|
244
285
|
def get(self, request_id: str) -> Usage:
|
|
@@ -249,7 +290,7 @@ class _InvalidResponse(Exception):
|
|
|
249
290
|
pass
|
|
250
291
|
|
|
251
292
|
|
|
252
|
-
class
|
|
293
|
+
class DataXPaginationError(Exception):
|
|
253
294
|
def __init__(self, cursor: str):
|
|
254
295
|
super().__init__("Pagination stopped because the API returned a repeated cursor.")
|
|
255
296
|
self.cursor = cursor
|
|
@@ -281,7 +322,12 @@ def _set_socket_timeout(connection: http.client.HTTPConnection, deadline: float)
|
|
|
281
322
|
|
|
282
323
|
|
|
283
324
|
def _retryable(status: int, code: str) -> bool:
|
|
284
|
-
return
|
|
325
|
+
return code not in NON_RETRYABLE_CODES and (status == 429 or status >= 500)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _network_backoff(attempt: int, timeout: float) -> float:
|
|
329
|
+
"""Exponential backoff with jitter, never longer than one attempt's timeout."""
|
|
330
|
+
return min(0.25 * 2 ** attempt, 8.0, timeout) * random.uniform(0.5, 1.0)
|
|
285
331
|
|
|
286
332
|
|
|
287
333
|
def _retry_delay(retry_after: str | None, problem: dict[str, Any]) -> float | None:
|
|
@@ -300,3 +346,10 @@ def _retry_delay(retry_after: str | None, problem: dict[str, Any]) -> float | No
|
|
|
300
346
|
else:
|
|
301
347
|
seconds = 0.1
|
|
302
348
|
return seconds if math.isfinite(seconds) and seconds >= 0 else None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# Deprecated aliases kept for code written against the preview names.
|
|
352
|
+
ExactApiClient = DataXClient
|
|
353
|
+
ExactApiError = DataXError
|
|
354
|
+
ExactApiConnectionError = DataXConnectionError
|
|
355
|
+
ExactApiPaginationError = DataXPaginationError
|