dataxapi 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.
- dataxapi/__init__.py +4 -0
- dataxapi/client.py +302 -0
- dataxapi/generated.py +5336 -0
- dataxapi/py.typed +1 -0
- dataxapi-0.1.0.dist-info/METADATA +34 -0
- dataxapi-0.1.0.dist-info/RECORD +8 -0
- dataxapi-0.1.0.dist-info/WHEEL +5 -0
- dataxapi-0.1.0.dist-info/top_level.txt +1 -0
dataxapi/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Generated by scripts/generate-exact-api-sdks.mjs. Do not edit.
|
|
2
|
+
from .client import 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, XiaohongshuNotesDetailV1Input, XiaohongshuNotesDetailV1Response, 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, DouyinUsersVideosV1Input, DouyinUsersVideosV1Response, DouyinUsersSearchV1Input, DouyinUsersSearchV1Response, DouyinTrendsHotsearchV1Input, DouyinTrendsHotsearchV1Response, DouyinTrendsRisingV1Input, DouyinTrendsRisingV1Response, DouyinRankingsLowfanV1Input, DouyinRankingsLowfanV1Response, DouyinRankingsHighcompletionV1Input, DouyinRankingsHighcompletionV1Response, DouyinMixesDetailV1Input, DouyinMixesDetailV1Response, DouyinMixesVideosV1Input, DouyinMixesVideosV1Response, XiaohongshuTopicsDetailV1Input, XiaohongshuTopicsDetailV1Response, XiaohongshuTopicsNotesV1Input, XiaohongshuTopicsNotesV1Response, XiaohongshuProductsSearchV1Input, XiaohongshuProductsSearchV1Response, XiaohongshuProductsDetailV1Input, XiaohongshuProductsDetailV1Response, XPostsBatchDetailV1Input, XPostsBatchDetailV1Response, 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, RedditSearchCommentsV1Input, RedditSearchCommentsV1Response, RedditSearchMediaV1Input, RedditSearchMediaV1Response, RedditSearchCommunitiesV1Input, RedditSearchCommunitiesV1Response, RedditSearchUsersV1Input, RedditSearchUsersV1Response, 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
|
|
4
|
+
__all__ = ["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", "XiaohongshuNotesDetailV1Input", "XiaohongshuNotesDetailV1Response", "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", "DouyinUsersVideosV1Input", "DouyinUsersVideosV1Response", "DouyinUsersSearchV1Input", "DouyinUsersSearchV1Response", "DouyinTrendsHotsearchV1Input", "DouyinTrendsHotsearchV1Response", "DouyinTrendsRisingV1Input", "DouyinTrendsRisingV1Response", "DouyinRankingsLowfanV1Input", "DouyinRankingsLowfanV1Response", "DouyinRankingsHighcompletionV1Input", "DouyinRankingsHighcompletionV1Response", "DouyinMixesDetailV1Input", "DouyinMixesDetailV1Response", "DouyinMixesVideosV1Input", "DouyinMixesVideosV1Response", "XiaohongshuTopicsDetailV1Input", "XiaohongshuTopicsDetailV1Response", "XiaohongshuTopicsNotesV1Input", "XiaohongshuTopicsNotesV1Response", "XiaohongshuProductsSearchV1Input", "XiaohongshuProductsSearchV1Response", "XiaohongshuProductsDetailV1Input", "XiaohongshuProductsDetailV1Response", "XPostsBatchDetailV1Input", "XPostsBatchDetailV1Response", "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", "RedditSearchCommentsV1Input", "RedditSearchCommentsV1Response", "RedditSearchMediaV1Input", "RedditSearchMediaV1Response", "RedditSearchCommunitiesV1Input", "RedditSearchCommunitiesV1Response", "RedditSearchUsersV1Input", "RedditSearchUsersV1Response", "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"]
|
dataxapi/client.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import io
|
|
3
|
+
import http.client
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
import socket
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
from urllib.parse import quote, urlsplit
|
|
12
|
+
|
|
13
|
+
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
|
+
|
|
16
|
+
class ExactApiError(Exception):
|
|
17
|
+
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"Exact API request failed with HTTP {status}"))
|
|
19
|
+
self.status = status
|
|
20
|
+
self.code = problem.get("code", "unknown_error")
|
|
21
|
+
self.detail = problem.get("detail", str(self))
|
|
22
|
+
self.request_id = problem.get("request_id")
|
|
23
|
+
self.charge_status = problem.get("charge_status")
|
|
24
|
+
self.charged_amount = problem.get("charged_amount")
|
|
25
|
+
self.retry_after_ms = problem.get("retry_after_ms")
|
|
26
|
+
self.retry_after = retry_after.strip(" \t") if retry_after is not None else None
|
|
27
|
+
self.idempotency_key = idempotency_key
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExactApiConnectionError(Exception):
|
|
31
|
+
def __init__(self, message: str, idempotency_key: str | None):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.idempotency_key = idempotency_key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExactApiClient(GeneratedOperationsClient):
|
|
37
|
+
def __init__(self, base_url: str, api_key: str, *, timeout: float = 10, max_retries: int = 2):
|
|
38
|
+
if not base_url or not api_key:
|
|
39
|
+
raise ValueError("base_url and api_key are required")
|
|
40
|
+
parsed_base_url = urlsplit(base_url)
|
|
41
|
+
loopback = parsed_base_url.hostname in ("localhost", "127.0.0.1", "::1")
|
|
42
|
+
if parsed_base_url.scheme not in ("http", "https") or not parsed_base_url.hostname or (parsed_base_url.scheme == "http" and not loopback) or parsed_base_url.username or parsed_base_url.password or parsed_base_url.query or parsed_base_url.fragment:
|
|
43
|
+
raise ValueError("base_url must be an HTTP(S) URL without embedded credentials")
|
|
44
|
+
self._base_url = base_url.rstrip("/")
|
|
45
|
+
self._api_key = api_key
|
|
46
|
+
self._timeout = _validate_timeout(timeout)
|
|
47
|
+
self._max_retries = _validate_retries(max_retries)
|
|
48
|
+
self.x = _X(self)
|
|
49
|
+
self.reddit = _Reddit(self)
|
|
50
|
+
self.xiaohongshu = _Xiaohongshu(self)
|
|
51
|
+
self.usage = _Usage(self)
|
|
52
|
+
|
|
53
|
+
def _call_operation(self, operation_id: OperationId, input: OperationInput, *, idempotency_key: str | None = None) -> OperationResponse:
|
|
54
|
+
operation = OPERATIONS[operation_id]
|
|
55
|
+
return cast(OperationResponse, self._execute(operation["method"], operation["path"], dict(input), idempotency_key))
|
|
56
|
+
|
|
57
|
+
def _execute(self, method: str, path: str, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
|
|
58
|
+
key = idempotency_key or f"sdk_{uuid.uuid4()}"
|
|
59
|
+
return self._request(path, method=method, body=body, idempotency_key=key)
|
|
60
|
+
|
|
61
|
+
def _request(self, path: str, *, method: str, body: dict[str, Any] | None = None, idempotency_key: str | None = None) -> dict[str, Any]:
|
|
62
|
+
headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"}
|
|
63
|
+
data = None
|
|
64
|
+
if body is not None:
|
|
65
|
+
headers["Content-Type"] = "application/json"
|
|
66
|
+
data = json.dumps(body).encode()
|
|
67
|
+
if idempotency_key:
|
|
68
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
69
|
+
|
|
70
|
+
for attempt in range(self._max_retries + 1):
|
|
71
|
+
try:
|
|
72
|
+
status, response_headers, payload = self._send(method, path, headers, data)
|
|
73
|
+
try:
|
|
74
|
+
decoded: object = json.loads(payload)
|
|
75
|
+
except (ValueError, RecursionError):
|
|
76
|
+
decoded = None
|
|
77
|
+
if 200 <= status < 300:
|
|
78
|
+
if not isinstance(decoded, dict):
|
|
79
|
+
raise _InvalidResponse("Exact API success response must be a JSON object")
|
|
80
|
+
return cast(dict[str, Any], decoded)
|
|
81
|
+
problem: dict[str, Any] = cast(dict[str, Any], decoded) if isinstance(decoded, dict) else {
|
|
82
|
+
"code": "invalid_error_response",
|
|
83
|
+
"detail": f"Exact API returned an invalid HTTP {status} error body.",
|
|
84
|
+
}
|
|
85
|
+
error = ExactApiError(status, problem, idempotency_key, response_headers.get("Retry-After"))
|
|
86
|
+
if attempt < self._max_retries and _retryable(status, error.code):
|
|
87
|
+
delay = _retry_delay(error.retry_after, problem)
|
|
88
|
+
if delay is not None and delay <= self._timeout:
|
|
89
|
+
if delay > 0:
|
|
90
|
+
time.sleep(delay)
|
|
91
|
+
continue
|
|
92
|
+
raise error
|
|
93
|
+
except (OSError, TimeoutError, socket.timeout, http.client.HTTPException, _InvalidResponse) as error:
|
|
94
|
+
if attempt < self._max_retries:
|
|
95
|
+
continue
|
|
96
|
+
raise ExactApiConnectionError(
|
|
97
|
+
"The request outcome is unknown. Retry the same logical operation with this idempotency key.",
|
|
98
|
+
idempotency_key,
|
|
99
|
+
) from error
|
|
100
|
+
raise AssertionError("unreachable")
|
|
101
|
+
|
|
102
|
+
def get_usage(self, request_id: str) -> Usage:
|
|
103
|
+
path = USAGE_OPERATION["path"].replace("{requestId}", quote(request_id, safe=""))
|
|
104
|
+
return cast(Usage, self._request(path, method=USAGE_OPERATION["method"]))
|
|
105
|
+
|
|
106
|
+
def _send(self, method: str, path: str, headers: dict[str, str], data: bytes | None) -> tuple[int, http.client.HTTPMessage, bytes]:
|
|
107
|
+
parsed = urlsplit(f"{self._base_url}{path}")
|
|
108
|
+
if parsed.scheme not in ("http", "https") or not parsed.hostname:
|
|
109
|
+
raise ValueError("base_url must use http or https")
|
|
110
|
+
deadline = time.monotonic() + self._timeout
|
|
111
|
+
connection_type = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
|
|
112
|
+
connection = connection_type(parsed.hostname, parsed.port, timeout=_remaining(deadline))
|
|
113
|
+
request_path = parsed.path or "/"
|
|
114
|
+
if parsed.query:
|
|
115
|
+
request_path += f"?{parsed.query}"
|
|
116
|
+
|
|
117
|
+
class DeadlineResponse(http.client.HTTPResponse):
|
|
118
|
+
def __init__(self, sock: socket.socket, debuglevel: int = 0, method: str | None = None, url: str | None = None):
|
|
119
|
+
super().__init__(sock, debuglevel=debuglevel, method=method, url=url)
|
|
120
|
+
reader = self.fp
|
|
121
|
+
self.fp = io.BufferedReader(_DeadlineReader(reader.detach(), sock, deadline))
|
|
122
|
+
|
|
123
|
+
connection.response_class = DeadlineResponse
|
|
124
|
+
response: http.client.HTTPResponse | None = None
|
|
125
|
+
try:
|
|
126
|
+
connection.connect()
|
|
127
|
+
_set_socket_timeout(connection, deadline)
|
|
128
|
+
connection.request(method, request_path, body=data, headers=headers)
|
|
129
|
+
response = connection.getresponse()
|
|
130
|
+
payload = response.read()
|
|
131
|
+
_remaining(deadline)
|
|
132
|
+
return response.status, response.headers, payload
|
|
133
|
+
finally:
|
|
134
|
+
if response is not None:
|
|
135
|
+
response.close()
|
|
136
|
+
connection.close()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _DeadlineReader(io.RawIOBase):
|
|
140
|
+
def __init__(self, reader: io.RawIOBase, sock: socket.socket, deadline: float):
|
|
141
|
+
self._reader = reader
|
|
142
|
+
self._socket = sock
|
|
143
|
+
self._deadline = deadline
|
|
144
|
+
|
|
145
|
+
def readable(self) -> bool:
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
def readinto(self, buffer: Any) -> int | None:
|
|
149
|
+
# HTTP status, headers and chunk framing can perform many reads internally.
|
|
150
|
+
self._socket.settimeout(_remaining(self._deadline))
|
|
151
|
+
return self._reader.readinto(buffer)
|
|
152
|
+
|
|
153
|
+
def close(self) -> None:
|
|
154
|
+
try:
|
|
155
|
+
self._reader.close()
|
|
156
|
+
finally:
|
|
157
|
+
super().close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class _X:
|
|
161
|
+
def __init__(self, client: ExactApiClient):
|
|
162
|
+
self.posts = _XPosts(client)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _XPosts:
|
|
166
|
+
def __init__(self, client: ExactApiClient):
|
|
167
|
+
self._client = client
|
|
168
|
+
|
|
169
|
+
def search(self, input: XPostsSearchV1Input, *, idempotency_key: str | None = None) -> XPostsSearchV1Response:
|
|
170
|
+
return self._client.call("x_posts_search_v1", input, idempotency_key=idempotency_key)
|
|
171
|
+
|
|
172
|
+
def search_pages(self, input: XPostsSearchV1Input) -> Iterator[XPostsSearchV1Response]:
|
|
173
|
+
current = dict(input)
|
|
174
|
+
seen_cursors: set[str] = {input["cursor"]} if "cursor" in input else set()
|
|
175
|
+
while True:
|
|
176
|
+
page = self.search(cast(XPostsSearchV1Input, current))
|
|
177
|
+
cursor = page["page"]["next_cursor"]
|
|
178
|
+
if cursor and cursor in seen_cursors:
|
|
179
|
+
raise ExactApiPaginationError(cursor)
|
|
180
|
+
if cursor:
|
|
181
|
+
seen_cursors.add(cursor)
|
|
182
|
+
yield page
|
|
183
|
+
if not cursor:
|
|
184
|
+
return
|
|
185
|
+
current = {**input, "cursor": cursor}
|
|
186
|
+
|
|
187
|
+
def detail(self, input: XPostsDetailV1Input, *, idempotency_key: str | None = None) -> XPostsDetailV1Response:
|
|
188
|
+
return self._client.call("x_posts_detail_v1", input, idempotency_key=idempotency_key)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class _Reddit:
|
|
192
|
+
def __init__(self, client: ExactApiClient):
|
|
193
|
+
self.posts = _RedditPosts(client)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _RedditPosts:
|
|
197
|
+
def __init__(self, client: ExactApiClient):
|
|
198
|
+
self._client = client
|
|
199
|
+
|
|
200
|
+
def detail(self, input: RedditPostsDetailV1Input, *, idempotency_key: str | None = None) -> RedditPostsDetailV1Response:
|
|
201
|
+
return self._client.call("reddit_posts_detail_v1", input, idempotency_key=idempotency_key)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class _Xiaohongshu:
|
|
205
|
+
def __init__(self, client: ExactApiClient):
|
|
206
|
+
self.pgy = _XiaohongshuPgy(client)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _XiaohongshuPgy:
|
|
210
|
+
def __init__(self, client: ExactApiClient):
|
|
211
|
+
self.notes = _XiaohongshuPgyNotes(client)
|
|
212
|
+
self.bloggers = _XiaohongshuPgyBloggers(client)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class _XiaohongshuPgyNotes:
|
|
216
|
+
def __init__(self, client: ExactApiClient):
|
|
217
|
+
self._client = client
|
|
218
|
+
|
|
219
|
+
def detail(self, input: XiaohongshuPgyNotesDetailV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyNotesDetailV1Response:
|
|
220
|
+
return self._client.call("xiaohongshu_pgy_notes_detail_v1", input, idempotency_key=idempotency_key)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _XiaohongshuPgyBloggers:
|
|
224
|
+
def __init__(self, client: ExactApiClient):
|
|
225
|
+
self._client = client
|
|
226
|
+
|
|
227
|
+
def detail(self, input: XiaohongshuPgyBloggersDetailV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyBloggersDetailV1Response:
|
|
228
|
+
return self._client.call("xiaohongshu_pgy_bloggers_detail_v1", input, idempotency_key=idempotency_key)
|
|
229
|
+
|
|
230
|
+
def notes(self, input: XiaohongshuPgyBloggersNotesV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyBloggersNotesV1Response:
|
|
231
|
+
return self._client.call("xiaohongshu_pgy_bloggers_notes_v1", input, idempotency_key=idempotency_key)
|
|
232
|
+
|
|
233
|
+
def core_data(self, input: XiaohongshuPgyBloggersCoredataV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyBloggersCoredataV1Response:
|
|
234
|
+
return self._client.call("xiaohongshu_pgy_bloggers_coredata_v1", input, idempotency_key=idempotency_key)
|
|
235
|
+
|
|
236
|
+
def data_summary(self, input: XiaohongshuPgyBloggersDatasummaryV1Input, *, idempotency_key: str | None = None) -> XiaohongshuPgyBloggersDatasummaryV1Response:
|
|
237
|
+
return self._client.call("xiaohongshu_pgy_bloggers_datasummary_v1", input, idempotency_key=idempotency_key)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class _Usage:
|
|
241
|
+
def __init__(self, client: ExactApiClient):
|
|
242
|
+
self._client = client
|
|
243
|
+
|
|
244
|
+
def get(self, request_id: str) -> Usage:
|
|
245
|
+
return self._client.get_usage(request_id)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class _InvalidResponse(Exception):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class ExactApiPaginationError(Exception):
|
|
253
|
+
def __init__(self, cursor: str):
|
|
254
|
+
super().__init__("Pagination stopped because the API returned a repeated cursor.")
|
|
255
|
+
self.cursor = cursor
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _remaining(deadline: float) -> float:
|
|
259
|
+
remaining = deadline - time.monotonic()
|
|
260
|
+
if remaining <= 0:
|
|
261
|
+
raise TimeoutError("request timeout exceeded")
|
|
262
|
+
return remaining
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _validate_timeout(value: object) -> float:
|
|
266
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0:
|
|
267
|
+
raise ValueError("timeout must be a finite positive number")
|
|
268
|
+
return float(value)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _validate_retries(value: object) -> int:
|
|
272
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
273
|
+
raise ValueError("max_retries must be a non-negative integer")
|
|
274
|
+
return value
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _set_socket_timeout(connection: http.client.HTTPConnection, deadline: float) -> None:
|
|
278
|
+
if connection.sock is None:
|
|
279
|
+
raise ConnectionError("connection socket is unavailable")
|
|
280
|
+
connection.sock.settimeout(_remaining(deadline))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _retryable(status: int, code: str) -> bool:
|
|
284
|
+
return status == 429 or (status >= 500 and code != "listing_paused") or (status == 409 and code == "request_in_progress")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _retry_delay(retry_after: str | None, problem: dict[str, Any]) -> float | None:
|
|
288
|
+
if "retry_after_ms" in problem:
|
|
289
|
+
retry_after_ms: object = problem["retry_after_ms"]
|
|
290
|
+
if isinstance(retry_after_ms, bool) or not isinstance(retry_after_ms, (int, float)):
|
|
291
|
+
return None
|
|
292
|
+
try:
|
|
293
|
+
seconds = retry_after_ms / 1000
|
|
294
|
+
except OverflowError:
|
|
295
|
+
return None
|
|
296
|
+
elif retry_after is not None:
|
|
297
|
+
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", retry_after):
|
|
298
|
+
return None
|
|
299
|
+
seconds = float(retry_after)
|
|
300
|
+
else:
|
|
301
|
+
seconds = 0.1
|
|
302
|
+
return seconds if math.isfinite(seconds) and seconds >= 0 else None
|