okdata 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.
- okdata/__init__.py +28 -0
- okdata/_async_client.py +119 -0
- okdata/_base.py +88 -0
- okdata/_client.py +135 -0
- okdata/_errors.py +88 -0
- okdata/_generated/__init__.py +1 -0
- okdata/_generated/_async.py +7150 -0
- okdata/_generated/_endpoints.py +227 -0
- okdata/_generated/_errors.py +36 -0
- okdata/_generated/_models.py +1329 -0
- okdata/_generated/_sync.py +7150 -0
- okdata/_money.py +44 -0
- okdata/_paging.py +208 -0
- okdata/_request.py +74 -0
- okdata/_result.py +119 -0
- okdata/_transport.py +103 -0
- okdata/_types.py +123 -0
- okdata/_version.py +1 -0
- okdata/py.typed +0 -0
- okdata-0.1.0.dist-info/METADATA +143 -0
- okdata-0.1.0.dist-info/RECORD +23 -0
- okdata-0.1.0.dist-info/WHEEL +4 -0
- okdata-0.1.0.dist-info/licenses/LICENSE +21 -0
okdata/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""okdata: social data for AI agents, paid per call.
|
|
2
|
+
|
|
3
|
+
>>> from okdata import OkData
|
|
4
|
+
>>> ok = OkData() # reads OKDATA_API_KEY
|
|
5
|
+
>>> page = ok.douyin.videos.search(query="露营装备", max_price="0.05")
|
|
6
|
+
|
|
7
|
+
One typed method per endpoint (generated from the catalog), sync (OkData) and async (AsyncOkData),
|
|
8
|
+
an automatic Idempotency-Key per call, retries driven by the error's ``retry`` field, and
|
|
9
|
+
``pages()`` / ``items()`` that stop before ``max_spend``.
|
|
10
|
+
"""
|
|
11
|
+
from ._async_client import AsyncOkData
|
|
12
|
+
from ._base import DEFAULT_BASE_URL, ENV_API_KEY, ENV_BASE_URL, endpoint
|
|
13
|
+
from ._client import OkData
|
|
14
|
+
from ._errors import OkDataConnectionError, OkDataError, RetryAdvice
|
|
15
|
+
from ._generated._endpoints import CATALOG_REVISION, ENDPOINTS, PRICE_BOOK
|
|
16
|
+
from ._generated._errors import ERROR_CODES, ERRORS
|
|
17
|
+
from ._generated._models import * # noqa: F401,F403 (Post, Account, Comment, ...)
|
|
18
|
+
from ._paging import AsyncItemPager, AsyncPager, ItemPager, Pager, StopReason
|
|
19
|
+
from ._types import Batch, Billing, CallWarning, EndpointInfo, Fields, Item, Meta, Missing, Page, Price, Result, View
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"OkData", "AsyncOkData", "OkDataError", "OkDataConnectionError", "RetryAdvice",
|
|
24
|
+
"Page", "Item", "Batch", "Result", "Meta", "Billing", "CallWarning", "Missing", "EndpointInfo",
|
|
25
|
+
"View", "Fields", "Price", "Pager", "ItemPager", "AsyncPager", "AsyncItemPager", "StopReason",
|
|
26
|
+
"ENDPOINTS", "CATALOG_REVISION", "PRICE_BOOK", "ERROR_CODES", "ERRORS", "endpoint",
|
|
27
|
+
"DEFAULT_BASE_URL", "ENV_API_KEY", "ENV_BASE_URL", "__version__",
|
|
28
|
+
]
|
okdata/_async_client.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""AsyncOkData: the asyncio client, the same surface as OkData with ``await``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any, Awaitable, Callable, Dict, Mapping, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._base import (
|
|
10
|
+
call_request, check_request_id, endpoint, make_config, merge_params, result_query, usage_query,
|
|
11
|
+
)
|
|
12
|
+
from ._generated._async import AsyncEndpoints
|
|
13
|
+
from ._paging import AsyncItemPager, AsyncPager, _Budget
|
|
14
|
+
from ._result import to_result
|
|
15
|
+
from ._transport import Send, send_async
|
|
16
|
+
from ._types import Price, Result, View
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _AsyncCalls:
|
|
20
|
+
def __init__(self, ok: "AsyncOkData") -> None:
|
|
21
|
+
self._ok = ok
|
|
22
|
+
|
|
23
|
+
async def get(self, request_id: str) -> Dict[str, Any]:
|
|
24
|
+
return await self._ok.request("GET", f"/v1/calls/{check_request_id(request_id)}")
|
|
25
|
+
|
|
26
|
+
async def result(self, request_id: str, *, view: Optional[View] = None, fields: Optional[Union[str, Sequence[str]]] = None) -> Result:
|
|
27
|
+
body = await send_async(self._ok._cfg, self._ok._http, self._ok._sleep,
|
|
28
|
+
Send("GET", f"/v1/calls/{check_request_id(request_id)}/result", result_query(view, fields)))
|
|
29
|
+
return to_result(body, None)
|
|
30
|
+
|
|
31
|
+
async def by_key(self, idempotency_key: str) -> Dict[str, Any]:
|
|
32
|
+
return await self._ok.request("GET", "/v1/calls", query=[("idempotency_key", idempotency_key)])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncOkData(AsyncEndpoints):
|
|
36
|
+
"""Async client for the okdata API.
|
|
37
|
+
|
|
38
|
+
>>> async with AsyncOkData() as ok:
|
|
39
|
+
... page = await ok.douyin.videos.search(query="露营装备")
|
|
40
|
+
... async for item in ok.items("douyin.videos.search", query="露营装备", max_spend="0.1"):
|
|
41
|
+
... ...
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_key: Optional[str] = None,
|
|
47
|
+
*,
|
|
48
|
+
base_url: Optional[str] = None,
|
|
49
|
+
timeout: float = 65.0,
|
|
50
|
+
max_retries: int = 2,
|
|
51
|
+
retry_base_delay: float = 0.5,
|
|
52
|
+
max_retry_delay: float = 20.0,
|
|
53
|
+
entry: str = "sdk",
|
|
54
|
+
source: Optional[str] = None,
|
|
55
|
+
end_user: Optional[str] = None,
|
|
56
|
+
user_agent: Optional[str] = None,
|
|
57
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
58
|
+
sleep: Optional[Callable[[float], Awaitable[None]]] = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._cfg = make_config(api_key, base_url, timeout, max_retries, retry_base_delay, max_retry_delay, entry, source, end_user, user_agent)
|
|
61
|
+
self.base_url = self._cfg.base_url
|
|
62
|
+
self._owns_http = http_client is None
|
|
63
|
+
self._http = http_client or httpx.AsyncClient()
|
|
64
|
+
self._sleep: Callable[[float], Awaitable[None]] = sleep or asyncio.sleep
|
|
65
|
+
self.calls = _AsyncCalls(self)
|
|
66
|
+
self._init_endpoints(self)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def has_key(self) -> bool:
|
|
70
|
+
return self._cfg.api_key is not None
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"AsyncOkData(base_url={self.base_url!r}, key={'set' if self.has_key else 'missing'})"
|
|
74
|
+
|
|
75
|
+
async def __aenter__(self) -> "AsyncOkData":
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
79
|
+
await self.aclose()
|
|
80
|
+
|
|
81
|
+
async def aclose(self) -> None:
|
|
82
|
+
if self._owns_http:
|
|
83
|
+
await self._http.aclose()
|
|
84
|
+
|
|
85
|
+
async def _call(self, endpoint_id: str, params: Mapping[str, Any], *, idempotency_key: Optional[str] = None, timeout: Optional[float] = None) -> Result:
|
|
86
|
+
req = call_request(endpoint_id, params, idempotency_key, timeout)
|
|
87
|
+
return to_result(await send_async(self._cfg, self._http, self._sleep, req), req.idempotency_key)
|
|
88
|
+
|
|
89
|
+
async def call(self, endpoint_id: str, params: Optional[Mapping[str, Any]] = None, *, idempotency_key: Optional[str] = None,
|
|
90
|
+
timeout: Optional[float] = None, **kwargs: Any) -> Result:
|
|
91
|
+
return await self._call(endpoint_id, merge_params(params, kwargs), idempotency_key=idempotency_key, timeout=timeout)
|
|
92
|
+
|
|
93
|
+
def pages(self, endpoint_id: str, params: Optional[Mapping[str, Any]] = None, *, max_pages: Optional[int] = None,
|
|
94
|
+
max_spend: Optional[Price] = None, timeout: Optional[float] = None, **kwargs: Any) -> AsyncPager[Dict[str, Any]]:
|
|
95
|
+
"""``async for page in ok.pages(...)``: see OkData.pages."""
|
|
96
|
+
budget = _Budget(endpoint_id, endpoint(endpoint_id), merge_params(params, kwargs), max_pages, max_spend)
|
|
97
|
+
return AsyncPager(budget, lambda p: self._call(endpoint_id, p, timeout=timeout))
|
|
98
|
+
|
|
99
|
+
def items(self, endpoint_id: str, params: Optional[Mapping[str, Any]] = None, *, max_items: Optional[int] = None,
|
|
100
|
+
max_pages: Optional[int] = None, max_spend: Optional[Price] = None, timeout: Optional[float] = None,
|
|
101
|
+
**kwargs: Any) -> AsyncItemPager[Dict[str, Any]]:
|
|
102
|
+
return AsyncItemPager(self.pages(endpoint_id, params, max_pages=max_pages, max_spend=max_spend, timeout=timeout, **kwargs), max_items)
|
|
103
|
+
|
|
104
|
+
async def account(self) -> Dict[str, Any]:
|
|
105
|
+
return await self.request("GET", "/v1/account")
|
|
106
|
+
|
|
107
|
+
async def catalog(self) -> Dict[str, Any]:
|
|
108
|
+
return await self.request("GET", "/v1/catalog", auth=False)
|
|
109
|
+
|
|
110
|
+
async def status(self) -> Dict[str, Any]:
|
|
111
|
+
return await self.request("GET", "/v1/catalog/status", auth=False)
|
|
112
|
+
|
|
113
|
+
async def usage(self, *, group_by: Optional[Sequence[str]] = None, since: Optional[str] = None, until: Optional[str] = None) -> Dict[str, Any]:
|
|
114
|
+
return await self.request("GET", "/v1/usage", query=usage_query(group_by, since, until))
|
|
115
|
+
|
|
116
|
+
async def request(self, method: str, path: str, *, query: Sequence[Any] = (), body: Optional[Dict[str, Any]] = None, auth: bool = True) -> Dict[str, Any]:
|
|
117
|
+
envelope = await send_async(self._cfg, self._http, self._sleep, Send(method, path, tuple(query), body, None, None, auth))
|
|
118
|
+
data = envelope.get("data")
|
|
119
|
+
return data if isinstance(data, dict) else {}
|
okdata/_base.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""What OkData and AsyncOkData share: options, headers, and turning a call into a request."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
|
8
|
+
|
|
9
|
+
from ._generated._endpoints import ENDPOINTS
|
|
10
|
+
from ._request import build_request, field_list
|
|
11
|
+
from ._result import RetryPolicy
|
|
12
|
+
from ._transport import Config, Send
|
|
13
|
+
from ._types import EndpointInfo
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://api.okdata.online"
|
|
17
|
+
ENV_API_KEY = "OKDATA_API_KEY"
|
|
18
|
+
ENV_BASE_URL = "OKDATA_BASE_URL"
|
|
19
|
+
_LABEL = re.compile(r"[\x21-\x7e]{1,128}")
|
|
20
|
+
_KEY = re.compile(r"[\x21-\x7e]+")
|
|
21
|
+
_REQUEST_ID = re.compile(r"[A-Za-z0-9-]{1,64}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def endpoint(endpoint_id: str) -> Optional[EndpointInfo]:
|
|
25
|
+
"""The catalog entry this SDK was generated with, or None for an id it does not know."""
|
|
26
|
+
return ENDPOINTS.get(endpoint_id)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _label(value: Optional[str], name: str, most: int = 128) -> Optional[str]:
|
|
30
|
+
if value is None:
|
|
31
|
+
return None
|
|
32
|
+
if not _LABEL.fullmatch(value) or len(value) > most:
|
|
33
|
+
raise ValueError(f"{name} must be 1-{most} visible ASCII characters, no spaces")
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_config(
|
|
38
|
+
api_key: Optional[str], base_url: Optional[str], timeout: float, max_retries: int, retry_base_delay: float,
|
|
39
|
+
max_retry_delay: float, entry: str, source: Optional[str], end_user: Optional[str], user_agent: Optional[str],
|
|
40
|
+
) -> Config:
|
|
41
|
+
key = (api_key if api_key is not None else os.environ.get(ENV_API_KEY, "")).strip()
|
|
42
|
+
if key and not _KEY.fullmatch(key):
|
|
43
|
+
raise ValueError("the API key has spaces or control characters in it; copy it again (keys start with okd_)")
|
|
44
|
+
url = (base_url or os.environ.get(ENV_BASE_URL) or DEFAULT_BASE_URL).rstrip("/")
|
|
45
|
+
if not re.match(r"https?://[^/]+", url):
|
|
46
|
+
raise ValueError(f"base_url must start with https:// or http://, got {url!r}")
|
|
47
|
+
if isinstance(max_retries, bool) or not isinstance(max_retries, int) or max_retries < 0:
|
|
48
|
+
raise ValueError("max_retries must be a whole number, 0 or more")
|
|
49
|
+
headers = {"X-Entry": _label(entry, "entry", 32) or "sdk", "User-Agent": " ".join(filter(None, [f"okdata-sdk-python/{__version__}", user_agent]))}
|
|
50
|
+
for header, value, name in (("X-Source", source, "source"), ("X-End-User", end_user, "end_user")):
|
|
51
|
+
if _label(value, name) is not None:
|
|
52
|
+
headers[header] = value # type: ignore[assignment]
|
|
53
|
+
return Config(url, key or None, timeout, RetryPolicy(max_retries, retry_base_delay, max_retry_delay), headers)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def call_request(endpoint_id: str, params: Mapping[str, Any], idempotency_key: Optional[str], timeout: Optional[float]) -> Send:
|
|
57
|
+
req = build_request(endpoint_id, ENDPOINTS.get(endpoint_id), params)
|
|
58
|
+
return Send(req.method, req.path, tuple(req.query), req.body, idempotency_key or str(uuid.uuid4()), req.max_price, True, timeout)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def merge_params(params: Optional[Mapping[str, Any]], kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
|
62
|
+
return {**(params or {}), **kwargs}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check_request_id(request_id: str) -> str:
|
|
66
|
+
if not _REQUEST_ID.fullmatch(request_id):
|
|
67
|
+
raise ValueError("request_id must be the request_id of an earlier call")
|
|
68
|
+
return request_id
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def result_query(view: Optional[str], fields: Optional[Union[str, Sequence[str]]]) -> Tuple[Tuple[str, str], ...]:
|
|
72
|
+
query: List[Tuple[str, str]] = []
|
|
73
|
+
if view is not None:
|
|
74
|
+
query.append(("view", view))
|
|
75
|
+
if fields is not None:
|
|
76
|
+
query.append(("fields", ",".join(field_list(fields))))
|
|
77
|
+
return tuple(query)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def usage_query(group_by: Optional[Sequence[str]], since: Optional[str], until: Optional[str]) -> Tuple[Tuple[str, str], ...]:
|
|
81
|
+
query: List[Tuple[str, str]] = []
|
|
82
|
+
if group_by:
|
|
83
|
+
query.append(("group_by", ",".join(group_by)))
|
|
84
|
+
if since:
|
|
85
|
+
query.append(("since", since))
|
|
86
|
+
if until:
|
|
87
|
+
query.append(("until", until))
|
|
88
|
+
return tuple(query)
|
okdata/_client.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""OkData: the synchronous client."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._base import (
|
|
10
|
+
call_request, check_request_id, endpoint, make_config, merge_params, result_query, usage_query,
|
|
11
|
+
)
|
|
12
|
+
from ._generated._sync import SyncEndpoints
|
|
13
|
+
from ._paging import ItemPager, Pager, _Budget
|
|
14
|
+
from ._result import to_result
|
|
15
|
+
from ._transport import Send, send_sync
|
|
16
|
+
from ._types import Price, Result, View
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Calls:
|
|
20
|
+
"""Earlier calls: their record, their kept result (free for 24 h), where an Idempotency-Key stands."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, ok: "OkData") -> None:
|
|
23
|
+
self._ok = ok
|
|
24
|
+
|
|
25
|
+
def get(self, request_id: str) -> Dict[str, Any]:
|
|
26
|
+
return self._ok.request("GET", f"/v1/calls/{check_request_id(request_id)}")
|
|
27
|
+
|
|
28
|
+
def result(self, request_id: str, *, view: Optional[View] = None, fields: Optional[Union[str, Sequence[str]]] = None) -> Result:
|
|
29
|
+
body = send_sync(self._ok._cfg, self._ok._http, self._ok._sleep,
|
|
30
|
+
Send("GET", f"/v1/calls/{check_request_id(request_id)}/result", result_query(view, fields)))
|
|
31
|
+
return to_result(body, None)
|
|
32
|
+
|
|
33
|
+
def by_key(self, idempotency_key: str) -> Dict[str, Any]:
|
|
34
|
+
return self._ok.request("GET", "/v1/calls", query=[("idempotency_key", idempotency_key)])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OkData(SyncEndpoints):
|
|
38
|
+
"""Client for the okdata API.
|
|
39
|
+
|
|
40
|
+
>>> ok = OkData() # reads OKDATA_API_KEY
|
|
41
|
+
>>> page = ok.douyin.videos.search(query="露营装备", max_price="0.05")
|
|
42
|
+
>>> page.items[0]["text"], page.cost
|
|
43
|
+
|
|
44
|
+
Every paid call carries an Idempotency-Key, reused by its retries, so a retry is never charged
|
|
45
|
+
twice. Failed calls are not charged.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
api_key: Optional[str] = None,
|
|
51
|
+
*,
|
|
52
|
+
base_url: Optional[str] = None,
|
|
53
|
+
timeout: float = 65.0,
|
|
54
|
+
max_retries: int = 2,
|
|
55
|
+
retry_base_delay: float = 0.5,
|
|
56
|
+
max_retry_delay: float = 20.0,
|
|
57
|
+
entry: str = "sdk",
|
|
58
|
+
source: Optional[str] = None,
|
|
59
|
+
end_user: Optional[str] = None,
|
|
60
|
+
user_agent: Optional[str] = None,
|
|
61
|
+
http_client: Optional[httpx.Client] = None,
|
|
62
|
+
sleep: Optional[Callable[[float], None]] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self._cfg = make_config(api_key, base_url, timeout, max_retries, retry_base_delay, max_retry_delay, entry, source, end_user, user_agent)
|
|
65
|
+
self.base_url = self._cfg.base_url
|
|
66
|
+
self._owns_http = http_client is None
|
|
67
|
+
self._http = http_client or httpx.Client()
|
|
68
|
+
self._sleep: Callable[[float], None] = sleep or time.sleep
|
|
69
|
+
self.calls = _Calls(self)
|
|
70
|
+
self._init_endpoints(self)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def has_key(self) -> bool:
|
|
74
|
+
"""Whether a key is set; the key itself is never shown."""
|
|
75
|
+
return self._cfg.api_key is not None
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"OkData(base_url={self.base_url!r}, key={'set' if self.has_key else 'missing'})"
|
|
79
|
+
|
|
80
|
+
def __enter__(self) -> "OkData":
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def __exit__(self, *exc: object) -> None:
|
|
84
|
+
self.close()
|
|
85
|
+
|
|
86
|
+
def close(self) -> None:
|
|
87
|
+
if self._owns_http:
|
|
88
|
+
self._http.close()
|
|
89
|
+
|
|
90
|
+
def _call(self, endpoint_id: str, params: Mapping[str, Any], *, idempotency_key: Optional[str] = None, timeout: Optional[float] = None) -> Result:
|
|
91
|
+
req = call_request(endpoint_id, params, idempotency_key, timeout)
|
|
92
|
+
return to_result(send_sync(self._cfg, self._http, self._sleep, req), req.idempotency_key)
|
|
93
|
+
|
|
94
|
+
def call(self, endpoint_id: str, params: Optional[Mapping[str, Any]] = None, *, idempotency_key: Optional[str] = None,
|
|
95
|
+
timeout: Optional[float] = None, **kwargs: Any) -> Result:
|
|
96
|
+
"""Any endpoint by id: ``call("weibo.trends.list")``. Ids newer than this SDK work too (GET)."""
|
|
97
|
+
return self._call(endpoint_id, merge_params(params, kwargs), idempotency_key=idempotency_key, timeout=timeout)
|
|
98
|
+
|
|
99
|
+
def pages(self, endpoint_id: str, params: Optional[Mapping[str, Any]] = None, *, max_pages: Optional[int] = None,
|
|
100
|
+
max_spend: Optional[Price] = None, timeout: Optional[float] = None, **kwargs: Any) -> Pager[Dict[str, Any]]:
|
|
101
|
+
"""Pages of a list endpoint, following next_cursor, within max_pages / max_spend (US dollars).
|
|
102
|
+
|
|
103
|
+
Before each page the pager adds what it could cost at most and stops if that would pass
|
|
104
|
+
max_spend; each call also carries X-Max-Price = what is left, so the server refuses dearer ones.
|
|
105
|
+
"""
|
|
106
|
+
budget = _Budget(endpoint_id, endpoint(endpoint_id), merge_params(params, kwargs), max_pages, max_spend)
|
|
107
|
+
return Pager(budget, lambda p: self._call(endpoint_id, p, timeout=timeout))
|
|
108
|
+
|
|
109
|
+
def items(self, endpoint_id: str, params: Optional[Mapping[str, Any]] = None, *, max_items: Optional[int] = None,
|
|
110
|
+
max_pages: Optional[int] = None, max_spend: Optional[Price] = None, timeout: Optional[float] = None,
|
|
111
|
+
**kwargs: Any) -> ItemPager[Dict[str, Any]]:
|
|
112
|
+
"""The items of a list endpoint across pages, within max_items / max_pages / max_spend."""
|
|
113
|
+
return ItemPager(self.pages(endpoint_id, params, max_pages=max_pages, max_spend=max_spend, timeout=timeout, **kwargs), max_items)
|
|
114
|
+
|
|
115
|
+
def account(self) -> Dict[str, Any]:
|
|
116
|
+
"""Balance, this key with its spending limits, and the account's rate limits. Free."""
|
|
117
|
+
return self.request("GET", "/v1/account")
|
|
118
|
+
|
|
119
|
+
def catalog(self) -> Dict[str, Any]:
|
|
120
|
+
"""Every endpoint with its parameters and price. Free, no key needed."""
|
|
121
|
+
return self.request("GET", "/v1/catalog", auth=False)
|
|
122
|
+
|
|
123
|
+
def status(self) -> Dict[str, Any]:
|
|
124
|
+
"""Which endpoints are up right now. Free, no key needed."""
|
|
125
|
+
return self.request("GET", "/v1/catalog/status", auth=False)
|
|
126
|
+
|
|
127
|
+
def usage(self, *, group_by: Optional[Sequence[str]] = None, since: Optional[str] = None, until: Optional[str] = None) -> Dict[str, Any]:
|
|
128
|
+
"""Calls, settled calls and charges grouped as asked (30 days by default). Free."""
|
|
129
|
+
return self.request("GET", "/v1/usage", query=usage_query(group_by, since, until))
|
|
130
|
+
|
|
131
|
+
def request(self, method: str, path: str, *, query: Sequence[Any] = (), body: Optional[Dict[str, Any]] = None, auth: bool = True) -> Dict[str, Any]:
|
|
132
|
+
"""A raw request to any route; returns the envelope's data."""
|
|
133
|
+
envelope = send_sync(self._cfg, self._http, self._sleep, Send(method, path, tuple(query), body, None, None, auth))
|
|
134
|
+
data = envelope.get("data")
|
|
135
|
+
return data if isinstance(data, dict) else {}
|
okdata/_errors.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Errors the client raises. Every API error says whether it was charged and whether to retry;
|
|
2
|
+
branch on ``code`` (and ``reason``), never on ``message``."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Literal, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
RetryAdvice = Literal["no", "yes", "once", "after_fix", "same_key"]
|
|
8
|
+
"""no: fails the same way again. yes: retry. once: one more try. after_fix: change what problems says.
|
|
9
|
+
same_key: resend with the same Idempotency-Key (never charged twice)."""
|
|
10
|
+
|
|
11
|
+
RETRY_ADVICE = ("no", "yes", "once", "after_fix", "same_key")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _text(body: Mapping[str, Any], key: str) -> Optional[str]:
|
|
15
|
+
value = body.get(key)
|
|
16
|
+
return value if isinstance(value, str) else None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OkDataError(Exception):
|
|
20
|
+
"""An error answer from the API, or a call that got none.
|
|
21
|
+
|
|
22
|
+
``charged`` is False for almost every error, True for a not_found the endpoint charges (then
|
|
23
|
+
``amount`` says how much), and None when it is not known (a lost answer, settlement_unknown):
|
|
24
|
+
look the call up with ``ok.calls.get(request_id)`` or ``ok.calls.by_key(idempotency_key)``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
code: str,
|
|
30
|
+
message: str,
|
|
31
|
+
*,
|
|
32
|
+
status: Optional[int] = None,
|
|
33
|
+
request_id: Optional[str] = None,
|
|
34
|
+
charged: Optional[bool] = False,
|
|
35
|
+
retry: RetryAdvice = "no",
|
|
36
|
+
retry_after_seconds: Optional[float] = None,
|
|
37
|
+
idempotency_key: Optional[str] = None,
|
|
38
|
+
body: Optional[Mapping[str, Any]] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(f"{code}: {message}")
|
|
41
|
+
data: Mapping[str, Any] = body or {}
|
|
42
|
+
self.code = code
|
|
43
|
+
self.message = message
|
|
44
|
+
self.status = status
|
|
45
|
+
self.request_id = request_id
|
|
46
|
+
self.charged = charged
|
|
47
|
+
self.retry: RetryAdvice = retry
|
|
48
|
+
self.retry_after_seconds = retry_after_seconds
|
|
49
|
+
self.idempotency_key = idempotency_key
|
|
50
|
+
self.body: Optional[Dict[str, Any]] = dict(body) if body is not None else None
|
|
51
|
+
self.message_zh = _text(data, "message_zh")
|
|
52
|
+
problems = data.get("problems")
|
|
53
|
+
self.problems: List[str] = [str(p) for p in problems] if isinstance(problems, list) else []
|
|
54
|
+
self.param = _text(data, "param")
|
|
55
|
+
self.reason = _text(data, "reason")
|
|
56
|
+
self.docs_url = _text(data, "docs_url")
|
|
57
|
+
self.amount = _text(data, "amount")
|
|
58
|
+
self.required = _text(data, "required")
|
|
59
|
+
self.available = _text(data, "available")
|
|
60
|
+
self.top_up_url = _text(data, "top_up_url")
|
|
61
|
+
self.next_step = _text(data, "next_step")
|
|
62
|
+
self.next_step_zh = _text(data, "next_step_zh")
|
|
63
|
+
|
|
64
|
+
def with_charged(self, charged: Optional[bool]) -> "OkDataError":
|
|
65
|
+
"""The same error with another charge state."""
|
|
66
|
+
return OkDataError(
|
|
67
|
+
self.code, self.message, status=self.status, request_id=self.request_id, charged=charged,
|
|
68
|
+
retry=self.retry, retry_after_seconds=self.retry_after_seconds,
|
|
69
|
+
idempotency_key=self.idempotency_key, body=self.body,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
return {
|
|
74
|
+
**(self.body or {}),
|
|
75
|
+
"code": self.code, "message": self.message, "status": self.status, "request_id": self.request_id,
|
|
76
|
+
"charged": self.charged, "retry": self.retry, "retry_after_seconds": self.retry_after_seconds,
|
|
77
|
+
"idempotency_key": self.idempotency_key,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
return f"OkDataError(code={self.code!r}, status={self.status!r}, charged={self.charged!r}, retry={self.retry!r}, request_id={self.request_id!r})"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OkDataConnectionError(OkDataError):
|
|
85
|
+
"""No answer came back (network failure or the client's own timeout); whether the call ran is unknown."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, message: str, code: Literal["connection_error", "client_timeout"], idempotency_key: Optional[str]) -> None:
|
|
88
|
+
super().__init__(code, message, charged=None, retry="same_key", idempotency_key=idempotency_key)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generated by scripts/generate.ts from @okdata/catalog. Do not edit by hand."""
|