thordata-sdk 0.8.0__tar.gz → 1.0.1__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.
- {thordata_sdk-0.8.0/src/thordata_sdk.egg-info → thordata_sdk-1.0.1}/PKG-INFO +2 -6
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/README.md +1 -5
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/pyproject.toml +1 -1
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/__init__.py +1 -1
- thordata_sdk-1.0.1/src/thordata/_example_utils.py +76 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/_utils.py +14 -31
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/async_client.py +153 -92
- thordata_sdk-1.0.1/src/thordata/client.py +980 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/models.py +57 -7
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/retry.py +2 -3
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1/src/thordata_sdk.egg-info}/PKG-INFO +2 -6
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata_sdk.egg-info/SOURCES.txt +1 -0
- thordata_sdk-1.0.1/tests/test_async_client.py +83 -0
- thordata_sdk-0.8.0/src/thordata/client.py +0 -1858
- thordata_sdk-0.8.0/tests/test_async_client.py +0 -73
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/LICENSE +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/setup.cfg +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/demo.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/enums.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata/exceptions.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata_sdk.egg-info/dependency_links.txt +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata_sdk.egg-info/requires.txt +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/src/thordata_sdk.egg-info/top_level.txt +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_async_client_errors.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_client.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_client_errors.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_enums.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_examples.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_exceptions.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_models.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_spec_parity.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_task_status_and_wait.py +0 -0
- {thordata_sdk-0.8.0 → thordata_sdk-1.0.1}/tests/test_user_agent.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thordata-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: The Official Python SDK for Thordata - AI Data Infrastructure & Proxy Network.
|
|
5
5
|
Author-email: Thordata Developer Team <support@thordata.com>
|
|
6
6
|
License: MIT
|
|
@@ -87,14 +87,10 @@ Set environment variables:
|
|
|
87
87
|
# Required for Scraper APIs (SERP, Universal, Tasks)
|
|
88
88
|
export THORDATA_SCRAPER_TOKEN=your_token
|
|
89
89
|
|
|
90
|
-
#
|
|
90
|
+
# Public/Location APIs (Dashboard -> My account -> API)
|
|
91
91
|
export THORDATA_PUBLIC_TOKEN=your_public_token
|
|
92
92
|
export THORDATA_PUBLIC_KEY=your_public_key
|
|
93
93
|
|
|
94
|
-
# Required for Public API NEW (Dashboard -> Public API NEW)
|
|
95
|
-
# If not set, SDK falls back to PUBLIC_TOKEN/KEY
|
|
96
|
-
export THORDATA_SIGN=your_sign
|
|
97
|
-
export THORDATA_API_KEY=your_api_key
|
|
98
94
|
```
|
|
99
95
|
|
|
100
96
|
---
|
|
@@ -44,14 +44,10 @@ Set environment variables:
|
|
|
44
44
|
# Required for Scraper APIs (SERP, Universal, Tasks)
|
|
45
45
|
export THORDATA_SCRAPER_TOKEN=your_token
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# Public/Location APIs (Dashboard -> My account -> API)
|
|
48
48
|
export THORDATA_PUBLIC_TOKEN=your_public_token
|
|
49
49
|
export THORDATA_PUBLIC_KEY=your_public_key
|
|
50
50
|
|
|
51
|
-
# Required for Public API NEW (Dashboard -> Public API NEW)
|
|
52
|
-
# If not set, SDK falls back to PUBLIC_TOKEN/KEY
|
|
53
|
-
export THORDATA_SIGN=your_sign
|
|
54
|
-
export THORDATA_API_KEY=your_api_key
|
|
55
51
|
```
|
|
56
52
|
|
|
57
53
|
---
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Iterable, Optional
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
load_dotenv = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_env() -> None:
|
|
15
|
+
"""Load .env from repo root if python-dotenv is installed."""
|
|
16
|
+
if load_dotenv is None:
|
|
17
|
+
return
|
|
18
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
19
|
+
load_dotenv(dotenv_path=repo_root / ".env")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def env(name: str) -> str:
|
|
23
|
+
return (os.getenv(name) or "").strip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def skip_if_missing(required: Iterable[str], *, tip: Optional[str] = None) -> bool:
|
|
27
|
+
missing = [k for k in required if not env(k)]
|
|
28
|
+
if not missing:
|
|
29
|
+
return False
|
|
30
|
+
print("Skipping live example: missing env:", ", ".join(missing))
|
|
31
|
+
if tip:
|
|
32
|
+
print(tip)
|
|
33
|
+
else:
|
|
34
|
+
print("Tip: copy .env.example to .env and fill values, then re-run.")
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_json_env(name: str, default: str = "{}") -> Any:
|
|
39
|
+
raw = env(name) or default
|
|
40
|
+
return json.loads(raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_task_parameters(raw: Any) -> dict[str, Any]:
|
|
44
|
+
"""Accept {..} or [{..}] and return a single dict for create_scraper_task(parameters=...)."""
|
|
45
|
+
if isinstance(raw, list):
|
|
46
|
+
if not raw:
|
|
47
|
+
raise ValueError("Task parameters JSON array must not be empty")
|
|
48
|
+
raw = raw[0]
|
|
49
|
+
if not isinstance(raw, dict):
|
|
50
|
+
raise ValueError("Task parameters must be a JSON object (or array of objects)")
|
|
51
|
+
return raw
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def output_dir() -> Path:
|
|
55
|
+
"""Return output dir for examples; defaults to examples/output (ignored by git)."""
|
|
56
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
57
|
+
d = env("THORDATA_OUTPUT_DIR") or str(repo_root / "examples" / "output")
|
|
58
|
+
p = Path(d)
|
|
59
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
return p
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_text(filename: str, content: str) -> Path:
|
|
64
|
+
p = output_dir() / filename
|
|
65
|
+
p.write_text(content, encoding="utf-8", errors="replace")
|
|
66
|
+
return p
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def write_json(filename: str, data: Any) -> Path:
|
|
70
|
+
p = output_dir() / filename
|
|
71
|
+
p.write_text(
|
|
72
|
+
json.dumps(data, ensure_ascii=False, indent=2),
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
errors="replace",
|
|
75
|
+
)
|
|
76
|
+
return p
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import base64
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
+
import platform
|
|
12
13
|
from typing import Any, Dict
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -171,37 +172,19 @@ def extract_error_message(payload: Any) -> str:
|
|
|
171
172
|
|
|
172
173
|
def build_user_agent(sdk_version: str, http_client: str) -> str:
|
|
173
174
|
"""
|
|
174
|
-
Build a
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
sdk_version: SDK version string.
|
|
178
|
-
http_client: "requests" or "aiohttp" (or any identifier).
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
A User-Agent string.
|
|
182
|
-
"""
|
|
183
|
-
import platform
|
|
184
|
-
|
|
185
|
-
py = platform.python_version()
|
|
186
|
-
system = platform.system()
|
|
187
|
-
return f"thordata-python-sdk/{sdk_version} (python {py}; {system}; {http_client})"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def build_sign_headers(sign: str, api_key: str) -> Dict[str, str]:
|
|
175
|
+
Build a standardized User-Agent for the SDK.
|
|
176
|
+
Format: thordata-python-sdk/{version} python/{py_ver} ({system}/{release}; {machine})
|
|
191
177
|
"""
|
|
192
|
-
|
|
178
|
+
py_ver = platform.python_version()
|
|
179
|
+
system = platform.system() or "unknown"
|
|
180
|
+
release = platform.release() or "unknown"
|
|
181
|
+
machine = platform.machine() or "unknown"
|
|
193
182
|
|
|
194
|
-
|
|
183
|
+
# Clean up strings to avoid UA parsing issues (remove newlines, etc)
|
|
184
|
+
system = system.replace(";", "").strip()
|
|
195
185
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
Headers dict with sign, apiKey, and Content-Type.
|
|
202
|
-
"""
|
|
203
|
-
return {
|
|
204
|
-
"sign": sign,
|
|
205
|
-
"apiKey": api_key,
|
|
206
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
207
|
-
}
|
|
186
|
+
return (
|
|
187
|
+
f"thordata-python-sdk/{sdk_version} "
|
|
188
|
+
f"python/{py_ver} "
|
|
189
|
+
f"({system}/{release}; {machine}; {http_client})"
|
|
190
|
+
)
|
|
@@ -25,7 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
import asyncio
|
|
26
26
|
import logging
|
|
27
27
|
import os
|
|
28
|
-
from datetime import date
|
|
28
|
+
from datetime import date
|
|
29
29
|
from typing import Any, Dict, List, Optional, Union
|
|
30
30
|
|
|
31
31
|
import aiohttp
|
|
@@ -35,7 +35,6 @@ from ._utils import (
|
|
|
35
35
|
build_auth_headers,
|
|
36
36
|
build_builder_headers,
|
|
37
37
|
build_public_api_headers,
|
|
38
|
-
build_sign_headers,
|
|
39
38
|
build_user_agent,
|
|
40
39
|
decode_base64_image,
|
|
41
40
|
extract_error_message,
|
|
@@ -51,14 +50,15 @@ from .exceptions import (
|
|
|
51
50
|
from .models import (
|
|
52
51
|
CommonSettings,
|
|
53
52
|
ProxyConfig,
|
|
53
|
+
ProxyProduct,
|
|
54
54
|
ProxyServer,
|
|
55
|
-
ProxyUser,
|
|
56
55
|
ProxyUserList,
|
|
57
56
|
ScraperTaskConfig,
|
|
58
57
|
SerpRequest,
|
|
59
58
|
UniversalScrapeRequest,
|
|
60
59
|
UsageStatistics,
|
|
61
60
|
VideoTaskConfig,
|
|
61
|
+
WhitelistProxyConfig,
|
|
62
62
|
)
|
|
63
63
|
from .retry import RetryConfig
|
|
64
64
|
|
|
@@ -100,8 +100,6 @@ class AsyncThordataClient:
|
|
|
100
100
|
scraper_token: str,
|
|
101
101
|
public_token: Optional[str] = None,
|
|
102
102
|
public_key: Optional[str] = None,
|
|
103
|
-
sign: Optional[str] = None,
|
|
104
|
-
api_key: Optional[str] = None,
|
|
105
103
|
proxy_host: str = "pr.thordata.net",
|
|
106
104
|
proxy_port: int = 9999,
|
|
107
105
|
timeout: int = 30,
|
|
@@ -121,18 +119,9 @@ class AsyncThordataClient:
|
|
|
121
119
|
self.public_token = public_token
|
|
122
120
|
self.public_key = public_key
|
|
123
121
|
|
|
124
|
-
# Automatic Fallback Logic: If sign/api_key is not provided, try using public_token/key
|
|
125
|
-
self.sign = sign or os.getenv("THORDATA_SIGN") or self.public_token
|
|
126
|
-
self.api_key = api_key or os.getenv("THORDATA_API_KEY") or self.public_key
|
|
127
|
-
|
|
128
|
-
# Public API authentication
|
|
129
|
-
self.sign = sign or os.getenv("THORDATA_SIGN")
|
|
130
|
-
self.api_key = api_key or os.getenv("THORDATA_API_KEY")
|
|
131
|
-
|
|
132
122
|
# Proxy configuration
|
|
133
123
|
self._proxy_host = proxy_host
|
|
134
124
|
self._proxy_port = proxy_port
|
|
135
|
-
self._default_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
136
125
|
|
|
137
126
|
# Timeout configuration
|
|
138
127
|
self._default_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
@@ -141,19 +130,13 @@ class AsyncThordataClient:
|
|
|
141
130
|
# Retry configuration
|
|
142
131
|
self._retry_config = retry_config or RetryConfig()
|
|
143
132
|
|
|
144
|
-
# Authentication mode
|
|
133
|
+
# Authentication mode (for scraping APIs)
|
|
145
134
|
self._auth_mode = auth_mode.lower()
|
|
146
135
|
if self._auth_mode not in ("bearer", "header_token"):
|
|
147
136
|
raise ThordataConfigError(
|
|
148
137
|
f"Invalid auth_mode: {auth_mode}. Must be 'bearer' or 'header_token'."
|
|
149
138
|
)
|
|
150
139
|
|
|
151
|
-
# Pre-calculate proxy auth
|
|
152
|
-
self._proxy_url = f"http://{proxy_host}:{proxy_port}"
|
|
153
|
-
self._proxy_auth = aiohttp.BasicAuth(
|
|
154
|
-
login=f"td-customer-{scraper_token}", password=""
|
|
155
|
-
)
|
|
156
|
-
|
|
157
140
|
# Base URLs (allow override via args or env vars for testing and custom routing)
|
|
158
141
|
scraperapi_base = (
|
|
159
142
|
scraperapi_base_url
|
|
@@ -179,6 +162,7 @@ class AsyncThordataClient:
|
|
|
179
162
|
or self.LOCATIONS_URL
|
|
180
163
|
).rstrip("/")
|
|
181
164
|
|
|
165
|
+
# Keep these env overrides for now
|
|
182
166
|
gateway_base = os.getenv(
|
|
183
167
|
"THORDATA_GATEWAY_BASE_URL", "https://api.thordata.com/api/gateway"
|
|
184
168
|
)
|
|
@@ -193,8 +177,11 @@ class AsyncThordataClient:
|
|
|
193
177
|
self._builder_url = f"{scraperapi_base}/builder"
|
|
194
178
|
self._video_builder_url = f"{scraperapi_base}/video_builder"
|
|
195
179
|
self._universal_url = f"{universalapi_base}/request"
|
|
180
|
+
|
|
196
181
|
self._status_url = f"{web_scraper_api_base}/tasks-status"
|
|
197
182
|
self._download_url = f"{web_scraper_api_base}/tasks-download"
|
|
183
|
+
self._list_url = f"{web_scraper_api_base}/tasks-list"
|
|
184
|
+
|
|
198
185
|
self._locations_base_url = locations_base
|
|
199
186
|
self._usage_stats_url = (
|
|
200
187
|
f"{locations_base.replace('/locations', '')}/account/usage-statistics"
|
|
@@ -202,16 +189,17 @@ class AsyncThordataClient:
|
|
|
202
189
|
self._proxy_users_url = (
|
|
203
190
|
f"{locations_base.replace('/locations', '')}/proxy-users"
|
|
204
191
|
)
|
|
192
|
+
|
|
205
193
|
whitelist_base = os.getenv(
|
|
206
194
|
"THORDATA_WHITELIST_BASE_URL", "https://api.thordata.com/api"
|
|
207
195
|
)
|
|
208
196
|
self._whitelist_url = f"{whitelist_base}/whitelisted-ips"
|
|
197
|
+
|
|
209
198
|
proxy_api_base = os.getenv(
|
|
210
199
|
"THORDATA_PROXY_API_BASE_URL", "https://api.thordata.com/api"
|
|
211
200
|
)
|
|
212
201
|
self._proxy_list_url = f"{proxy_api_base}/proxy/proxy-list"
|
|
213
202
|
self._proxy_expiration_url = f"{proxy_api_base}/proxy/expiration-time"
|
|
214
|
-
self._list_url = f"{web_scraper_api_base}/tasks-list"
|
|
215
203
|
|
|
216
204
|
# Session initialized lazily
|
|
217
205
|
self._session: Optional[aiohttp.ClientSession] = None
|
|
@@ -271,11 +259,26 @@ class AsyncThordataClient:
|
|
|
271
259
|
|
|
272
260
|
logger.debug(f"Async Proxy GET: {url}")
|
|
273
261
|
|
|
274
|
-
if proxy_config:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
262
|
+
if proxy_config is None:
|
|
263
|
+
proxy_config = self._get_default_proxy_config_from_env()
|
|
264
|
+
|
|
265
|
+
if proxy_config is None:
|
|
266
|
+
raise ThordataConfigError(
|
|
267
|
+
"Proxy credentials are missing. "
|
|
268
|
+
"Pass proxy_config=ProxyConfig(username=..., password=..., product=...) "
|
|
269
|
+
"or set THORDATA_RESIDENTIAL_USERNAME/THORDATA_RESIDENTIAL_PASSWORD (or DATACENTER/MOBILE)."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# aiohttp has limited support for "https://" proxies (TLS to proxy / TLS-in-TLS).
|
|
273
|
+
# Your account's proxy endpoint requires HTTPS proxy, so we explicitly block here
|
|
274
|
+
# to avoid confusing "it always fails" behavior.
|
|
275
|
+
if getattr(proxy_config, "protocol", "http").lower() == "https":
|
|
276
|
+
raise ThordataConfigError(
|
|
277
|
+
"Proxy Network requires an HTTPS proxy endpoint (TLS to proxy) for your account. "
|
|
278
|
+
"aiohttp support for 'https://' proxies is limited and may fail. "
|
|
279
|
+
"Please use ThordataClient.get/post (sync client) for Proxy Network requests."
|
|
280
|
+
)
|
|
281
|
+
proxy_url, proxy_auth = proxy_config.to_aiohttp_config()
|
|
279
282
|
|
|
280
283
|
try:
|
|
281
284
|
return await session.get(
|
|
@@ -312,11 +315,26 @@ class AsyncThordataClient:
|
|
|
312
315
|
|
|
313
316
|
logger.debug(f"Async Proxy POST: {url}")
|
|
314
317
|
|
|
315
|
-
if proxy_config:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
if proxy_config is None:
|
|
319
|
+
proxy_config = self._get_default_proxy_config_from_env()
|
|
320
|
+
|
|
321
|
+
if proxy_config is None:
|
|
322
|
+
raise ThordataConfigError(
|
|
323
|
+
"Proxy credentials are missing. "
|
|
324
|
+
"Pass proxy_config=ProxyConfig(username=..., password=..., product=...) "
|
|
325
|
+
"or set THORDATA_RESIDENTIAL_USERNAME/THORDATA_RESIDENTIAL_PASSWORD (or DATACENTER/MOBILE)."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# aiohttp has limited support for "https://" proxies (TLS to proxy / TLS-in-TLS).
|
|
329
|
+
# Your account's proxy endpoint requires HTTPS proxy, so we explicitly block here
|
|
330
|
+
# to avoid confusing "it always fails" behavior.
|
|
331
|
+
if getattr(proxy_config, "protocol", "http").lower() == "https":
|
|
332
|
+
raise ThordataConfigError(
|
|
333
|
+
"Proxy Network requires an HTTPS proxy endpoint (TLS to proxy) for your account. "
|
|
334
|
+
"aiohttp support for 'https://' proxies is limited and may fail. "
|
|
335
|
+
"Please use ThordataClient.get/post (sync client) for Proxy Network requests."
|
|
336
|
+
)
|
|
337
|
+
proxy_url, proxy_auth = proxy_config.to_aiohttp_config()
|
|
320
338
|
|
|
321
339
|
try:
|
|
322
340
|
return await session.post(
|
|
@@ -973,23 +991,14 @@ class AsyncThordataClient:
|
|
|
973
991
|
|
|
974
992
|
async def get_residential_balance(self) -> Dict[str, Any]:
|
|
975
993
|
"""
|
|
976
|
-
Get residential proxy balance
|
|
977
|
-
|
|
978
|
-
Requires sign and apiKey credentials.
|
|
994
|
+
Get residential proxy balance.
|
|
979
995
|
|
|
980
|
-
|
|
981
|
-
Dict with 'balance' (bytes) and 'expire_time' (timestamp).
|
|
996
|
+
Uses public_token/public_key.
|
|
982
997
|
"""
|
|
983
|
-
if not self.sign or not self.api_key:
|
|
984
|
-
raise ThordataConfigError(
|
|
985
|
-
"sign and api_key are required for Public API NEW. "
|
|
986
|
-
"Set THORDATA_SIGN and THORDATA_API_KEY environment variables."
|
|
987
|
-
)
|
|
988
|
-
|
|
989
998
|
session = self._get_session()
|
|
990
|
-
headers =
|
|
999
|
+
headers = self._build_gateway_headers()
|
|
991
1000
|
|
|
992
|
-
logger.info("Async getting residential proxy balance
|
|
1001
|
+
logger.info("Async getting residential proxy balance")
|
|
993
1002
|
|
|
994
1003
|
try:
|
|
995
1004
|
async with session.post(
|
|
@@ -1025,26 +1034,13 @@ class AsyncThordataClient:
|
|
|
1025
1034
|
end_time: Union[str, int],
|
|
1026
1035
|
) -> Dict[str, Any]:
|
|
1027
1036
|
"""
|
|
1028
|
-
Get residential proxy usage records
|
|
1029
|
-
|
|
1030
|
-
Args:
|
|
1031
|
-
start_time: Start timestamp (Unix timestamp or YYYY-MM-DD HH:MM:SS).
|
|
1032
|
-
end_time: End timestamp (Unix timestamp or YYYY-MM-DD HH:MM:SS).
|
|
1037
|
+
Get residential proxy usage records.
|
|
1033
1038
|
|
|
1034
|
-
|
|
1035
|
-
Dict with usage data including 'all_flow', 'all_used_flow', 'data' list.
|
|
1039
|
+
Uses public_token/public_key.
|
|
1036
1040
|
"""
|
|
1037
|
-
if not self.sign or not self.api_key:
|
|
1038
|
-
raise ThordataConfigError(
|
|
1039
|
-
"sign and api_key are required for Public API NEW."
|
|
1040
|
-
)
|
|
1041
|
-
|
|
1042
1041
|
session = self._get_session()
|
|
1043
|
-
headers =
|
|
1044
|
-
payload = {
|
|
1045
|
-
"start_time": str(start_time),
|
|
1046
|
-
"end_time": str(end_time),
|
|
1047
|
-
}
|
|
1042
|
+
headers = self._build_gateway_headers()
|
|
1043
|
+
payload = {"start_time": str(start_time), "end_time": str(end_time)}
|
|
1048
1044
|
|
|
1049
1045
|
logger.info(f"Async getting residential usage: {start_time} to {end_time}")
|
|
1050
1046
|
|
|
@@ -1296,20 +1292,14 @@ class AsyncThordataClient:
|
|
|
1296
1292
|
|
|
1297
1293
|
async def get_isp_regions(self) -> List[Dict[str, Any]]:
|
|
1298
1294
|
"""
|
|
1299
|
-
Get available ISP proxy regions
|
|
1295
|
+
Get available ISP proxy regions.
|
|
1300
1296
|
|
|
1301
|
-
|
|
1302
|
-
List of regions with id, continent, country, city, num, pricing.
|
|
1297
|
+
Uses public_token/public_key.
|
|
1303
1298
|
"""
|
|
1304
|
-
if not self.sign or not self.api_key:
|
|
1305
|
-
raise ThordataConfigError(
|
|
1306
|
-
"sign and api_key are required for Public API NEW."
|
|
1307
|
-
)
|
|
1308
|
-
|
|
1309
1299
|
session = self._get_session()
|
|
1310
|
-
headers =
|
|
1300
|
+
headers = self._build_gateway_headers()
|
|
1311
1301
|
|
|
1312
|
-
logger.info("Async getting ISP regions
|
|
1302
|
+
logger.info("Async getting ISP regions")
|
|
1313
1303
|
|
|
1314
1304
|
try:
|
|
1315
1305
|
async with session.post(
|
|
@@ -1341,20 +1331,14 @@ class AsyncThordataClient:
|
|
|
1341
1331
|
|
|
1342
1332
|
async def list_isp_proxies(self) -> List[Dict[str, Any]]:
|
|
1343
1333
|
"""
|
|
1344
|
-
List ISP proxies
|
|
1334
|
+
List ISP proxies.
|
|
1345
1335
|
|
|
1346
|
-
|
|
1347
|
-
List of ISP proxies with ip, port, user, pwd, startTime, expireTime.
|
|
1336
|
+
Uses public_token/public_key.
|
|
1348
1337
|
"""
|
|
1349
|
-
if not self.sign or not self.api_key:
|
|
1350
|
-
raise ThordataConfigError(
|
|
1351
|
-
"sign and api_key are required for Public API NEW."
|
|
1352
|
-
)
|
|
1353
|
-
|
|
1354
1338
|
session = self._get_session()
|
|
1355
|
-
headers =
|
|
1339
|
+
headers = self._build_gateway_headers()
|
|
1356
1340
|
|
|
1357
|
-
logger.info("Async listing ISP proxies
|
|
1341
|
+
logger.info("Async listing ISP proxies")
|
|
1358
1342
|
|
|
1359
1343
|
try:
|
|
1360
1344
|
async with session.post(
|
|
@@ -1386,20 +1370,14 @@ class AsyncThordataClient:
|
|
|
1386
1370
|
|
|
1387
1371
|
async def get_wallet_balance(self) -> Dict[str, Any]:
|
|
1388
1372
|
"""
|
|
1389
|
-
Get wallet balance for ISP proxies
|
|
1373
|
+
Get wallet balance for ISP proxies.
|
|
1390
1374
|
|
|
1391
|
-
|
|
1392
|
-
Dict with 'walletBalance'.
|
|
1375
|
+
Uses public_token/public_key.
|
|
1393
1376
|
"""
|
|
1394
|
-
if not self.sign or not self.api_key:
|
|
1395
|
-
raise ThordataConfigError(
|
|
1396
|
-
"sign and api_key are required for Public API NEW."
|
|
1397
|
-
)
|
|
1398
|
-
|
|
1399
1377
|
session = self._get_session()
|
|
1400
|
-
headers =
|
|
1378
|
+
headers = self._build_gateway_headers()
|
|
1401
1379
|
|
|
1402
|
-
logger.info("Async getting wallet balance
|
|
1380
|
+
logger.info("Async getting wallet balance")
|
|
1403
1381
|
|
|
1404
1382
|
try:
|
|
1405
1383
|
async with session.post(
|
|
@@ -1592,3 +1570,86 @@ class AsyncThordataClient:
|
|
|
1592
1570
|
"public_token and public_key are required for this operation. "
|
|
1593
1571
|
"Please provide them when initializing AsyncThordataClient."
|
|
1594
1572
|
)
|
|
1573
|
+
|
|
1574
|
+
def _get_proxy_endpoint_overrides(
|
|
1575
|
+
self, product: ProxyProduct
|
|
1576
|
+
) -> tuple[Optional[str], Optional[int], str]:
|
|
1577
|
+
prefix = product.value.upper()
|
|
1578
|
+
|
|
1579
|
+
host = os.getenv(f"THORDATA_{prefix}_PROXY_HOST") or os.getenv(
|
|
1580
|
+
"THORDATA_PROXY_HOST"
|
|
1581
|
+
)
|
|
1582
|
+
port_raw = os.getenv(f"THORDATA_{prefix}_PROXY_PORT") or os.getenv(
|
|
1583
|
+
"THORDATA_PROXY_PORT"
|
|
1584
|
+
)
|
|
1585
|
+
protocol = (
|
|
1586
|
+
os.getenv(f"THORDATA_{prefix}_PROXY_PROTOCOL")
|
|
1587
|
+
or os.getenv("THORDATA_PROXY_PROTOCOL")
|
|
1588
|
+
or "http"
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1591
|
+
port: Optional[int] = None
|
|
1592
|
+
if port_raw:
|
|
1593
|
+
try:
|
|
1594
|
+
port = int(port_raw)
|
|
1595
|
+
except ValueError:
|
|
1596
|
+
port = None
|
|
1597
|
+
|
|
1598
|
+
return host or None, port, protocol
|
|
1599
|
+
|
|
1600
|
+
def _get_default_proxy_config_from_env(self) -> Optional[ProxyConfig]:
|
|
1601
|
+
u = os.getenv("THORDATA_RESIDENTIAL_USERNAME")
|
|
1602
|
+
p = os.getenv("THORDATA_RESIDENTIAL_PASSWORD")
|
|
1603
|
+
if u and p:
|
|
1604
|
+
host, port, protocol = self._get_proxy_endpoint_overrides(
|
|
1605
|
+
ProxyProduct.RESIDENTIAL
|
|
1606
|
+
)
|
|
1607
|
+
return ProxyConfig(
|
|
1608
|
+
username=u,
|
|
1609
|
+
password=p,
|
|
1610
|
+
product=ProxyProduct.RESIDENTIAL,
|
|
1611
|
+
host=host,
|
|
1612
|
+
port=port,
|
|
1613
|
+
protocol=protocol,
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
u = os.getenv("THORDATA_DATACENTER_USERNAME")
|
|
1617
|
+
p = os.getenv("THORDATA_DATACENTER_PASSWORD")
|
|
1618
|
+
if u and p:
|
|
1619
|
+
host, port, protocol = self._get_proxy_endpoint_overrides(
|
|
1620
|
+
ProxyProduct.DATACENTER
|
|
1621
|
+
)
|
|
1622
|
+
return ProxyConfig(
|
|
1623
|
+
username=u,
|
|
1624
|
+
password=p,
|
|
1625
|
+
product=ProxyProduct.DATACENTER,
|
|
1626
|
+
host=host,
|
|
1627
|
+
port=port,
|
|
1628
|
+
protocol=protocol,
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1631
|
+
u = os.getenv("THORDATA_MOBILE_USERNAME")
|
|
1632
|
+
p = os.getenv("THORDATA_MOBILE_PASSWORD")
|
|
1633
|
+
if u and p:
|
|
1634
|
+
host, port, protocol = self._get_proxy_endpoint_overrides(
|
|
1635
|
+
ProxyProduct.MOBILE
|
|
1636
|
+
)
|
|
1637
|
+
return ProxyConfig(
|
|
1638
|
+
username=u,
|
|
1639
|
+
password=p,
|
|
1640
|
+
product=ProxyProduct.MOBILE,
|
|
1641
|
+
host=host,
|
|
1642
|
+
port=port,
|
|
1643
|
+
protocol=protocol,
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
return None
|
|
1647
|
+
|
|
1648
|
+
def _build_gateway_headers(self) -> Dict[str, str]:
|
|
1649
|
+
"""
|
|
1650
|
+
Headers for gateway-style endpoints.
|
|
1651
|
+
|
|
1652
|
+
Per our SDK rule: ONLY public_token/public_key exist.
|
|
1653
|
+
"""
|
|
1654
|
+
self._require_public_credentials()
|
|
1655
|
+
return build_public_api_headers(self.public_token or "", self.public_key or "")
|