xtree-sdk 0.1.0__py3-none-any.whl
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.
- xtree/__init__.py +7 -0
- xtree/_http.py +111 -0
- xtree/account.py +19 -0
- xtree/bilibili/__init__.py +40 -0
- xtree/cli.py +138 -0
- xtree/client.py +89 -0
- xtree/douyin/__init__.py +40 -0
- xtree/exceptions.py +89 -0
- xtree/facebook/__init__.py +19 -0
- xtree/jd/__init__.py +22 -0
- xtree/kuaishou/__init__.py +37 -0
- xtree/pdd/__init__.py +22 -0
- xtree/registry.py +264 -0
- xtree/taobao/__init__.py +22 -0
- xtree/tiktok/__init__.py +31 -0
- xtree/types.py +69 -0
- xtree/wechat/__init__.py +18 -0
- xtree/wechat/_helpers.py +40 -0
- xtree/wechat/accounts.py +80 -0
- xtree/wechat/articles.py +63 -0
- xtree/wechat/channels.py +108 -0
- xtree/wechat/library.py +58 -0
- xtree/wechat/links.py +17 -0
- xtree/wechat/search.py +166 -0
- xtree/weibo/__init__.py +43 -0
- xtree/xhs/__init__.py +37 -0
- xtree/youtube/__init__.py +25 -0
- xtree_sdk-0.1.0.dist-info/METADATA +97 -0
- xtree_sdk-0.1.0.dist-info/RECORD +31 -0
- xtree_sdk-0.1.0.dist-info/WHEEL +4 -0
- xtree_sdk-0.1.0.dist-info/entry_points.txt +2 -0
xtree/__init__.py
ADDED
xtree/_http.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""HTTP transport with envelope parsing and retries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from xtree.exceptions import XTreeError, error_from_envelope
|
|
11
|
+
from xtree.types import APIResponse, ResponseMeta
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.xtree.io/api"
|
|
14
|
+
API_KEY_HEADER = "X-XTREE-Key"
|
|
15
|
+
VERIFY_CODE_HEADER = "X-Verify-Code"
|
|
16
|
+
DEFAULT_TIMEOUT = 60.0
|
|
17
|
+
RETRY_STATUS = {429, 502, 503}
|
|
18
|
+
MAX_RETRIES = 3
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HTTPClient:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
api_key: str,
|
|
26
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
27
|
+
verify_code: str | None = None,
|
|
28
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
29
|
+
client: httpx.Client | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.base_url = base_url.rstrip("/")
|
|
32
|
+
self.verify_code = verify_code
|
|
33
|
+
self._owns_client = client is None
|
|
34
|
+
self._client = client or httpx.Client(timeout=timeout)
|
|
35
|
+
self._headers = {
|
|
36
|
+
API_KEY_HEADER: api_key,
|
|
37
|
+
"Accept": "application/json",
|
|
38
|
+
"User-Agent": "xtree-python/0.1.0",
|
|
39
|
+
}
|
|
40
|
+
if verify_code:
|
|
41
|
+
self._headers[VERIFY_CODE_HEADER] = verify_code
|
|
42
|
+
self.last_meta: ResponseMeta | None = None
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
if self._owns_client:
|
|
46
|
+
self._client.close()
|
|
47
|
+
|
|
48
|
+
def __enter__(self) -> HTTPClient:
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __exit__(self, *args: object) -> None:
|
|
52
|
+
self.close()
|
|
53
|
+
|
|
54
|
+
def request(
|
|
55
|
+
self,
|
|
56
|
+
method: str,
|
|
57
|
+
path: str,
|
|
58
|
+
*,
|
|
59
|
+
json: dict[str, Any] | None = None,
|
|
60
|
+
params: dict[str, Any] | None = None,
|
|
61
|
+
) -> APIResponse[Any]:
|
|
62
|
+
url = f"{self.base_url}{path}"
|
|
63
|
+
last_exc: Exception | None = None
|
|
64
|
+
|
|
65
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
66
|
+
try:
|
|
67
|
+
response = self._client.request(
|
|
68
|
+
method,
|
|
69
|
+
url,
|
|
70
|
+
headers=self._headers,
|
|
71
|
+
json=json,
|
|
72
|
+
params=params,
|
|
73
|
+
)
|
|
74
|
+
except httpx.HTTPError as exc:
|
|
75
|
+
last_exc = exc
|
|
76
|
+
if attempt >= MAX_RETRIES:
|
|
77
|
+
raise XTreeError(str(exc), code="TRANSPORT_ERROR") from exc
|
|
78
|
+
time.sleep(2**attempt * 0.5)
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
payload = _parse_json(response)
|
|
82
|
+
if response.status_code in RETRY_STATUS and attempt < MAX_RETRIES:
|
|
83
|
+
time.sleep(2**attempt * 0.5)
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
return _unwrap_envelope(payload, status_code=response.status_code)
|
|
87
|
+
|
|
88
|
+
if last_exc:
|
|
89
|
+
raise XTreeError(str(last_exc), code="TRANSPORT_ERROR") from last_exc
|
|
90
|
+
raise XTreeError("Request failed after retries", code="TRANSPORT_ERROR")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_json(response: httpx.Response) -> dict[str, Any]:
|
|
94
|
+
try:
|
|
95
|
+
data = response.json()
|
|
96
|
+
if isinstance(data, dict):
|
|
97
|
+
return data
|
|
98
|
+
except ValueError:
|
|
99
|
+
pass
|
|
100
|
+
raise XTreeError(
|
|
101
|
+
f"Non-JSON response (HTTP {response.status_code})",
|
|
102
|
+
code="INVALID_RESPONSE",
|
|
103
|
+
status_code=response.status_code,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _unwrap_envelope(payload: dict[str, Any], *, status_code: int) -> APIResponse[Any]:
|
|
108
|
+
meta = ResponseMeta.from_dict(payload.get("meta") or {})
|
|
109
|
+
if payload.get("ok") is True:
|
|
110
|
+
return APIResponse(data=payload.get("data"), meta=meta)
|
|
111
|
+
raise error_from_envelope(payload, status_code=status_code)
|
xtree/account.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Account endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AccountAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def balance(self) -> APIResponse[dict[str, Any]]:
|
|
16
|
+
"""GET /account/balance — free, returns balance_cny."""
|
|
17
|
+
resp = self._http.request("GET", "/account/balance")
|
|
18
|
+
self._http.last_meta = resp.meta
|
|
19
|
+
return resp
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""AUTO-GENERATED `bilibili` platform client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BilibiliAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def search_videos(self, **body: Any) -> APIResponse[Any]:
|
|
16
|
+
return self._http.request("POST", "/bilibili/search/videos", json=body)
|
|
17
|
+
|
|
18
|
+
def videos_detail(self, **body: Any) -> APIResponse[Any]:
|
|
19
|
+
return self._http.request("POST", "/bilibili/videos/detail", json=body)
|
|
20
|
+
|
|
21
|
+
def videos_comments(self, **body: Any) -> APIResponse[Any]:
|
|
22
|
+
return self._http.request("POST", "/bilibili/videos/comments", json=body)
|
|
23
|
+
|
|
24
|
+
def videos_download(self, **body: Any) -> APIResponse[Any]:
|
|
25
|
+
return self._http.request("POST", "/bilibili/videos/download", json=body)
|
|
26
|
+
|
|
27
|
+
def users_profile(self, **body: Any) -> APIResponse[Any]:
|
|
28
|
+
return self._http.request("POST", "/bilibili/users/profile", json=body)
|
|
29
|
+
|
|
30
|
+
def users_posts(self, **body: Any) -> APIResponse[Any]:
|
|
31
|
+
return self._http.request("POST", "/bilibili/users/posts", json=body)
|
|
32
|
+
|
|
33
|
+
def users_videos(self, **body: Any) -> APIResponse[Any]:
|
|
34
|
+
return self._http.request("POST", "/bilibili/users/videos", json=body)
|
|
35
|
+
|
|
36
|
+
def popular_list(self, **body: Any) -> APIResponse[Any]:
|
|
37
|
+
return self._http.request("POST", "/bilibili/popular/list", json=body)
|
|
38
|
+
|
|
39
|
+
def rank_list(self, **body: Any) -> APIResponse[Any]:
|
|
40
|
+
return self._http.request("POST", "/bilibili/rank/list", json=body)
|
xtree/cli.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""xtree CLI — balance, wechat helpers, and raw call."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from xtree.client import XTreeClient
|
|
12
|
+
from xtree.exceptions import XTreeError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(prog="xtree", description="XTREE API CLI")
|
|
17
|
+
parser.add_argument("--json", action="store_true", help="Print data as JSON")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--verbose",
|
|
20
|
+
action="store_true",
|
|
21
|
+
help="Print meta to stderr (request_id, cost_cny, balance_cny)",
|
|
22
|
+
)
|
|
23
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
24
|
+
|
|
25
|
+
sub.add_parser("balance", help="GET /account/balance")
|
|
26
|
+
|
|
27
|
+
call = sub.add_parser("call", help="Raw API call (any path)")
|
|
28
|
+
call.add_argument("method", choices=["GET", "POST"])
|
|
29
|
+
call.add_argument("path", help="API path, e.g. /wechat/search/articles")
|
|
30
|
+
call.add_argument("--body", default=None, help="JSON body for POST")
|
|
31
|
+
call.add_argument("--body-file", default=None, help="JSON file for POST body")
|
|
32
|
+
|
|
33
|
+
wechat = sub.add_parser("wechat", help="WeChat platform shortcuts")
|
|
34
|
+
wechat_sub = wechat.add_subparsers(dest="wechat_group", required=True)
|
|
35
|
+
|
|
36
|
+
search = wechat_sub.add_parser("search", help="搜一搜")
|
|
37
|
+
search_sub = search.add_subparsers(dest="search_action", required=True)
|
|
38
|
+
for name, help_text in [
|
|
39
|
+
("articles", "POST /wechat/search/articles"),
|
|
40
|
+
("videos", "POST /wechat/search/videos"),
|
|
41
|
+
("mixed", "POST /wechat/search/mixed"),
|
|
42
|
+
("accounts", "POST /wechat/search/accounts"),
|
|
43
|
+
("suggestions", "POST /wechat/search/suggestions"),
|
|
44
|
+
]:
|
|
45
|
+
p = search_sub.add_parser(name, help=help_text)
|
|
46
|
+
p.add_argument("--keyword", required=True)
|
|
47
|
+
p.add_argument("--page", type=int, default=1)
|
|
48
|
+
p.add_argument("--session", default=None)
|
|
49
|
+
|
|
50
|
+
library = wechat_sub.add_parser("library", help="库搜索 (Phase 2)")
|
|
51
|
+
library_sub = library.add_subparsers(dest="library_action", required=True)
|
|
52
|
+
lib_articles = library_sub.add_parser("articles", help="POST /wechat/library/articles/search")
|
|
53
|
+
lib_articles.add_argument("--keyword", required=True)
|
|
54
|
+
lib_articles.add_argument("--page", type=int, default=1)
|
|
55
|
+
|
|
56
|
+
args = parser.parse_args(argv)
|
|
57
|
+
resp = None
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with XTreeClient() as client:
|
|
61
|
+
resp = _dispatch(client, args, parser)
|
|
62
|
+
if args.verbose and client.last_meta:
|
|
63
|
+
_print_meta(client.last_meta)
|
|
64
|
+
except (XTreeError, ValueError, json.JSONDecodeError) as exc:
|
|
65
|
+
print(str(exc), file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
if args.json or args.command != "balance":
|
|
69
|
+
print(json.dumps(resp.data, ensure_ascii=False, indent=2))
|
|
70
|
+
else:
|
|
71
|
+
balance = (resp.data or {}).get("balance_cny")
|
|
72
|
+
print(f"balance_cny: {balance}")
|
|
73
|
+
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dispatch(client: XTreeClient, args: argparse.Namespace, parser: argparse.ArgumentParser) -> Any:
|
|
78
|
+
if args.command == "balance":
|
|
79
|
+
return client.account.balance()
|
|
80
|
+
|
|
81
|
+
if args.command == "call":
|
|
82
|
+
body = _load_body(args.body, args.body_file)
|
|
83
|
+
return client.request(args.method, args.path, json=body if args.method == "POST" else None)
|
|
84
|
+
|
|
85
|
+
if args.command == "wechat" and args.wechat_group == "search":
|
|
86
|
+
kwargs = {"keyword": args.keyword, "page": args.page, "session": args.session}
|
|
87
|
+
actions = {
|
|
88
|
+
"articles": client.wechat.search.articles,
|
|
89
|
+
"videos": client.wechat.search.videos,
|
|
90
|
+
"mixed": client.wechat.search.mixed,
|
|
91
|
+
"accounts": client.wechat.search.accounts,
|
|
92
|
+
"suggestions": client.wechat.search.suggestions,
|
|
93
|
+
}
|
|
94
|
+
fn = actions.get(args.search_action)
|
|
95
|
+
if fn is None:
|
|
96
|
+
parser.error("unknown wechat search action")
|
|
97
|
+
return fn(**kwargs)
|
|
98
|
+
|
|
99
|
+
if args.command == "wechat" and args.wechat_group == "library":
|
|
100
|
+
if args.library_action == "articles":
|
|
101
|
+
return client.wechat.library.search_articles(args.keyword, page=args.page)
|
|
102
|
+
|
|
103
|
+
parser.error("unknown command")
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_body(body: str | None, body_file: str | None) -> dict[str, Any]:
|
|
108
|
+
if body_file:
|
|
109
|
+
return json.loads(Path(body_file).read_text(encoding="utf-8"))
|
|
110
|
+
if body:
|
|
111
|
+
return json.loads(body)
|
|
112
|
+
return {}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _print_meta(meta: Any) -> None:
|
|
116
|
+
payload = {
|
|
117
|
+
"request_id": meta.request_id,
|
|
118
|
+
"cost_cny": meta.cost_cny,
|
|
119
|
+
"balance_cny": meta.balance_cny,
|
|
120
|
+
"rate_limit_remaining": meta.rate_limit_remaining,
|
|
121
|
+
}
|
|
122
|
+
for key in (
|
|
123
|
+
"observation_id",
|
|
124
|
+
"observed_at",
|
|
125
|
+
"basis",
|
|
126
|
+
"contract_rev",
|
|
127
|
+
"origin",
|
|
128
|
+
"cache_hit",
|
|
129
|
+
"cache_age_sec",
|
|
130
|
+
):
|
|
131
|
+
value = getattr(meta, key, None)
|
|
132
|
+
if value is not None:
|
|
133
|
+
payload[key] = value
|
|
134
|
+
print(json.dumps(payload, ensure_ascii=False), file=sys.stderr)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
raise SystemExit(main())
|
xtree/client.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""XTreeClient — entry point for the Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from xtree._http import DEFAULT_BASE_URL, HTTPClient
|
|
9
|
+
from xtree.account import AccountAPI
|
|
10
|
+
from xtree.types import APIResponse, ResponseMeta
|
|
11
|
+
from xtree.wechat import WechatAPI
|
|
12
|
+
from xtree.douyin import DouyinAPI
|
|
13
|
+
from xtree.xhs import XhsAPI
|
|
14
|
+
from xtree.kuaishou import KuaishouAPI
|
|
15
|
+
from xtree.weibo import WeiboAPI
|
|
16
|
+
from xtree.bilibili import BilibiliAPI
|
|
17
|
+
from xtree.tiktok import TiktokAPI
|
|
18
|
+
from xtree.youtube import YoutubeAPI
|
|
19
|
+
from xtree.facebook import FacebookAPI
|
|
20
|
+
from xtree.taobao import TaobaoAPI
|
|
21
|
+
from xtree.jd import JdAPI
|
|
22
|
+
from xtree.pdd import PddAPI
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class XTreeClient:
|
|
26
|
+
"""HTTP client for https://api.xtree.io/api."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
base_url: str | None = None,
|
|
33
|
+
verify_code: str | None = None,
|
|
34
|
+
timeout: float = 60.0,
|
|
35
|
+
http_client: Any | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
key = api_key or os.environ.get("XTREE_API_KEY")
|
|
38
|
+
if not key:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"api_key is required (pass api_key= or set XTREE_API_KEY)"
|
|
41
|
+
)
|
|
42
|
+
url = base_url or os.environ.get("XTREE_BASE_URL") or DEFAULT_BASE_URL
|
|
43
|
+
vcode = verify_code or os.environ.get("XTREE_VERIFY_CODE")
|
|
44
|
+
self._http = HTTPClient(
|
|
45
|
+
api_key=key,
|
|
46
|
+
base_url=url,
|
|
47
|
+
verify_code=vcode,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
client=http_client,
|
|
50
|
+
)
|
|
51
|
+
self.account = AccountAPI(self._http)
|
|
52
|
+
self.wechat = WechatAPI(self._http)
|
|
53
|
+
self.douyin = DouyinAPI(self._http)
|
|
54
|
+
self.xhs = XhsAPI(self._http)
|
|
55
|
+
self.kuaishou = KuaishouAPI(self._http)
|
|
56
|
+
self.weibo = WeiboAPI(self._http)
|
|
57
|
+
self.bilibili = BilibiliAPI(self._http)
|
|
58
|
+
self.tiktok = TiktokAPI(self._http)
|
|
59
|
+
self.youtube = YoutubeAPI(self._http)
|
|
60
|
+
self.facebook = FacebookAPI(self._http)
|
|
61
|
+
self.taobao = TaobaoAPI(self._http)
|
|
62
|
+
self.jd = JdAPI(self._http)
|
|
63
|
+
self.pdd = PddAPI(self._http)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def last_meta(self) -> ResponseMeta | None:
|
|
67
|
+
return self._http.last_meta
|
|
68
|
+
|
|
69
|
+
def request(
|
|
70
|
+
self,
|
|
71
|
+
method: str,
|
|
72
|
+
path: str,
|
|
73
|
+
*,
|
|
74
|
+
json: dict[str, Any] | None = None,
|
|
75
|
+
params: dict[str, Any] | None = None,
|
|
76
|
+
) -> APIResponse[Any]:
|
|
77
|
+
"""Low-level API call with envelope parsing."""
|
|
78
|
+
resp = self._http.request(method, path, json=json, params=params)
|
|
79
|
+
self._http.last_meta = resp.meta
|
|
80
|
+
return resp
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
self._http.close()
|
|
84
|
+
|
|
85
|
+
def __enter__(self) -> XTreeClient:
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def __exit__(self, *args: object) -> None:
|
|
89
|
+
self.close()
|
xtree/douyin/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""AUTO-GENERATED `douyin` platform client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DouyinAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def videos_detail(self, **body: Any) -> APIResponse[Any]:
|
|
16
|
+
return self._http.request("POST", "/douyin/videos/detail", json=body)
|
|
17
|
+
|
|
18
|
+
def users_profile(self, **body: Any) -> APIResponse[Any]:
|
|
19
|
+
return self._http.request("POST", "/douyin/users/profile", json=body)
|
|
20
|
+
|
|
21
|
+
def users_posts(self, **body: Any) -> APIResponse[Any]:
|
|
22
|
+
return self._http.request("POST", "/douyin/users/posts", json=body)
|
|
23
|
+
|
|
24
|
+
def videos_comments(self, **body: Any) -> APIResponse[Any]:
|
|
25
|
+
return self._http.request("POST", "/douyin/videos/comments", json=body)
|
|
26
|
+
|
|
27
|
+
def videos_sub_comments(self, **body: Any) -> APIResponse[Any]:
|
|
28
|
+
return self._http.request("POST", "/douyin/videos/sub-comments", json=body)
|
|
29
|
+
|
|
30
|
+
def search_videos(self, **body: Any) -> APIResponse[Any]:
|
|
31
|
+
return self._http.request("POST", "/douyin/search/videos", json=body)
|
|
32
|
+
|
|
33
|
+
def search_suggestions(self, **body: Any) -> APIResponse[Any]:
|
|
34
|
+
return self._http.request("POST", "/douyin/search/suggestions", json=body)
|
|
35
|
+
|
|
36
|
+
def users_creator_metrics(self, **body: Any) -> APIResponse[Any]:
|
|
37
|
+
return self._http.request("POST", "/douyin/users/creator-metrics", json=body)
|
|
38
|
+
|
|
39
|
+
def ids_resolve(self, **body: Any) -> APIResponse[Any]:
|
|
40
|
+
return self._http.request("POST", "/douyin/ids/resolve", json=body)
|
xtree/exceptions.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""XTREE API errors mapped from envelope.error.code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class XTreeError(Exception):
|
|
9
|
+
"""Base error for failed API calls."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
code: str = "UNKNOWN",
|
|
16
|
+
status_code: int | None = None,
|
|
17
|
+
request_id: str | None = None,
|
|
18
|
+
details: Any = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.code = code
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.request_id = request_id
|
|
24
|
+
self.details = details
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
parts = [self.code, str(self.args[0])]
|
|
28
|
+
if self.request_id:
|
|
29
|
+
parts.append(f"request_id={self.request_id}")
|
|
30
|
+
return ": ".join(parts)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class XTreeAuthError(XTreeError):
|
|
34
|
+
"""401 UNAUTHORIZED or 403 FORBIDDEN."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class XTreeValidationError(XTreeError):
|
|
38
|
+
"""400 VALIDATION_ERROR."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class XTreeInsufficientBalanceError(XTreeError):
|
|
42
|
+
"""402 INSUFFICIENT_BALANCE."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class XTreeRateLimitError(XTreeError):
|
|
46
|
+
"""429 RATE_LIMITED."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class XTreeServiceError(XTreeError):
|
|
50
|
+
"""502 INTERNAL_ERROR(这次没有完成交付)或 503 EDGE_UNAVAILABLE。"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# 旧名保留,便于已有 except 继续接住 502。
|
|
54
|
+
XTreeUpstreamError = XTreeServiceError
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_ERROR_MAP: dict[str, type[XTreeError]] = {
|
|
58
|
+
"UNAUTHORIZED": XTreeAuthError,
|
|
59
|
+
"FORBIDDEN": XTreeAuthError,
|
|
60
|
+
"VALIDATION_ERROR": XTreeValidationError,
|
|
61
|
+
"INSUFFICIENT_BALANCE": XTreeInsufficientBalanceError,
|
|
62
|
+
"RATE_LIMITED": XTreeRateLimitError,
|
|
63
|
+
"UPSTREAM_ERROR": XTreeServiceError,
|
|
64
|
+
"EDGE_UNAVAILABLE": XTreeServiceError,
|
|
65
|
+
"NOT_FOUND": XTreeError,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def error_from_envelope(
|
|
70
|
+
payload: dict[str, Any],
|
|
71
|
+
*,
|
|
72
|
+
status_code: int | None = None,
|
|
73
|
+
) -> XTreeError:
|
|
74
|
+
err = payload.get("error") or {}
|
|
75
|
+
code = str(err.get("code") or "UNKNOWN")
|
|
76
|
+
message = str(err.get("message") or "API request failed")
|
|
77
|
+
meta = payload.get("meta") or {}
|
|
78
|
+
request_id = meta.get("request_id")
|
|
79
|
+
details = err.get("details")
|
|
80
|
+
cls = _ERROR_MAP.get(code, XTreeError)
|
|
81
|
+
if code == "INTERNAL_ERROR" and status_code == 502:
|
|
82
|
+
cls = XTreeServiceError
|
|
83
|
+
return cls(
|
|
84
|
+
message,
|
|
85
|
+
code=code,
|
|
86
|
+
status_code=status_code,
|
|
87
|
+
request_id=request_id,
|
|
88
|
+
details=details,
|
|
89
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""AUTO-GENERATED `facebook` platform client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FacebookAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def search_general(self, **body: Any) -> APIResponse[Any]:
|
|
16
|
+
return self._http.request("POST", "/facebook/search/general", json=body)
|
|
17
|
+
|
|
18
|
+
def search_users(self, **body: Any) -> APIResponse[Any]:
|
|
19
|
+
return self._http.request("POST", "/facebook/search/users", json=body)
|
xtree/jd/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""AUTO-GENERATED `jd` platform client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JdAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def items_detail(self, **body: Any) -> APIResponse[Any]:
|
|
16
|
+
return self._http.request("POST", "/jd/items/detail", json=body)
|
|
17
|
+
|
|
18
|
+
def items_comments(self, **body: Any) -> APIResponse[Any]:
|
|
19
|
+
return self._http.request("POST", "/jd/items/comments", json=body)
|
|
20
|
+
|
|
21
|
+
def items_comments_v2(self, **body: Any) -> APIResponse[Any]:
|
|
22
|
+
return self._http.request("POST", "/jd/items/comments/v2", json=body)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""AUTO-GENERATED `kuaishou` platform client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KuaishouAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def videos_detail(self, **body: Any) -> APIResponse[Any]:
|
|
16
|
+
return self._http.request("POST", "/kuaishou/videos/detail", json=body)
|
|
17
|
+
|
|
18
|
+
def videos_comments(self, **body: Any) -> APIResponse[Any]:
|
|
19
|
+
return self._http.request("POST", "/kuaishou/videos/comments", json=body)
|
|
20
|
+
|
|
21
|
+
def users_profile(self, **body: Any) -> APIResponse[Any]:
|
|
22
|
+
return self._http.request("POST", "/kuaishou/users/profile", json=body)
|
|
23
|
+
|
|
24
|
+
def users_posts(self, **body: Any) -> APIResponse[Any]:
|
|
25
|
+
return self._http.request("POST", "/kuaishou/users/posts", json=body)
|
|
26
|
+
|
|
27
|
+
def search_videos(self, **body: Any) -> APIResponse[Any]:
|
|
28
|
+
return self._http.request("POST", "/kuaishou/search/videos", json=body)
|
|
29
|
+
|
|
30
|
+
def videos_short_link(self, **body: Any) -> APIResponse[Any]:
|
|
31
|
+
return self._http.request("POST", "/kuaishou/videos/short-link", json=body)
|
|
32
|
+
|
|
33
|
+
def hot_board(self, **body: Any) -> APIResponse[Any]:
|
|
34
|
+
return self._http.request("POST", "/kuaishou/hot/board", json=body)
|
|
35
|
+
|
|
36
|
+
def ids_resolve(self, **body: Any) -> APIResponse[Any]:
|
|
37
|
+
return self._http.request("POST", "/kuaishou/ids/resolve", json=body)
|
xtree/pdd/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""AUTO-GENERATED `pdd` platform client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from xtree._http import HTTPClient
|
|
8
|
+
from xtree.types import APIResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PddAPI:
|
|
12
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
13
|
+
self._http = http
|
|
14
|
+
|
|
15
|
+
def items_comments_submit(self, **body: Any) -> APIResponse[Any]:
|
|
16
|
+
return self._http.request("POST", "/pdd/items/comments/submit", json=body)
|
|
17
|
+
|
|
18
|
+
def items_comments_status(self, **body: Any) -> APIResponse[Any]:
|
|
19
|
+
return self._http.request("POST", "/pdd/items/comments/status", json=body)
|
|
20
|
+
|
|
21
|
+
def items_comments_fetch(self, **body: Any) -> APIResponse[Any]:
|
|
22
|
+
return self._http.request("POST", "/pdd/items/comments/fetch", json=body)
|