dataxapi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataxapi-0.1.0/PKG-INFO +34 -0
- dataxapi-0.1.0/README.md +19 -0
- dataxapi-0.1.0/pyproject.toml +34 -0
- dataxapi-0.1.0/setup.cfg +4 -0
- dataxapi-0.1.0/src/dataxapi/__init__.py +4 -0
- dataxapi-0.1.0/src/dataxapi/client.py +302 -0
- dataxapi-0.1.0/src/dataxapi/generated.py +5336 -0
- dataxapi-0.1.0/src/dataxapi/py.typed +1 -0
- dataxapi-0.1.0/src/dataxapi.egg-info/PKG-INFO +34 -0
- dataxapi-0.1.0/src/dataxapi.egg-info/SOURCES.txt +11 -0
- dataxapi-0.1.0/src/dataxapi.egg-info/dependency_links.txt +1 -0
- dataxapi-0.1.0/src/dataxapi.egg-info/top_level.txt +1 -0
- dataxapi-0.1.0/tests/test_client.py +423 -0
dataxapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataxapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DataXAPI Python SDK: exact public social data endpoints with one CNY balance
|
|
5
|
+
Author-email: "DataXAPI (Aurevant LLC)" <support@dataxapi.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://dataxapi.com
|
|
8
|
+
Project-URL: Documentation, https://dataxapi.com/en/apis/
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# DataXAPI Python SDK(内部 Preview)
|
|
17
|
+
|
|
18
|
+
该包由当前 Catalog Release/OpenAPI 生成 `TypedDict` 输入、输出、错误和样例;运行时只负责认证、超时、同一逻辑调用的幂等重试、分页和 Usage。它不包含 Provider、价格判断或接口选择逻辑。
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import os
|
|
22
|
+
from dataxapi import ExactApiClient
|
|
23
|
+
|
|
24
|
+
client = ExactApiClient(
|
|
25
|
+
os.environ["DATAXAPI_API_BASE_URL"],
|
|
26
|
+
os.environ["DATAXAPI_API_KEY"],
|
|
27
|
+
)
|
|
28
|
+
result = client.x.posts.search({"query": "OpenAI min_faves:100"})
|
|
29
|
+
print(result["data"]["items"], result["meta"]["charged_amount"])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`timeout` 限制每次网络尝试的总时长,覆盖响应头和正文。SDK 只自动等待不超过该值的合法重试间隔;更长或无效的服务端间隔会直接返回原 `ExactApiError`,由调用方决定何时重试。错误保留 `retry_after_ms`/`retry_after` 和 `idempotency_key`;恢复同一次调用时必须复用该 Key。合法的正文间隔优先于 `Retry-After`,每次调用最多进行 `max_retries + 1` 次尝试。
|
|
33
|
+
|
|
34
|
+
当前仍使用内部包名并带有 `Private :: Do Not Upload` 分类。正式 PyPI 发布账户与包名尚未签收;本文内部包示例不代表已经可以从公开 Registry 安装。
|
dataxapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# DataXAPI Python SDK(内部 Preview)
|
|
2
|
+
|
|
3
|
+
该包由当前 Catalog Release/OpenAPI 生成 `TypedDict` 输入、输出、错误和样例;运行时只负责认证、超时、同一逻辑调用的幂等重试、分页和 Usage。它不包含 Provider、价格判断或接口选择逻辑。
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import os
|
|
7
|
+
from dataxapi import ExactApiClient
|
|
8
|
+
|
|
9
|
+
client = ExactApiClient(
|
|
10
|
+
os.environ["DATAXAPI_API_BASE_URL"],
|
|
11
|
+
os.environ["DATAXAPI_API_KEY"],
|
|
12
|
+
)
|
|
13
|
+
result = client.x.posts.search({"query": "OpenAI min_faves:100"})
|
|
14
|
+
print(result["data"]["items"], result["meta"]["charged_amount"])
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`timeout` 限制每次网络尝试的总时长,覆盖响应头和正文。SDK 只自动等待不超过该值的合法重试间隔;更长或无效的服务端间隔会直接返回原 `ExactApiError`,由调用方决定何时重试。错误保留 `retry_after_ms`/`retry_after` 和 `idempotency_key`;恢复同一次调用时必须复用该 Key。合法的正文间隔优先于 `Retry-After`,每次调用最多进行 `max_retries + 1` 次尝试。
|
|
18
|
+
|
|
19
|
+
当前仍使用内部包名并带有 `Private :: Do Not Upload` 分类。正式 PyPI 发布账户与包名尚未签收;本文内部包示例不代表已经可以从公开 Registry 安装。
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dataxapi"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = ">=3.11"
|
|
5
|
+
description = "DataXAPI Python SDK: exact public social data endpoints with one CNY balance"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "DataXAPI (Aurevant LLC)", email = "support@dataxapi.com" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Typing :: Typed",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://dataxapi.com"
|
|
18
|
+
Documentation = "https://dataxapi.com/en/apis/"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=77"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["src"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
dataxapi = ["py.typed"]
|
|
29
|
+
|
|
30
|
+
[tool.pyright]
|
|
31
|
+
include = ["src"]
|
|
32
|
+
extraPaths = ["src"]
|
|
33
|
+
pythonVersion = "3.11"
|
|
34
|
+
typeCheckingMode = "strict"
|
dataxapi-0.1.0/setup.cfg
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"]
|
|
@@ -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
|