tickflow 0.1.0.dev0__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 CHANGED
@@ -1,40 +1,27 @@
1
- """TickFlow Python SDK - Market Data API Client.
1
+ """TickFlow Python SDK - 高性能行情数据客户端。
2
2
 
3
- A high-quality Python client for TickFlow market data API, supporting
4
- A-shares (China), US stocks, and Hong Kong stocks.
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
- Async Usage
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("AAPL.US", as_dataframe=True)
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
- ExchangeListResponse,
63
- ExchangeSummary,
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
- # Sentinel
104
- "NOT_GIVEN",
105
- "NotGiven",
106
- # Generated types
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
- from typing import Any, Generic, Optional, TypeVar, Union
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 ConnectionError, TimeoutError, raise_for_status
11
- from ._types import NOT_GIVEN, Headers, NotGiven, Query, Timeout, strip_not_given
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 and return the JSON response.
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
- try:
151
- response = self._client.request(
152
- method,
153
- url,
154
- params=params,
155
- json=json,
156
- headers=headers,
157
- timeout=request_timeout,
158
- )
159
- except httpx.ConnectError as e:
160
- raise ConnectionError(f"Failed to connect to {url}: {e}") from e
161
- except httpx.TimeoutException as e:
162
- raise TimeoutError(f"Request to {url} timed out") from e
163
-
164
- # Parse response
165
- try:
166
- response_body = response.json()
167
- except Exception:
168
- response_body = {"message": response.text, "code": "PARSE_ERROR"}
169
-
170
- # Check for errors
171
- raise_for_status(response.status_code, response_body)
172
-
173
- return response_body
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", path, params=params, extra_headers=extra_headers, timeout=timeout
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 and return the JSON response.
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
- try:
299
- response = await self._client.request(
300
- method,
301
- url,
302
- params=params,
303
- json=json,
304
- headers=headers,
305
- timeout=request_timeout,
306
- )
307
- except httpx.ConnectError as e:
308
- raise ConnectionError(f"Failed to connect to {url}: {e}") from e
309
- except httpx.TimeoutException as e:
310
- raise TimeoutError(f"Request to {url} timed out") from e
311
-
312
- # Parse response
313
- try:
314
- response_body = response.json()
315
- except Exception:
316
- response_body = {"message": response.text, "code": "PARSE_ERROR"}
317
-
318
- # Check for errors
319
- raise_for_status(response.status_code, response_body)
320
-
321
- return response_body
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", path, params=params, extra_headers=extra_headers, timeout=timeout
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
  )