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 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