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 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
+ ]
@@ -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
+ )
@@ -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)}