tickflow 0.1.0__tar.gz
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-0.1.0/PKG-INFO +36 -0
- tickflow-0.1.0/README.md +0 -0
- tickflow-0.1.0/pyproject.toml +62 -0
- tickflow-0.1.0/setup.cfg +4 -0
- tickflow-0.1.0/tickflow/__init__.py +136 -0
- tickflow-0.1.0/tickflow/_base_client.py +353 -0
- tickflow-0.1.0/tickflow/_exceptions.py +140 -0
- tickflow-0.1.0/tickflow/_types.py +60 -0
- tickflow-0.1.0/tickflow/client.py +264 -0
- tickflow-0.1.0/tickflow/generated_model.py +261 -0
- tickflow-0.1.0/tickflow/resources/__init__.py +20 -0
- tickflow-0.1.0/tickflow/resources/_base.py +29 -0
- tickflow-0.1.0/tickflow/resources/exchanges.py +116 -0
- tickflow-0.1.0/tickflow/resources/klines.py +460 -0
- tickflow-0.1.0/tickflow/resources/quotes.py +397 -0
- tickflow-0.1.0/tickflow/resources/symbols.py +176 -0
- tickflow-0.1.0/tickflow/resources/universes.py +138 -0
- tickflow-0.1.0/tickflow.egg-info/PKG-INFO +36 -0
- tickflow-0.1.0/tickflow.egg-info/SOURCES.txt +20 -0
- tickflow-0.1.0/tickflow.egg-info/dependency_links.txt +1 -0
- tickflow-0.1.0/tickflow.egg-info/requires.txt +14 -0
- tickflow-0.1.0/tickflow.egg-info/top_level.txt +1 -0
tickflow-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tickflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TickFlow Python Client - High-performance market data API for A-shares, US stocks, and Hong Kong stocks
|
|
5
|
+
Author: TickFlow Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://tickflow.org
|
|
8
|
+
Project-URL: Documentation, https://docs.tickflow.org
|
|
9
|
+
Project-URL: Repository, https://github.com/tickflow/tickflow-python
|
|
10
|
+
Keywords: finance,market-data,stocks,trading,api-client,async
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: httpx>=0.28.1
|
|
27
|
+
Requires-Dist: pandas>=1.3.0
|
|
28
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
29
|
+
Provides-Extra: pandas
|
|
30
|
+
Requires-Dist: pandas>=1.3.0; extra == "pandas"
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: pandas>=1.3.0; extra == "all"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: datamodel-code-generator[http]>=0.45.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
tickflow-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tickflow"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "TickFlow Python Client - High-performance market data API for A-shares, US stocks, and Hong Kong stocks"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "TickFlow Team" }
|
|
10
|
+
]
|
|
11
|
+
keywords = ["finance", "market-data", "stocks", "trading", "api-client", "async"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.28.1",
|
|
30
|
+
"pandas>=1.3.0",
|
|
31
|
+
"typing-extensions>=4.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
pandas = ["pandas>=1.3.0"]
|
|
36
|
+
all = ["pandas>=1.3.0"]
|
|
37
|
+
dev = [
|
|
38
|
+
"datamodel-code-generator[http]>=0.45.0",
|
|
39
|
+
"pytest>=7.0.0",
|
|
40
|
+
"pytest-asyncio>=0.21.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://tickflow.org"
|
|
45
|
+
Documentation = "https://docs.tickflow.org"
|
|
46
|
+
Repository = "https://github.com/tickflow/tickflow-python"
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
50
|
+
build-backend = "setuptools.build_meta"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["."]
|
|
54
|
+
include = ["tickflow*"]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
tickflow = ["py.typed"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
asyncio_mode = "auto"
|
|
62
|
+
testpaths = ["tests"]
|
tickflow-0.1.0/setup.cfg
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
|
+
)
|