tickflow 0.1.0__py3-none-any.whl → 0.1.0.dev1__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.
- tickflow/__init__.py +18 -79
- tickflow/_base_client.py +213 -70
- tickflow/client.py +20 -13
- tickflow/generated_model.py +86 -84
- tickflow/resources/__init__.py +3 -3
- tickflow/resources/exchanges.py +49 -23
- tickflow/resources/instruments.py +180 -0
- tickflow/resources/klines.py +263 -29
- tickflow-0.1.0.dev1.dist-info/METADATA +109 -0
- tickflow-0.1.0.dev1.dist-info/RECORD +17 -0
- tickflow/resources/symbols.py +0 -176
- tickflow-0.1.0.dist-info/METADATA +0 -36
- tickflow-0.1.0.dist-info/RECORD +0 -17
- {tickflow-0.1.0.dist-info → tickflow-0.1.0.dev1.dist-info}/WHEEL +0 -0
- {tickflow-0.1.0.dist-info → tickflow-0.1.0.dev1.dist-info}/top_level.txt +0 -0
tickflow/__init__.py
CHANGED
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
"""TickFlow Python SDK -
|
|
1
|
+
"""TickFlow Python SDK - 高性能行情数据客户端。
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
3
|
+
支持 A股、美股、港股的行情数据查询,提供同步和异步两种接口。
|
|
4
|
+
|
|
5
|
+
Examples
|
|
6
|
+
--------
|
|
7
|
+
同步使用:
|
|
5
8
|
|
|
6
|
-
Quick Start
|
|
7
|
-
-----------
|
|
8
9
|
>>> from tickflow import TickFlow
|
|
9
|
-
>>>
|
|
10
|
-
>>> # Initialize client
|
|
11
10
|
>>> client = TickFlow(api_key="your-api-key")
|
|
12
|
-
>>>
|
|
13
|
-
>>> # Get K-line data as pandas DataFrame
|
|
14
|
-
>>> df = client.klines.get("600000.SH", period="1d", count=100, as_dataframe=True)
|
|
11
|
+
>>> df = client.klines.get("600000.SH", as_dataframe=True)
|
|
15
12
|
>>> print(df.tail())
|
|
16
|
-
>>>
|
|
17
|
-
>>> # Get real-time quotes
|
|
18
|
-
>>> quotes = client.quotes.get(symbols=["600000.SH", "AAPL.US"])
|
|
19
|
-
>>> for q in quotes:
|
|
20
|
-
... print(f"{q['symbol']}: {q['last_price']}")
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
异步使用:
|
|
15
|
+
|
|
24
16
|
>>> import asyncio
|
|
25
17
|
>>> from tickflow import AsyncTickFlow
|
|
26
18
|
>>>
|
|
27
19
|
>>> async def main():
|
|
28
20
|
... async with AsyncTickFlow(api_key="your-api-key") as client:
|
|
29
|
-
... df = await client.klines.get("
|
|
21
|
+
... df = await client.klines.get("600000.SH", as_dataframe=True)
|
|
30
22
|
... print(df.tail())
|
|
31
23
|
>>>
|
|
32
24
|
>>> asyncio.run(main())
|
|
33
|
-
|
|
34
|
-
Environment Variables
|
|
35
|
-
---------------------
|
|
36
|
-
- TICKFLOW_API_KEY: API key for authentication
|
|
37
|
-
- TICKFLOW_BASE_URL: Custom base URL (optional)
|
|
38
25
|
"""
|
|
39
26
|
|
|
40
27
|
from ._exceptions import (
|
|
@@ -49,38 +36,15 @@ from ._exceptions import (
|
|
|
49
36
|
TickFlowError,
|
|
50
37
|
TimeoutError,
|
|
51
38
|
)
|
|
52
|
-
from ._types import NOT_GIVEN, NotGiven
|
|
53
39
|
from .client import AsyncTickFlow, TickFlow
|
|
54
|
-
|
|
55
|
-
# Re-export generated types for convenience
|
|
56
|
-
from .generated_model import ( # Core types; K-line types; Quote types; Symbol types; Exchange types; Universe types; Error types
|
|
57
|
-
ApiError,
|
|
58
|
-
BidAsk,
|
|
59
|
-
CNQuoteExt,
|
|
60
|
-
CNSymbolExt,
|
|
40
|
+
from .generated_model import (
|
|
61
41
|
CompactKlineData,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
ExchangeSymbolsResponse,
|
|
65
|
-
HKQuoteExt,
|
|
66
|
-
HKSymbolExt,
|
|
67
|
-
Kline,
|
|
68
|
-
KlinesBatchResponse,
|
|
69
|
-
KlinesResponse,
|
|
42
|
+
Instrument,
|
|
43
|
+
InstrumentType,
|
|
70
44
|
Period,
|
|
71
45
|
Quote,
|
|
72
|
-
QuotesResponse,
|
|
73
46
|
Region,
|
|
74
47
|
SessionStatus,
|
|
75
|
-
SymbolMeta,
|
|
76
|
-
SymbolMetaResponse,
|
|
77
|
-
Universe,
|
|
78
|
-
UniverseDetail,
|
|
79
|
-
UniverseDetailResponse,
|
|
80
|
-
UniverseListResponse,
|
|
81
|
-
UniverseSummary,
|
|
82
|
-
USQuoteExt,
|
|
83
|
-
USSymbolExt,
|
|
84
48
|
)
|
|
85
49
|
|
|
86
50
|
__version__ = "0.1.0"
|
|
@@ -100,37 +64,12 @@ __all__ = [
|
|
|
100
64
|
"InternalServerError",
|
|
101
65
|
"ConnectionError",
|
|
102
66
|
"TimeoutError",
|
|
103
|
-
#
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
|
|
67
|
+
# Types
|
|
68
|
+
"CompactKlineData",
|
|
69
|
+
"Instrument",
|
|
70
|
+
"InstrumentType",
|
|
107
71
|
"Period",
|
|
72
|
+
"Quote",
|
|
108
73
|
"Region",
|
|
109
74
|
"SessionStatus",
|
|
110
|
-
"CompactKlineData",
|
|
111
|
-
"Kline",
|
|
112
|
-
"KlinesResponse",
|
|
113
|
-
"KlinesBatchResponse",
|
|
114
|
-
"Quote",
|
|
115
|
-
"QuotesResponse",
|
|
116
|
-
"BidAsk",
|
|
117
|
-
"CNQuoteExt",
|
|
118
|
-
"USQuoteExt",
|
|
119
|
-
"HKQuoteExt",
|
|
120
|
-
"SymbolMeta",
|
|
121
|
-
"SymbolMetaResponse",
|
|
122
|
-
"CNSymbolExt",
|
|
123
|
-
"USSymbolExt",
|
|
124
|
-
"HKSymbolExt",
|
|
125
|
-
"ExchangeSummary",
|
|
126
|
-
"ExchangeListResponse",
|
|
127
|
-
"ExchangeSymbolsResponse",
|
|
128
|
-
"Universe",
|
|
129
|
-
"UniverseSummary",
|
|
130
|
-
"UniverseDetail",
|
|
131
|
-
"UniverseListResponse",
|
|
132
|
-
"UniverseDetailResponse",
|
|
133
|
-
"ApiError",
|
|
134
|
-
# Version
|
|
135
|
-
"__version__",
|
|
136
75
|
]
|
tickflow/_base_client.py
CHANGED
|
@@ -1,23 +1,86 @@
|
|
|
1
|
-
"""Base HTTP client implementation for sync and async operations."""
|
|
1
|
+
"""Base HTTP client implementation with retry support for sync and async operations."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import os
|
|
6
|
-
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Callable, Optional, TypeVar, Union
|
|
7
10
|
|
|
8
11
|
import httpx
|
|
9
12
|
|
|
10
|
-
from ._exceptions import
|
|
11
|
-
|
|
13
|
+
from ._exceptions import (
|
|
14
|
+
APIError,
|
|
15
|
+
ConnectionError,
|
|
16
|
+
InternalServerError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
TimeoutError,
|
|
19
|
+
raise_for_status,
|
|
20
|
+
)
|
|
21
|
+
from ._types import NOT_GIVEN, Headers, NotGiven, Query, Timeout
|
|
12
22
|
|
|
13
23
|
__all__ = ["SyncAPIClient", "AsyncAPIClient"]
|
|
14
24
|
|
|
15
25
|
DEFAULT_BASE_URL = "https://api.tickflow.org"
|
|
16
26
|
DEFAULT_TIMEOUT = 30.0
|
|
27
|
+
DEFAULT_MAX_RETRIES = 3
|
|
17
28
|
|
|
18
29
|
T = TypeVar("T")
|
|
19
30
|
|
|
20
31
|
|
|
32
|
+
def _should_retry(exception: Exception) -> bool:
|
|
33
|
+
"""Determine if an exception is retryable.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
exception : Exception
|
|
38
|
+
The exception to check.
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
bool
|
|
43
|
+
True if the request should be retried.
|
|
44
|
+
"""
|
|
45
|
+
# Retry on connection errors and timeouts
|
|
46
|
+
if isinstance(exception, (ConnectionError, TimeoutError)):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# Retry on server errors (5xx) and rate limits (429)
|
|
50
|
+
if isinstance(exception, (InternalServerError, RateLimitError)):
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _calculate_retry_delay(
|
|
57
|
+
attempt: int, base_delay: float = 1.0, max_delay: float = 30.0
|
|
58
|
+
) -> float:
|
|
59
|
+
"""Calculate exponential backoff delay with jitter.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
attempt : int
|
|
64
|
+
Current attempt number (0-indexed).
|
|
65
|
+
base_delay : float
|
|
66
|
+
Base delay in seconds.
|
|
67
|
+
max_delay : float
|
|
68
|
+
Maximum delay in seconds.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
float
|
|
73
|
+
Delay in seconds.
|
|
74
|
+
"""
|
|
75
|
+
# Exponential backoff: 1s, 2s, 4s, 8s, ...
|
|
76
|
+
delay = base_delay * (2**attempt)
|
|
77
|
+
# Add jitter (±25%)
|
|
78
|
+
jitter = delay * 0.25 * (2 * random.random() - 1)
|
|
79
|
+
delay = delay + jitter
|
|
80
|
+
# Cap at max delay
|
|
81
|
+
return min(delay, max_delay)
|
|
82
|
+
|
|
83
|
+
|
|
21
84
|
class BaseClient:
|
|
22
85
|
"""Base class with shared configuration for API clients."""
|
|
23
86
|
|
|
@@ -26,6 +89,7 @@ class BaseClient:
|
|
|
26
89
|
api_key: Optional[str] = None,
|
|
27
90
|
base_url: Optional[str] = None,
|
|
28
91
|
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
92
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
29
93
|
default_headers: Optional[Headers] = None,
|
|
30
94
|
) -> None:
|
|
31
95
|
self.api_key = api_key or os.environ.get("TICKFLOW_API_KEY")
|
|
@@ -38,6 +102,7 @@ class BaseClient:
|
|
|
38
102
|
base_url or os.environ.get("TICKFLOW_BASE_URL") or DEFAULT_BASE_URL
|
|
39
103
|
).rstrip("/")
|
|
40
104
|
self.timeout = timeout
|
|
105
|
+
self.max_retries = max_retries
|
|
41
106
|
self._default_headers = dict(default_headers) if default_headers else {}
|
|
42
107
|
|
|
43
108
|
def _build_headers(self, extra_headers: Optional[Headers] = None) -> dict[str, str]:
|
|
@@ -58,7 +123,7 @@ class BaseClient:
|
|
|
58
123
|
|
|
59
124
|
|
|
60
125
|
class SyncAPIClient(BaseClient):
|
|
61
|
-
"""Synchronous HTTP client for TickFlow API.
|
|
126
|
+
"""Synchronous HTTP client for TickFlow API with automatic retry.
|
|
62
127
|
|
|
63
128
|
Parameters
|
|
64
129
|
----------
|
|
@@ -69,6 +134,10 @@ class SyncAPIClient(BaseClient):
|
|
|
69
134
|
Base URL for the API. Defaults to https://api.tickflow.org.
|
|
70
135
|
timeout : float, optional
|
|
71
136
|
Request timeout in seconds. Defaults to 30.0.
|
|
137
|
+
max_retries : int, optional
|
|
138
|
+
Maximum number of retry attempts for failed requests. Defaults to 3.
|
|
139
|
+
Retries occur on connection errors, timeouts, server errors (5xx),
|
|
140
|
+
and rate limits (429).
|
|
72
141
|
default_headers : dict, optional
|
|
73
142
|
Default headers to include in all requests.
|
|
74
143
|
|
|
@@ -83,9 +152,10 @@ class SyncAPIClient(BaseClient):
|
|
|
83
152
|
api_key: Optional[str] = None,
|
|
84
153
|
base_url: Optional[str] = None,
|
|
85
154
|
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
155
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
86
156
|
default_headers: Optional[Headers] = None,
|
|
87
157
|
) -> None:
|
|
88
|
-
super().__init__(api_key, base_url, timeout, default_headers)
|
|
158
|
+
super().__init__(api_key, base_url, timeout, max_retries, default_headers)
|
|
89
159
|
self._client = httpx.Client(timeout=timeout)
|
|
90
160
|
|
|
91
161
|
def __enter__(self) -> "SyncAPIClient":
|
|
@@ -107,8 +177,9 @@ class SyncAPIClient(BaseClient):
|
|
|
107
177
|
json: Optional[dict[str, Any]] = None,
|
|
108
178
|
extra_headers: Optional[Headers] = None,
|
|
109
179
|
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
180
|
+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
|
|
110
181
|
) -> Any:
|
|
111
|
-
"""Make an HTTP request
|
|
182
|
+
"""Make an HTTP request with automatic retry on failures.
|
|
112
183
|
|
|
113
184
|
Parameters
|
|
114
185
|
----------
|
|
@@ -124,6 +195,8 @@ class SyncAPIClient(BaseClient):
|
|
|
124
195
|
Additional headers for this request.
|
|
125
196
|
timeout : float, optional
|
|
126
197
|
Override timeout for this request.
|
|
198
|
+
max_retries : int, optional
|
|
199
|
+
Override max retries for this request.
|
|
127
200
|
|
|
128
201
|
Returns
|
|
129
202
|
-------
|
|
@@ -133,44 +206,67 @@ class SyncAPIClient(BaseClient):
|
|
|
133
206
|
Raises
|
|
134
207
|
------
|
|
135
208
|
APIError
|
|
136
|
-
If the API returns an error response.
|
|
209
|
+
If the API returns an error response after all retries.
|
|
137
210
|
ConnectionError
|
|
138
|
-
If there's a network connection issue.
|
|
211
|
+
If there's a network connection issue after all retries.
|
|
139
212
|
TimeoutError
|
|
140
|
-
If the request times out.
|
|
213
|
+
If the request times out after all retries.
|
|
141
214
|
"""
|
|
142
215
|
url = self._build_url(path)
|
|
143
216
|
headers = self._build_headers(extra_headers)
|
|
144
217
|
request_timeout = timeout if not isinstance(timeout, NotGiven) else self.timeout
|
|
218
|
+
retries = (
|
|
219
|
+
max_retries if not isinstance(max_retries, NotGiven) else self.max_retries
|
|
220
|
+
)
|
|
145
221
|
|
|
146
222
|
# Filter out None values from params
|
|
147
223
|
if params:
|
|
148
224
|
params = {k: v for k, v in params.items() if v is not None}
|
|
149
225
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
226
|
+
last_exception: Optional[Exception] = None
|
|
227
|
+
|
|
228
|
+
for attempt in range(retries + 1):
|
|
229
|
+
try:
|
|
230
|
+
response = self._client.request(
|
|
231
|
+
method,
|
|
232
|
+
url,
|
|
233
|
+
params=params,
|
|
234
|
+
json=json,
|
|
235
|
+
headers=headers,
|
|
236
|
+
timeout=request_timeout,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Parse response
|
|
240
|
+
try:
|
|
241
|
+
response_body = response.json()
|
|
242
|
+
except Exception:
|
|
243
|
+
response_body = {"message": response.text, "code": "PARSE_ERROR"}
|
|
244
|
+
|
|
245
|
+
# Check for errors (may raise retryable exceptions)
|
|
246
|
+
raise_for_status(response.status_code, response_body)
|
|
247
|
+
|
|
248
|
+
return response_body
|
|
249
|
+
|
|
250
|
+
except httpx.ConnectError as e:
|
|
251
|
+
last_exception = ConnectionError(f"Failed to connect to {url}: {e}")
|
|
252
|
+
except httpx.TimeoutException as e:
|
|
253
|
+
last_exception = TimeoutError(f"Request to {url} timed out")
|
|
254
|
+
except APIError as e:
|
|
255
|
+
last_exception = e
|
|
256
|
+
if not _should_retry(e):
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
# Check if we should retry
|
|
260
|
+
if attempt < retries and _should_retry(last_exception):
|
|
261
|
+
delay = _calculate_retry_delay(attempt)
|
|
262
|
+
time.sleep(delay)
|
|
263
|
+
else:
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
# All retries exhausted
|
|
267
|
+
if last_exception:
|
|
268
|
+
raise last_exception
|
|
269
|
+
raise RuntimeError("Unexpected state: no exception but request failed")
|
|
174
270
|
|
|
175
271
|
def get(
|
|
176
272
|
self,
|
|
@@ -179,10 +275,16 @@ class SyncAPIClient(BaseClient):
|
|
|
179
275
|
params: Optional[Query] = None,
|
|
180
276
|
extra_headers: Optional[Headers] = None,
|
|
181
277
|
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
278
|
+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
|
|
182
279
|
) -> Any:
|
|
183
|
-
"""Make a GET request."""
|
|
280
|
+
"""Make a GET request with automatic retry."""
|
|
184
281
|
return self._request(
|
|
185
|
-
"GET",
|
|
282
|
+
"GET",
|
|
283
|
+
path,
|
|
284
|
+
params=params,
|
|
285
|
+
extra_headers=extra_headers,
|
|
286
|
+
timeout=timeout,
|
|
287
|
+
max_retries=max_retries,
|
|
186
288
|
)
|
|
187
289
|
|
|
188
290
|
def post(
|
|
@@ -193,8 +295,9 @@ class SyncAPIClient(BaseClient):
|
|
|
193
295
|
params: Optional[Query] = None,
|
|
194
296
|
extra_headers: Optional[Headers] = None,
|
|
195
297
|
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
298
|
+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
|
|
196
299
|
) -> Any:
|
|
197
|
-
"""Make a POST request."""
|
|
300
|
+
"""Make a POST request with automatic retry."""
|
|
198
301
|
return self._request(
|
|
199
302
|
"POST",
|
|
200
303
|
path,
|
|
@@ -202,11 +305,12 @@ class SyncAPIClient(BaseClient):
|
|
|
202
305
|
params=params,
|
|
203
306
|
extra_headers=extra_headers,
|
|
204
307
|
timeout=timeout,
|
|
308
|
+
max_retries=max_retries,
|
|
205
309
|
)
|
|
206
310
|
|
|
207
311
|
|
|
208
312
|
class AsyncAPIClient(BaseClient):
|
|
209
|
-
"""Asynchronous HTTP client for TickFlow API.
|
|
313
|
+
"""Asynchronous HTTP client for TickFlow API with automatic retry.
|
|
210
314
|
|
|
211
315
|
Parameters
|
|
212
316
|
----------
|
|
@@ -217,6 +321,10 @@ class AsyncAPIClient(BaseClient):
|
|
|
217
321
|
Base URL for the API. Defaults to https://api.tickflow.org.
|
|
218
322
|
timeout : float, optional
|
|
219
323
|
Request timeout in seconds. Defaults to 30.0.
|
|
324
|
+
max_retries : int, optional
|
|
325
|
+
Maximum number of retry attempts for failed requests. Defaults to 3.
|
|
326
|
+
Retries occur on connection errors, timeouts, server errors (5xx),
|
|
327
|
+
and rate limits (429).
|
|
220
328
|
default_headers : dict, optional
|
|
221
329
|
Default headers to include in all requests.
|
|
222
330
|
|
|
@@ -231,9 +339,10 @@ class AsyncAPIClient(BaseClient):
|
|
|
231
339
|
api_key: Optional[str] = None,
|
|
232
340
|
base_url: Optional[str] = None,
|
|
233
341
|
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
342
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
234
343
|
default_headers: Optional[Headers] = None,
|
|
235
344
|
) -> None:
|
|
236
|
-
super().__init__(api_key, base_url, timeout, default_headers)
|
|
345
|
+
super().__init__(api_key, base_url, timeout, max_retries, default_headers)
|
|
237
346
|
self._client = httpx.AsyncClient(timeout=timeout)
|
|
238
347
|
|
|
239
348
|
async def __aenter__(self) -> "AsyncAPIClient":
|
|
@@ -255,8 +364,9 @@ class AsyncAPIClient(BaseClient):
|
|
|
255
364
|
json: Optional[dict[str, Any]] = None,
|
|
256
365
|
extra_headers: Optional[Headers] = None,
|
|
257
366
|
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
367
|
+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
|
|
258
368
|
) -> Any:
|
|
259
|
-
"""Make an async HTTP request
|
|
369
|
+
"""Make an async HTTP request with automatic retry on failures.
|
|
260
370
|
|
|
261
371
|
Parameters
|
|
262
372
|
----------
|
|
@@ -272,6 +382,8 @@ class AsyncAPIClient(BaseClient):
|
|
|
272
382
|
Additional headers for this request.
|
|
273
383
|
timeout : float, optional
|
|
274
384
|
Override timeout for this request.
|
|
385
|
+
max_retries : int, optional
|
|
386
|
+
Override max retries for this request.
|
|
275
387
|
|
|
276
388
|
Returns
|
|
277
389
|
-------
|
|
@@ -281,44 +393,67 @@ class AsyncAPIClient(BaseClient):
|
|
|
281
393
|
Raises
|
|
282
394
|
------
|
|
283
395
|
APIError
|
|
284
|
-
If the API returns an error response.
|
|
396
|
+
If the API returns an error response after all retries.
|
|
285
397
|
ConnectionError
|
|
286
|
-
If there's a network connection issue.
|
|
398
|
+
If there's a network connection issue after all retries.
|
|
287
399
|
TimeoutError
|
|
288
|
-
If the request times out.
|
|
400
|
+
If the request times out after all retries.
|
|
289
401
|
"""
|
|
290
402
|
url = self._build_url(path)
|
|
291
403
|
headers = self._build_headers(extra_headers)
|
|
292
404
|
request_timeout = timeout if not isinstance(timeout, NotGiven) else self.timeout
|
|
405
|
+
retries = (
|
|
406
|
+
max_retries if not isinstance(max_retries, NotGiven) else self.max_retries
|
|
407
|
+
)
|
|
293
408
|
|
|
294
409
|
# Filter out None values from params
|
|
295
410
|
if params:
|
|
296
411
|
params = {k: v for k, v in params.items() if v is not None}
|
|
297
412
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
413
|
+
last_exception: Optional[Exception] = None
|
|
414
|
+
|
|
415
|
+
for attempt in range(retries + 1):
|
|
416
|
+
try:
|
|
417
|
+
response = await self._client.request(
|
|
418
|
+
method,
|
|
419
|
+
url,
|
|
420
|
+
params=params,
|
|
421
|
+
json=json,
|
|
422
|
+
headers=headers,
|
|
423
|
+
timeout=request_timeout,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Parse response
|
|
427
|
+
try:
|
|
428
|
+
response_body = response.json()
|
|
429
|
+
except Exception:
|
|
430
|
+
response_body = {"message": response.text, "code": "PARSE_ERROR"}
|
|
431
|
+
|
|
432
|
+
# Check for errors (may raise retryable exceptions)
|
|
433
|
+
raise_for_status(response.status_code, response_body)
|
|
434
|
+
|
|
435
|
+
return response_body
|
|
436
|
+
|
|
437
|
+
except httpx.ConnectError as e:
|
|
438
|
+
last_exception = ConnectionError(f"Failed to connect to {url}: {e}")
|
|
439
|
+
except httpx.TimeoutException as e:
|
|
440
|
+
last_exception = TimeoutError(f"Request to {url} timed out")
|
|
441
|
+
except APIError as e:
|
|
442
|
+
last_exception = e
|
|
443
|
+
if not _should_retry(e):
|
|
444
|
+
raise
|
|
445
|
+
|
|
446
|
+
# Check if we should retry
|
|
447
|
+
if attempt < retries and _should_retry(last_exception):
|
|
448
|
+
delay = _calculate_retry_delay(attempt)
|
|
449
|
+
await asyncio.sleep(delay)
|
|
450
|
+
else:
|
|
451
|
+
break
|
|
452
|
+
|
|
453
|
+
# All retries exhausted
|
|
454
|
+
if last_exception:
|
|
455
|
+
raise last_exception
|
|
456
|
+
raise RuntimeError("Unexpected state: no exception but request failed")
|
|
322
457
|
|
|
323
458
|
async def get(
|
|
324
459
|
self,
|
|
@@ -327,10 +462,16 @@ class AsyncAPIClient(BaseClient):
|
|
|
327
462
|
params: Optional[Query] = None,
|
|
328
463
|
extra_headers: Optional[Headers] = None,
|
|
329
464
|
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
465
|
+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
|
|
330
466
|
) -> Any:
|
|
331
|
-
"""Make an async GET request."""
|
|
467
|
+
"""Make an async GET request with automatic retry."""
|
|
332
468
|
return await self._request(
|
|
333
|
-
"GET",
|
|
469
|
+
"GET",
|
|
470
|
+
path,
|
|
471
|
+
params=params,
|
|
472
|
+
extra_headers=extra_headers,
|
|
473
|
+
timeout=timeout,
|
|
474
|
+
max_retries=max_retries,
|
|
334
475
|
)
|
|
335
476
|
|
|
336
477
|
async def post(
|
|
@@ -341,8 +482,9 @@ class AsyncAPIClient(BaseClient):
|
|
|
341
482
|
params: Optional[Query] = None,
|
|
342
483
|
extra_headers: Optional[Headers] = None,
|
|
343
484
|
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
485
|
+
max_retries: Union[int, NotGiven] = NOT_GIVEN,
|
|
344
486
|
) -> Any:
|
|
345
|
-
"""Make an async POST request."""
|
|
487
|
+
"""Make an async POST request with automatic retry."""
|
|
346
488
|
return await self._request(
|
|
347
489
|
"POST",
|
|
348
490
|
path,
|
|
@@ -350,4 +492,5 @@ class AsyncAPIClient(BaseClient):
|
|
|
350
492
|
params=params,
|
|
351
493
|
extra_headers=extra_headers,
|
|
352
494
|
timeout=timeout,
|
|
495
|
+
max_retries=max_retries,
|
|
353
496
|
)
|