tickflow 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.
- tickflow/__init__.py +136 -0
- tickflow/_base_client.py +353 -0
- tickflow/_exceptions.py +140 -0
- tickflow/_types.py +60 -0
- tickflow/client.py +264 -0
- tickflow/generated_model.py +261 -0
- tickflow/resources/__init__.py +20 -0
- tickflow/resources/_base.py +29 -0
- tickflow/resources/exchanges.py +116 -0
- tickflow/resources/klines.py +460 -0
- tickflow/resources/quotes.py +397 -0
- tickflow/resources/symbols.py +176 -0
- tickflow/resources/universes.py +138 -0
- tickflow-0.1.0.dist-info/METADATA +36 -0
- tickflow-0.1.0.dist-info/RECORD +17 -0
- tickflow-0.1.0.dist-info/WHEEL +5 -0
- tickflow-0.1.0.dist-info/top_level.txt +1 -0
tickflow/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""TickFlow Python SDK - Market Data API Client.
|
|
2
|
+
|
|
3
|
+
A high-quality Python client for TickFlow market data API, supporting
|
|
4
|
+
A-shares (China), US stocks, and Hong Kong stocks.
|
|
5
|
+
|
|
6
|
+
Quick Start
|
|
7
|
+
-----------
|
|
8
|
+
>>> from tickflow import TickFlow
|
|
9
|
+
>>>
|
|
10
|
+
>>> # Initialize client
|
|
11
|
+
>>> 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)
|
|
15
|
+
>>> 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
|
+
|
|
22
|
+
Async Usage
|
|
23
|
+
-----------
|
|
24
|
+
>>> import asyncio
|
|
25
|
+
>>> from tickflow import AsyncTickFlow
|
|
26
|
+
>>>
|
|
27
|
+
>>> async def main():
|
|
28
|
+
... async with AsyncTickFlow(api_key="your-api-key") as client:
|
|
29
|
+
... df = await client.klines.get("AAPL.US", as_dataframe=True)
|
|
30
|
+
... print(df.tail())
|
|
31
|
+
>>>
|
|
32
|
+
>>> 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
|
+
"""
|
|
39
|
+
|
|
40
|
+
from ._exceptions import (
|
|
41
|
+
APIError,
|
|
42
|
+
AuthenticationError,
|
|
43
|
+
BadRequestError,
|
|
44
|
+
ConnectionError,
|
|
45
|
+
InternalServerError,
|
|
46
|
+
NotFoundError,
|
|
47
|
+
PermissionError,
|
|
48
|
+
RateLimitError,
|
|
49
|
+
TickFlowError,
|
|
50
|
+
TimeoutError,
|
|
51
|
+
)
|
|
52
|
+
from ._types import NOT_GIVEN, NotGiven
|
|
53
|
+
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,
|
|
61
|
+
CompactKlineData,
|
|
62
|
+
ExchangeListResponse,
|
|
63
|
+
ExchangeSummary,
|
|
64
|
+
ExchangeSymbolsResponse,
|
|
65
|
+
HKQuoteExt,
|
|
66
|
+
HKSymbolExt,
|
|
67
|
+
Kline,
|
|
68
|
+
KlinesBatchResponse,
|
|
69
|
+
KlinesResponse,
|
|
70
|
+
Period,
|
|
71
|
+
Quote,
|
|
72
|
+
QuotesResponse,
|
|
73
|
+
Region,
|
|
74
|
+
SessionStatus,
|
|
75
|
+
SymbolMeta,
|
|
76
|
+
SymbolMetaResponse,
|
|
77
|
+
Universe,
|
|
78
|
+
UniverseDetail,
|
|
79
|
+
UniverseDetailResponse,
|
|
80
|
+
UniverseListResponse,
|
|
81
|
+
UniverseSummary,
|
|
82
|
+
USQuoteExt,
|
|
83
|
+
USSymbolExt,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
__version__ = "0.1.0"
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
# Main clients
|
|
90
|
+
"TickFlow",
|
|
91
|
+
"AsyncTickFlow",
|
|
92
|
+
# Exceptions
|
|
93
|
+
"TickFlowError",
|
|
94
|
+
"APIError",
|
|
95
|
+
"AuthenticationError",
|
|
96
|
+
"PermissionError",
|
|
97
|
+
"NotFoundError",
|
|
98
|
+
"BadRequestError",
|
|
99
|
+
"RateLimitError",
|
|
100
|
+
"InternalServerError",
|
|
101
|
+
"ConnectionError",
|
|
102
|
+
"TimeoutError",
|
|
103
|
+
# Sentinel
|
|
104
|
+
"NOT_GIVEN",
|
|
105
|
+
"NotGiven",
|
|
106
|
+
# Generated types
|
|
107
|
+
"Period",
|
|
108
|
+
"Region",
|
|
109
|
+
"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
|
+
]
|
tickflow/_base_client.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Base HTTP client implementation for sync and async operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Generic, Optional, TypeVar, Union
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._exceptions import ConnectionError, TimeoutError, raise_for_status
|
|
11
|
+
from ._types import NOT_GIVEN, Headers, NotGiven, Query, Timeout, strip_not_given
|
|
12
|
+
|
|
13
|
+
__all__ = ["SyncAPIClient", "AsyncAPIClient"]
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "https://api.tickflow.org"
|
|
16
|
+
DEFAULT_TIMEOUT = 30.0
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseClient:
|
|
22
|
+
"""Base class with shared configuration for API clients."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
api_key: Optional[str] = None,
|
|
27
|
+
base_url: Optional[str] = None,
|
|
28
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
29
|
+
default_headers: Optional[Headers] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.api_key = api_key or os.environ.get("TICKFLOW_API_KEY")
|
|
32
|
+
if not self.api_key:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"API key is required. Pass `api_key` or set TICKFLOW_API_KEY environment variable."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self.base_url = (
|
|
38
|
+
base_url or os.environ.get("TICKFLOW_BASE_URL") or DEFAULT_BASE_URL
|
|
39
|
+
).rstrip("/")
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
self._default_headers = dict(default_headers) if default_headers else {}
|
|
42
|
+
|
|
43
|
+
def _build_headers(self, extra_headers: Optional[Headers] = None) -> dict[str, str]:
|
|
44
|
+
"""Build request headers with authentication."""
|
|
45
|
+
headers = {
|
|
46
|
+
"x-api-key": self.api_key,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"Accept": "application/json",
|
|
49
|
+
**self._default_headers,
|
|
50
|
+
}
|
|
51
|
+
if extra_headers:
|
|
52
|
+
headers.update(extra_headers)
|
|
53
|
+
return headers
|
|
54
|
+
|
|
55
|
+
def _build_url(self, path: str) -> str:
|
|
56
|
+
"""Build full URL from path."""
|
|
57
|
+
return f"{self.base_url}{path}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SyncAPIClient(BaseClient):
|
|
61
|
+
"""Synchronous HTTP client for TickFlow API.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
api_key : str, optional
|
|
66
|
+
API key for authentication. If not provided, reads from TICKFLOW_API_KEY
|
|
67
|
+
environment variable.
|
|
68
|
+
base_url : str, optional
|
|
69
|
+
Base URL for the API. Defaults to https://api.tickflow.org.
|
|
70
|
+
timeout : float, optional
|
|
71
|
+
Request timeout in seconds. Defaults to 30.0.
|
|
72
|
+
default_headers : dict, optional
|
|
73
|
+
Default headers to include in all requests.
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> client = SyncAPIClient(api_key="your-api-key")
|
|
78
|
+
>>> response = client.get("/v1/exchanges")
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
api_key: Optional[str] = None,
|
|
84
|
+
base_url: Optional[str] = None,
|
|
85
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
86
|
+
default_headers: Optional[Headers] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
super().__init__(api_key, base_url, timeout, default_headers)
|
|
89
|
+
self._client = httpx.Client(timeout=timeout)
|
|
90
|
+
|
|
91
|
+
def __enter__(self) -> "SyncAPIClient":
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def __exit__(self, *args: Any) -> None:
|
|
95
|
+
self.close()
|
|
96
|
+
|
|
97
|
+
def close(self) -> None:
|
|
98
|
+
"""Close the underlying HTTP client."""
|
|
99
|
+
self._client.close()
|
|
100
|
+
|
|
101
|
+
def _request(
|
|
102
|
+
self,
|
|
103
|
+
method: str,
|
|
104
|
+
path: str,
|
|
105
|
+
*,
|
|
106
|
+
params: Optional[Query] = None,
|
|
107
|
+
json: Optional[dict[str, Any]] = None,
|
|
108
|
+
extra_headers: Optional[Headers] = None,
|
|
109
|
+
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
110
|
+
) -> Any:
|
|
111
|
+
"""Make an HTTP request and return the JSON response.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
method : str
|
|
116
|
+
HTTP method (GET, POST, etc.).
|
|
117
|
+
path : str
|
|
118
|
+
API endpoint path.
|
|
119
|
+
params : dict, optional
|
|
120
|
+
Query parameters.
|
|
121
|
+
json : dict, optional
|
|
122
|
+
JSON request body.
|
|
123
|
+
extra_headers : dict, optional
|
|
124
|
+
Additional headers for this request.
|
|
125
|
+
timeout : float, optional
|
|
126
|
+
Override timeout for this request.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
Any
|
|
131
|
+
Parsed JSON response.
|
|
132
|
+
|
|
133
|
+
Raises
|
|
134
|
+
------
|
|
135
|
+
APIError
|
|
136
|
+
If the API returns an error response.
|
|
137
|
+
ConnectionError
|
|
138
|
+
If there's a network connection issue.
|
|
139
|
+
TimeoutError
|
|
140
|
+
If the request times out.
|
|
141
|
+
"""
|
|
142
|
+
url = self._build_url(path)
|
|
143
|
+
headers = self._build_headers(extra_headers)
|
|
144
|
+
request_timeout = timeout if not isinstance(timeout, NotGiven) else self.timeout
|
|
145
|
+
|
|
146
|
+
# Filter out None values from params
|
|
147
|
+
if params:
|
|
148
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
149
|
+
|
|
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
|
|
174
|
+
|
|
175
|
+
def get(
|
|
176
|
+
self,
|
|
177
|
+
path: str,
|
|
178
|
+
*,
|
|
179
|
+
params: Optional[Query] = None,
|
|
180
|
+
extra_headers: Optional[Headers] = None,
|
|
181
|
+
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
182
|
+
) -> Any:
|
|
183
|
+
"""Make a GET request."""
|
|
184
|
+
return self._request(
|
|
185
|
+
"GET", path, params=params, extra_headers=extra_headers, timeout=timeout
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def post(
|
|
189
|
+
self,
|
|
190
|
+
path: str,
|
|
191
|
+
*,
|
|
192
|
+
json: Optional[dict[str, Any]] = None,
|
|
193
|
+
params: Optional[Query] = None,
|
|
194
|
+
extra_headers: Optional[Headers] = None,
|
|
195
|
+
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
196
|
+
) -> Any:
|
|
197
|
+
"""Make a POST request."""
|
|
198
|
+
return self._request(
|
|
199
|
+
"POST",
|
|
200
|
+
path,
|
|
201
|
+
json=json,
|
|
202
|
+
params=params,
|
|
203
|
+
extra_headers=extra_headers,
|
|
204
|
+
timeout=timeout,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class AsyncAPIClient(BaseClient):
|
|
209
|
+
"""Asynchronous HTTP client for TickFlow API.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
api_key : str, optional
|
|
214
|
+
API key for authentication. If not provided, reads from TICKFLOW_API_KEY
|
|
215
|
+
environment variable.
|
|
216
|
+
base_url : str, optional
|
|
217
|
+
Base URL for the API. Defaults to https://api.tickflow.org.
|
|
218
|
+
timeout : float, optional
|
|
219
|
+
Request timeout in seconds. Defaults to 30.0.
|
|
220
|
+
default_headers : dict, optional
|
|
221
|
+
Default headers to include in all requests.
|
|
222
|
+
|
|
223
|
+
Examples
|
|
224
|
+
--------
|
|
225
|
+
>>> async with AsyncAPIClient(api_key="your-api-key") as client:
|
|
226
|
+
... response = await client.get("/v1/exchanges")
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
api_key: Optional[str] = None,
|
|
232
|
+
base_url: Optional[str] = None,
|
|
233
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
234
|
+
default_headers: Optional[Headers] = None,
|
|
235
|
+
) -> None:
|
|
236
|
+
super().__init__(api_key, base_url, timeout, default_headers)
|
|
237
|
+
self._client = httpx.AsyncClient(timeout=timeout)
|
|
238
|
+
|
|
239
|
+
async def __aenter__(self) -> "AsyncAPIClient":
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
243
|
+
await self.close()
|
|
244
|
+
|
|
245
|
+
async def close(self) -> None:
|
|
246
|
+
"""Close the underlying HTTP client."""
|
|
247
|
+
await self._client.aclose()
|
|
248
|
+
|
|
249
|
+
async def _request(
|
|
250
|
+
self,
|
|
251
|
+
method: str,
|
|
252
|
+
path: str,
|
|
253
|
+
*,
|
|
254
|
+
params: Optional[Query] = None,
|
|
255
|
+
json: Optional[dict[str, Any]] = None,
|
|
256
|
+
extra_headers: Optional[Headers] = None,
|
|
257
|
+
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
258
|
+
) -> Any:
|
|
259
|
+
"""Make an async HTTP request and return the JSON response.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
method : str
|
|
264
|
+
HTTP method (GET, POST, etc.).
|
|
265
|
+
path : str
|
|
266
|
+
API endpoint path.
|
|
267
|
+
params : dict, optional
|
|
268
|
+
Query parameters.
|
|
269
|
+
json : dict, optional
|
|
270
|
+
JSON request body.
|
|
271
|
+
extra_headers : dict, optional
|
|
272
|
+
Additional headers for this request.
|
|
273
|
+
timeout : float, optional
|
|
274
|
+
Override timeout for this request.
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
Any
|
|
279
|
+
Parsed JSON response.
|
|
280
|
+
|
|
281
|
+
Raises
|
|
282
|
+
------
|
|
283
|
+
APIError
|
|
284
|
+
If the API returns an error response.
|
|
285
|
+
ConnectionError
|
|
286
|
+
If there's a network connection issue.
|
|
287
|
+
TimeoutError
|
|
288
|
+
If the request times out.
|
|
289
|
+
"""
|
|
290
|
+
url = self._build_url(path)
|
|
291
|
+
headers = self._build_headers(extra_headers)
|
|
292
|
+
request_timeout = timeout if not isinstance(timeout, NotGiven) else self.timeout
|
|
293
|
+
|
|
294
|
+
# Filter out None values from params
|
|
295
|
+
if params:
|
|
296
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
297
|
+
|
|
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
|
|
322
|
+
|
|
323
|
+
async def get(
|
|
324
|
+
self,
|
|
325
|
+
path: str,
|
|
326
|
+
*,
|
|
327
|
+
params: Optional[Query] = None,
|
|
328
|
+
extra_headers: Optional[Headers] = None,
|
|
329
|
+
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
330
|
+
) -> Any:
|
|
331
|
+
"""Make an async GET request."""
|
|
332
|
+
return await self._request(
|
|
333
|
+
"GET", path, params=params, extra_headers=extra_headers, timeout=timeout
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
async def post(
|
|
337
|
+
self,
|
|
338
|
+
path: str,
|
|
339
|
+
*,
|
|
340
|
+
json: Optional[dict[str, Any]] = None,
|
|
341
|
+
params: Optional[Query] = None,
|
|
342
|
+
extra_headers: Optional[Headers] = None,
|
|
343
|
+
timeout: Union[Timeout, NotGiven] = NOT_GIVEN,
|
|
344
|
+
) -> Any:
|
|
345
|
+
"""Make an async POST request."""
|
|
346
|
+
return await self._request(
|
|
347
|
+
"POST",
|
|
348
|
+
path,
|
|
349
|
+
json=json,
|
|
350
|
+
params=params,
|
|
351
|
+
extra_headers=extra_headers,
|
|
352
|
+
timeout=timeout,
|
|
353
|
+
)
|
tickflow/_exceptions.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Custom exceptions for the TickFlow SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TickFlowError(Exception):
|
|
9
|
+
"""Base exception for all TickFlow SDK errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str) -> None:
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.message = message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIError(TickFlowError):
|
|
17
|
+
"""Error returned by the TickFlow API.
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
message : str
|
|
22
|
+
Human-readable error message.
|
|
23
|
+
code : str
|
|
24
|
+
Error code returned by the API (e.g., "INVALID_PERIOD", "SYMBOL_NOT_FOUND").
|
|
25
|
+
status_code : int
|
|
26
|
+
HTTP status code.
|
|
27
|
+
details : Any, optional
|
|
28
|
+
Additional error details for debugging.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
message: str,
|
|
34
|
+
*,
|
|
35
|
+
code: str,
|
|
36
|
+
status_code: int,
|
|
37
|
+
details: Optional[Any] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
self.code = code
|
|
41
|
+
self.status_code = status_code
|
|
42
|
+
self.details = details
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
return (
|
|
46
|
+
f"{self.__class__.__name__}("
|
|
47
|
+
f"message={self.message!r}, "
|
|
48
|
+
f"code={self.code!r}, "
|
|
49
|
+
f"status_code={self.status_code})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AuthenticationError(APIError):
|
|
54
|
+
"""Authentication failed (401)."""
|
|
55
|
+
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PermissionError(APIError):
|
|
60
|
+
"""Permission denied (403)."""
|
|
61
|
+
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class NotFoundError(APIError):
|
|
66
|
+
"""Resource not found (404)."""
|
|
67
|
+
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BadRequestError(APIError):
|
|
72
|
+
"""Invalid request parameters (400)."""
|
|
73
|
+
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RateLimitError(APIError):
|
|
78
|
+
"""Rate limit exceeded (429)."""
|
|
79
|
+
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InternalServerError(APIError):
|
|
84
|
+
"""Server error (5xx)."""
|
|
85
|
+
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ConnectionError(TickFlowError):
|
|
90
|
+
"""Network connection error."""
|
|
91
|
+
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TimeoutError(TickFlowError):
|
|
96
|
+
"""Request timeout."""
|
|
97
|
+
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def raise_for_status(status_code: int, response_body: dict[str, Any]) -> None:
|
|
102
|
+
"""Raise an appropriate exception based on status code and response body.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
status_code : int
|
|
107
|
+
HTTP status code.
|
|
108
|
+
response_body : dict
|
|
109
|
+
Parsed JSON response body.
|
|
110
|
+
|
|
111
|
+
Raises
|
|
112
|
+
------
|
|
113
|
+
APIError
|
|
114
|
+
Appropriate subclass based on the status code.
|
|
115
|
+
"""
|
|
116
|
+
if status_code < 400:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
message = response_body.get("message", "Unknown error")
|
|
120
|
+
code = response_body.get("code", "UNKNOWN")
|
|
121
|
+
details = response_body.get("details")
|
|
122
|
+
|
|
123
|
+
error_cls: type[APIError]
|
|
124
|
+
|
|
125
|
+
if status_code == 400:
|
|
126
|
+
error_cls = BadRequestError
|
|
127
|
+
elif status_code == 401:
|
|
128
|
+
error_cls = AuthenticationError
|
|
129
|
+
elif status_code == 403:
|
|
130
|
+
error_cls = PermissionError
|
|
131
|
+
elif status_code == 404:
|
|
132
|
+
error_cls = NotFoundError
|
|
133
|
+
elif status_code == 429:
|
|
134
|
+
error_cls = RateLimitError
|
|
135
|
+
elif status_code >= 500:
|
|
136
|
+
error_cls = InternalServerError
|
|
137
|
+
else:
|
|
138
|
+
error_cls = APIError
|
|
139
|
+
|
|
140
|
+
raise error_cls(message, code=code, status_code=status_code, details=details)
|
tickflow/_types.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Custom types and sentinel values for the TickFlow SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Mapping, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"NOT_GIVEN",
|
|
12
|
+
"NotGiven",
|
|
13
|
+
"Headers",
|
|
14
|
+
"Query",
|
|
15
|
+
"Timeout",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _NotGiven:
|
|
20
|
+
"""Sentinel class for distinguishing omitted arguments from None.
|
|
21
|
+
|
|
22
|
+
This allows us to differentiate between:
|
|
23
|
+
- `param=None` (explicitly passing None)
|
|
24
|
+
- `param` not provided (using the default)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
def __bool__(self) -> bool:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return "NOT_GIVEN"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
NOT_GIVEN = _NotGiven()
|
|
37
|
+
|
|
38
|
+
# Type alias for the sentinel
|
|
39
|
+
NotGiven = _NotGiven
|
|
40
|
+
|
|
41
|
+
# Type aliases for common parameter types
|
|
42
|
+
Headers = Mapping[str, str]
|
|
43
|
+
Query = Mapping[str, Union[str, int, bool, None]]
|
|
44
|
+
Timeout = Union[float, None]
|
|
45
|
+
|
|
46
|
+
# Generic type for response models
|
|
47
|
+
T = TypeVar("T")
|
|
48
|
+
|
|
49
|
+
# Type for DataFrame or raw response
|
|
50
|
+
DataFrameType = TypeVar("DataFrameType", bound="pd.DataFrame")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_given(value: Any) -> bool:
|
|
54
|
+
"""Check if a value was explicitly provided (not NOT_GIVEN)."""
|
|
55
|
+
return not isinstance(value, _NotGiven)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def strip_not_given(params: dict[str, Any]) -> dict[str, Any]:
|
|
59
|
+
"""Remove NOT_GIVEN values from a dictionary."""
|
|
60
|
+
return {k: v for k, v in params.items() if is_given(v)}
|