tickerdb 0.1.1__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.
- tickerdb/__init__.py +64 -0
- tickerdb/async_client.py +502 -0
- tickerdb/client.py +500 -0
- tickerdb/exceptions.py +53 -0
- tickerdb/py.typed +0 -0
- tickerdb/types.py +140 -0
- tickerdb-0.1.1.dist-info/METADATA +216 -0
- tickerdb-0.1.1.dist-info/RECORD +9 -0
- tickerdb-0.1.1.dist-info/WHEEL +4 -0
tickerdb/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""TickerDB Python SDK - Financial data at your fingertips.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from tickerdb import TickerDB
|
|
6
|
+
|
|
7
|
+
client = TickerDB("your_api_key")
|
|
8
|
+
result = client.summary("AAPL")
|
|
9
|
+
|
|
10
|
+
For async usage::
|
|
11
|
+
|
|
12
|
+
from tickerdb import AsyncTickerDB
|
|
13
|
+
|
|
14
|
+
async with AsyncTickerDB("your_api_key") as client:
|
|
15
|
+
result = await client.summary("AAPL")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .async_client import AsyncSearchQuery, AsyncTickerDB
|
|
19
|
+
from .client import SearchQuery, TickerDB
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
DataUnavailableError,
|
|
23
|
+
ForbiddenError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
RateLimitError,
|
|
26
|
+
TickerDBError,
|
|
27
|
+
)
|
|
28
|
+
from .types import (
|
|
29
|
+
APIResponse,
|
|
30
|
+
BandMeta,
|
|
31
|
+
Event,
|
|
32
|
+
EventsContext,
|
|
33
|
+
EventsResponse,
|
|
34
|
+
RateLimits,
|
|
35
|
+
SchemaResponse,
|
|
36
|
+
SearchParams,
|
|
37
|
+
SearchResponse,
|
|
38
|
+
Stability,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"TickerDB",
|
|
43
|
+
"AsyncTickerDB",
|
|
44
|
+
"SearchQuery",
|
|
45
|
+
"AsyncSearchQuery",
|
|
46
|
+
"TickerDBError",
|
|
47
|
+
"AuthenticationError",
|
|
48
|
+
"ForbiddenError",
|
|
49
|
+
"NotFoundError",
|
|
50
|
+
"RateLimitError",
|
|
51
|
+
"DataUnavailableError",
|
|
52
|
+
"APIResponse",
|
|
53
|
+
"BandMeta",
|
|
54
|
+
"Event",
|
|
55
|
+
"EventsContext",
|
|
56
|
+
"EventsResponse",
|
|
57
|
+
"RateLimits",
|
|
58
|
+
"SchemaResponse",
|
|
59
|
+
"SearchParams",
|
|
60
|
+
"SearchResponse",
|
|
61
|
+
"Stability",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
__version__ = "0.1.0"
|
tickerdb/async_client.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Asynchronous TickerDB client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
DataUnavailableError,
|
|
10
|
+
ForbiddenError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
TickerDBError,
|
|
14
|
+
)
|
|
15
|
+
from .types import RateLimits
|
|
16
|
+
|
|
17
|
+
_DEFAULT_BASE_URL = "https://api.tickerdb.com/v1"
|
|
18
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_rate_limits(headers: httpx.Headers) -> RateLimits:
|
|
22
|
+
"""Extract rate-limit information from response headers."""
|
|
23
|
+
|
|
24
|
+
def _int_or_none(key: str) -> Optional[int]:
|
|
25
|
+
val = headers.get(key)
|
|
26
|
+
if val is None:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
return int(val)
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return RateLimits(
|
|
34
|
+
request_limit=_int_or_none("X-Request-Limit"),
|
|
35
|
+
requests_used=_int_or_none("X-Requests-Used"),
|
|
36
|
+
requests_remaining=_int_or_none("X-Requests-Remaining"),
|
|
37
|
+
request_reset=headers.get("X-Request-Reset"),
|
|
38
|
+
hourly_request_limit=_int_or_none("X-Hourly-Request-Limit"),
|
|
39
|
+
hourly_requests_used=_int_or_none("X-Hourly-Requests-Used"),
|
|
40
|
+
hourly_requests_remaining=_int_or_none("X-Hourly-Requests-Remaining"),
|
|
41
|
+
hourly_request_reset=headers.get("X-Hourly-Request-Reset"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
46
|
+
"""Raise a typed TickerDBError if the response indicates an error."""
|
|
47
|
+
if response.status_code < 400:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
body = response.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
raise TickerDBError(
|
|
54
|
+
status_code=response.status_code,
|
|
55
|
+
error_type="unknown_error",
|
|
56
|
+
message=response.text or "Unknown error",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
error = body.get("error", {})
|
|
60
|
+
error_type = error.get("type", "unknown_error")
|
|
61
|
+
message = error.get("message", "Unknown error")
|
|
62
|
+
upgrade_url = error.get("upgrade_url")
|
|
63
|
+
reset = error.get("reset")
|
|
64
|
+
|
|
65
|
+
kwargs: Dict[str, Any] = dict(
|
|
66
|
+
status_code=response.status_code,
|
|
67
|
+
error_type=error_type,
|
|
68
|
+
message=message,
|
|
69
|
+
upgrade_url=upgrade_url,
|
|
70
|
+
reset=reset,
|
|
71
|
+
raw=body,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
error_cls = {
|
|
75
|
+
401: AuthenticationError,
|
|
76
|
+
403: ForbiddenError,
|
|
77
|
+
404: NotFoundError,
|
|
78
|
+
429: RateLimitError,
|
|
79
|
+
503: DataUnavailableError,
|
|
80
|
+
}.get(response.status_code, TickerDBError)
|
|
81
|
+
|
|
82
|
+
raise error_cls(**kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncSearchQuery:
|
|
86
|
+
"""Fluent query builder for the async search endpoint.
|
|
87
|
+
|
|
88
|
+
Usage::
|
|
89
|
+
|
|
90
|
+
results = await client.query() \\
|
|
91
|
+
.eq("momentum_rsi_zone", "oversold") \\
|
|
92
|
+
.eq("sector", "Technology") \\
|
|
93
|
+
.select("ticker", "sector", "momentum_rsi_zone") \\
|
|
94
|
+
.sort("extremes_condition_percentile", "asc") \\
|
|
95
|
+
.limit(10) \\
|
|
96
|
+
.execute()
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, client: "AsyncTickerDB") -> None:
|
|
100
|
+
self._client = client
|
|
101
|
+
self._filters: list = []
|
|
102
|
+
self._fields: Optional[List[str]] = None
|
|
103
|
+
self._sort_by: Optional[str] = None
|
|
104
|
+
self._sort_direction: Optional[str] = None
|
|
105
|
+
self._limit: Optional[int] = None
|
|
106
|
+
self._offset: Optional[int] = None
|
|
107
|
+
self._timeframe: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
def eq(self, field: str, value: Any) -> "AsyncSearchQuery":
|
|
110
|
+
self._filters.append({"field": field, "op": "eq", "value": value})
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def neq(self, field: str, value: Any) -> "AsyncSearchQuery":
|
|
114
|
+
self._filters.append({"field": field, "op": "neq", "value": value})
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def in_(self, field: str, values: list) -> "AsyncSearchQuery":
|
|
118
|
+
self._filters.append({"field": field, "op": "in", "value": values})
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def gt(self, field: str, value: Any) -> "AsyncSearchQuery":
|
|
122
|
+
self._filters.append({"field": field, "op": "gt", "value": value})
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def gte(self, field: str, value: Any) -> "AsyncSearchQuery":
|
|
126
|
+
self._filters.append({"field": field, "op": "gte", "value": value})
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def lt(self, field: str, value: Any) -> "AsyncSearchQuery":
|
|
130
|
+
self._filters.append({"field": field, "op": "lt", "value": value})
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def lte(self, field: str, value: Any) -> "AsyncSearchQuery":
|
|
134
|
+
self._filters.append({"field": field, "op": "lte", "value": value})
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def select(self, *fields: str) -> "AsyncSearchQuery":
|
|
138
|
+
self._fields = list(fields)
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def sort(self, field: str, direction: str = "desc") -> "AsyncSearchQuery":
|
|
142
|
+
self._sort_by = field
|
|
143
|
+
self._sort_direction = direction
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def limit(self, n: int) -> "AsyncSearchQuery":
|
|
147
|
+
self._limit = n
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def offset(self, n: int) -> "AsyncSearchQuery":
|
|
151
|
+
self._offset = n
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def timeframe(self, tf: str) -> "AsyncSearchQuery":
|
|
155
|
+
self._timeframe = tf
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
async def execute(self) -> Dict[str, Any]:
|
|
159
|
+
"""Execute the built query and return results."""
|
|
160
|
+
return await self._client.search(
|
|
161
|
+
filters=self._filters,
|
|
162
|
+
fields=self._fields,
|
|
163
|
+
sort_by=self._sort_by,
|
|
164
|
+
sort_direction=self._sort_direction,
|
|
165
|
+
limit=self._limit,
|
|
166
|
+
offset=self._offset,
|
|
167
|
+
timeframe=self._timeframe,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class AsyncTickerDB:
|
|
172
|
+
"""Asynchronous client for the TickerDB financial data API.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
api_key: Your TickerDB bearer token.
|
|
176
|
+
base_url: Override the default API base URL.
|
|
177
|
+
timeout: Request timeout in seconds (default 30).
|
|
178
|
+
**httpx_kwargs: Additional keyword arguments forwarded to ``httpx.AsyncClient``.
|
|
179
|
+
|
|
180
|
+
Usage::
|
|
181
|
+
|
|
182
|
+
import asyncio
|
|
183
|
+
from tickerdb import AsyncTickerDB
|
|
184
|
+
|
|
185
|
+
async def main():
|
|
186
|
+
async with AsyncTickerDB("your_api_key") as client:
|
|
187
|
+
result = await client.summary("AAPL")
|
|
188
|
+
print(result["data"])
|
|
189
|
+
|
|
190
|
+
asyncio.run(main())
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
api_key: str,
|
|
196
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
197
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
198
|
+
**httpx_kwargs: Any,
|
|
199
|
+
) -> None:
|
|
200
|
+
self._base_url = base_url.rstrip("/")
|
|
201
|
+
self._client = httpx.AsyncClient(
|
|
202
|
+
headers={
|
|
203
|
+
"Authorization": f"Bearer {api_key}",
|
|
204
|
+
"Accept": "application/json",
|
|
205
|
+
"User-Agent": "tickerdb-python/0.1.0",
|
|
206
|
+
},
|
|
207
|
+
timeout=timeout,
|
|
208
|
+
**httpx_kwargs,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Internal helpers
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
async def _request(
|
|
216
|
+
self,
|
|
217
|
+
method: str,
|
|
218
|
+
path: str,
|
|
219
|
+
*,
|
|
220
|
+
params: Optional[Dict[str, Any]] = None,
|
|
221
|
+
json: Optional[Dict[str, Any]] = None,
|
|
222
|
+
) -> Dict[str, Any]:
|
|
223
|
+
"""Send a request and return parsed data + rate limits."""
|
|
224
|
+
url = f"{self._base_url}{path}"
|
|
225
|
+
|
|
226
|
+
if params:
|
|
227
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
228
|
+
|
|
229
|
+
response = await self._client.request(method, url, params=params, json=json)
|
|
230
|
+
_raise_for_status(response)
|
|
231
|
+
|
|
232
|
+
data = response.json()
|
|
233
|
+
rate_limits = _parse_rate_limits(response.headers)
|
|
234
|
+
|
|
235
|
+
return {"data": data, "rate_limits": rate_limits}
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Public API methods
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
async def summary(
|
|
242
|
+
self,
|
|
243
|
+
ticker: str,
|
|
244
|
+
*,
|
|
245
|
+
timeframe: Optional[str] = None,
|
|
246
|
+
date: Optional[str] = None,
|
|
247
|
+
start: Optional[str] = None,
|
|
248
|
+
end: Optional[str] = None,
|
|
249
|
+
field: Optional[str] = None,
|
|
250
|
+
band: Optional[str] = None,
|
|
251
|
+
limit: Optional[int] = None,
|
|
252
|
+
before: Optional[str] = None,
|
|
253
|
+
after: Optional[str] = None,
|
|
254
|
+
context_ticker: Optional[str] = None,
|
|
255
|
+
context_field: Optional[str] = None,
|
|
256
|
+
context_band: Optional[str] = None,
|
|
257
|
+
) -> Dict[str, Any]:
|
|
258
|
+
"""Get a summary for a single ticker.
|
|
259
|
+
|
|
260
|
+
Supports 4 modes depending on which parameters are provided:
|
|
261
|
+
|
|
262
|
+
- **Snapshot** (default): Current categorical state.
|
|
263
|
+
- **Historical snapshot**: Pass ``date`` for a point-in-time snapshot.
|
|
264
|
+
- **Historical series**: Pass ``start``/``end`` for a date range.
|
|
265
|
+
- **Events**: Pass ``field`` (and optionally ``band``) for band
|
|
266
|
+
transition history with aftermath data.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
ticker: Asset ticker symbol (e.g. ``"AAPL"``).
|
|
270
|
+
timeframe: ``"daily"`` or ``"weekly"`` (default ``"daily"``).
|
|
271
|
+
date: ISO 8601 date string (``YYYY-MM-DD``) for point-in-time.
|
|
272
|
+
start: Range start date (``YYYY-MM-DD``). Use with ``end``.
|
|
273
|
+
end: Range end date (``YYYY-MM-DD``). Use with ``start``.
|
|
274
|
+
field: Band field name for event queries (e.g. ``"rsi_zone"``).
|
|
275
|
+
band: Filter to a specific band value (e.g. ``"deep_oversold"``).
|
|
276
|
+
limit: Max event results (1-100). Only used with ``field``.
|
|
277
|
+
before: Return events before this date (``YYYY-MM-DD``).
|
|
278
|
+
after: Return events after this date (``YYYY-MM-DD``).
|
|
279
|
+
context_ticker: Cross-asset correlation ticker (e.g. ``"SPY"``).
|
|
280
|
+
context_field: Band field on context ticker.
|
|
281
|
+
context_band: Required band on context ticker.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
285
|
+
"""
|
|
286
|
+
return await self._request(
|
|
287
|
+
"GET",
|
|
288
|
+
f"/summary/{ticker}",
|
|
289
|
+
params={
|
|
290
|
+
"timeframe": timeframe,
|
|
291
|
+
"date": date,
|
|
292
|
+
"start": start,
|
|
293
|
+
"end": end,
|
|
294
|
+
"field": field,
|
|
295
|
+
"band": band,
|
|
296
|
+
"limit": limit,
|
|
297
|
+
"before": before,
|
|
298
|
+
"after": after,
|
|
299
|
+
"context_ticker": context_ticker,
|
|
300
|
+
"context_field": context_field,
|
|
301
|
+
"context_band": context_band,
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
async def search(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
309
|
+
timeframe: Optional[str] = None,
|
|
310
|
+
limit: Optional[int] = None,
|
|
311
|
+
offset: Optional[int] = None,
|
|
312
|
+
fields: Optional[List[str]] = None,
|
|
313
|
+
sort_by: Optional[str] = None,
|
|
314
|
+
sort_direction: Optional[str] = None,
|
|
315
|
+
) -> Dict[str, Any]:
|
|
316
|
+
"""Search for assets matching filter criteria.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
filters: Dict of filter criteria.
|
|
320
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
321
|
+
limit: Max results to return.
|
|
322
|
+
offset: Pagination offset.
|
|
323
|
+
fields: List of column names to return (e.g.
|
|
324
|
+
``["ticker", "sector", "momentum_rsi_zone"]``).
|
|
325
|
+
Use ``["*"]`` for all 120+ fields. Default if omitted: ticker,
|
|
326
|
+
asset_class, sector, performance, trend_direction, momentum_rsi_zone,
|
|
327
|
+
extremes_condition, extremes_condition_rarity, volatility_regime,
|
|
328
|
+
volume_ratio_band, fundamentals_valuation_zone, range_position.
|
|
329
|
+
``ticker`` is always included.
|
|
330
|
+
sort_by: Column name to sort results by. Must be a valid field
|
|
331
|
+
from the schema.
|
|
332
|
+
sort_direction: ``"asc"`` or ``"desc"`` (default ``"desc"``).
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
336
|
+
"""
|
|
337
|
+
import json as _json
|
|
338
|
+
|
|
339
|
+
params: Dict[str, Any] = {
|
|
340
|
+
"timeframe": timeframe,
|
|
341
|
+
"limit": limit,
|
|
342
|
+
"offset": offset,
|
|
343
|
+
"sort_by": sort_by,
|
|
344
|
+
"sort_direction": sort_direction,
|
|
345
|
+
}
|
|
346
|
+
if filters is not None:
|
|
347
|
+
params["filters"] = _json.dumps(filters)
|
|
348
|
+
if fields is not None:
|
|
349
|
+
params["fields"] = _json.dumps(fields)
|
|
350
|
+
return await self._request("GET", "/search", params=params)
|
|
351
|
+
|
|
352
|
+
def query(self) -> AsyncSearchQuery:
|
|
353
|
+
"""Create a fluent query builder for the search endpoint.
|
|
354
|
+
|
|
355
|
+
Usage::
|
|
356
|
+
|
|
357
|
+
results = await client.query() \\
|
|
358
|
+
.eq("momentum_rsi_zone", "oversold") \\
|
|
359
|
+
.eq("sector", "Technology") \\
|
|
360
|
+
.select("ticker", "sector", "momentum_rsi_zone") \\
|
|
361
|
+
.sort("extremes_condition_percentile", "asc") \\
|
|
362
|
+
.limit(10) \\
|
|
363
|
+
.execute()
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
An :class:`AsyncSearchQuery` builder instance.
|
|
367
|
+
"""
|
|
368
|
+
return AsyncSearchQuery(self)
|
|
369
|
+
|
|
370
|
+
async def schema(self) -> Dict[str, Any]:
|
|
371
|
+
"""Get the schema of available fields and their valid band values.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
375
|
+
"""
|
|
376
|
+
return await self._request("GET", "/schema/fields")
|
|
377
|
+
|
|
378
|
+
async def watchlist(
|
|
379
|
+
self,
|
|
380
|
+
tickers: List[str],
|
|
381
|
+
*,
|
|
382
|
+
timeframe: Optional[str] = None,
|
|
383
|
+
) -> Dict[str, Any]:
|
|
384
|
+
"""Get watchlist data for multiple tickers.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
tickers: List of ticker symbols.
|
|
388
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
392
|
+
"""
|
|
393
|
+
body: Dict[str, Any] = {"tickers": tickers}
|
|
394
|
+
if timeframe is not None:
|
|
395
|
+
body["timeframe"] = timeframe
|
|
396
|
+
return await self._request("POST", "/watchlist", json=body)
|
|
397
|
+
|
|
398
|
+
async def watchlist_changes(
|
|
399
|
+
self,
|
|
400
|
+
*,
|
|
401
|
+
timeframe: Optional[str] = None,
|
|
402
|
+
) -> Dict[str, Any]:
|
|
403
|
+
"""Get field-level state changes for your saved watchlist tickers.
|
|
404
|
+
|
|
405
|
+
Returns structured diffs showing what changed since the last pipeline
|
|
406
|
+
run (day-over-day for daily, week-over-week for weekly). Available on
|
|
407
|
+
all tiers.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
414
|
+
"""
|
|
415
|
+
params: Dict[str, str] = {}
|
|
416
|
+
if timeframe is not None:
|
|
417
|
+
params["timeframe"] = timeframe
|
|
418
|
+
return await self._request("GET", "/watchlist/changes", params=params)
|
|
419
|
+
|
|
420
|
+
# ------------------------------------------------------------------
|
|
421
|
+
# Webhook management
|
|
422
|
+
# ------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
async def list_webhooks(self) -> Dict[str, Any]:
|
|
425
|
+
"""List all webhooks for the current account.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
429
|
+
"""
|
|
430
|
+
return await self._request("GET", "/webhooks")
|
|
431
|
+
|
|
432
|
+
async def create_webhook(
|
|
433
|
+
self,
|
|
434
|
+
url: str,
|
|
435
|
+
events: Optional[Dict[str, bool]] = None,
|
|
436
|
+
) -> Dict[str, Any]:
|
|
437
|
+
"""Create a new webhook.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
url: The URL to receive webhook events.
|
|
441
|
+
events: Dict mapping event names to enabled booleans.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
445
|
+
"""
|
|
446
|
+
body: Dict[str, Any] = {"url": url}
|
|
447
|
+
if events is not None:
|
|
448
|
+
body["events"] = events
|
|
449
|
+
return await self._request("POST", "/webhooks", json=body)
|
|
450
|
+
|
|
451
|
+
async def update_webhook(
|
|
452
|
+
self,
|
|
453
|
+
id: str,
|
|
454
|
+
*,
|
|
455
|
+
url: Optional[str] = None,
|
|
456
|
+
events: Optional[Dict[str, bool]] = None,
|
|
457
|
+
active: Optional[bool] = None,
|
|
458
|
+
) -> Dict[str, Any]:
|
|
459
|
+
"""Update an existing webhook.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
id: The webhook ID.
|
|
463
|
+
url: New URL for the webhook.
|
|
464
|
+
events: Updated event subscriptions.
|
|
465
|
+
active: Whether the webhook is active.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
469
|
+
"""
|
|
470
|
+
body: Dict[str, Any] = {"id": id}
|
|
471
|
+
if url is not None:
|
|
472
|
+
body["url"] = url
|
|
473
|
+
if events is not None:
|
|
474
|
+
body["events"] = events
|
|
475
|
+
if active is not None:
|
|
476
|
+
body["active"] = active
|
|
477
|
+
return await self._request("PUT", "/webhooks", json=body)
|
|
478
|
+
|
|
479
|
+
async def delete_webhook(self, id: str) -> Dict[str, Any]:
|
|
480
|
+
"""Delete a webhook.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
id: The webhook ID to delete.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
487
|
+
"""
|
|
488
|
+
return await self._request("DELETE", "/webhooks", json={"id": id})
|
|
489
|
+
|
|
490
|
+
# ------------------------------------------------------------------
|
|
491
|
+
# Context manager support
|
|
492
|
+
# ------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
async def close(self) -> None:
|
|
495
|
+
"""Close the underlying HTTP client."""
|
|
496
|
+
await self._client.aclose()
|
|
497
|
+
|
|
498
|
+
async def __aenter__(self) -> "AsyncTickerDB":
|
|
499
|
+
return self
|
|
500
|
+
|
|
501
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
502
|
+
await self.close()
|