tardis-dev 3.0.0__tar.gz → 3.2.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.
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/PKG-INFO +7 -4
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/README.md +5 -3
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/pyproject.toml +2 -1
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/_http.py +32 -7
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/replay.py +85 -39
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/LICENSE +0 -0
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/__init__.py +0 -0
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/_options.py +0 -0
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/channel.py +0 -0
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/clear_cache.py +0 -0
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/download_datasets.py +0 -0
- {tardis_dev-3.0.0 → tardis_dev-3.2.0}/tardis_dev/get_exchange_details.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tardis-dev
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Python client for tardis.dev - historical tick-level cryptocurrency market data replay API.
|
|
5
5
|
Home-page: https://github.com/tardis-dev/tardis-python
|
|
6
6
|
License: MPL-2.0
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
|
19
19
|
Requires-Dist: aiohttp (>=3.10.0)
|
|
20
20
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
21
|
+
Requires-Dist: zstandard (>=0.19.0)
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
22
23
|
|
|
23
24
|
# tardis-dev
|
|
@@ -28,7 +29,9 @@ Description-Content-Type: text/markdown
|
|
|
28
29
|
|
|
29
30
|
<br/>
|
|
30
31
|
|
|
31
|
-
Python `tardis-dev` package provides convenient access to tick-level historical cryptocurrency market data in exchange-native format. It focuses on two primary workflows: replaying historical market data and downloading CSV
|
|
32
|
+
Python `tardis-dev` package provides convenient access to tick-level historical cryptocurrency market data in exchange-native format. It focuses on two primary workflows: replaying historical market data and downloading historical market data as CSV files. If you need normalized replay or real-time streaming, use the Node.js client or Tardis Machine.
|
|
33
|
+
|
|
34
|
+
`replay()` accepts ISO date strings or Python `datetime` values. Naive datetimes are treated as UTC.
|
|
32
35
|
|
|
33
36
|
<br/>
|
|
34
37
|
|
|
@@ -59,7 +62,7 @@ asyncio.run(main())
|
|
|
59
62
|
|
|
60
63
|
<br/>
|
|
61
64
|
|
|
62
|
-
- [
|
|
65
|
+
- [historical market data downloads as CSV files](https://docs.tardis.dev/python-client/quickstart#csv-dataset-downloads)
|
|
63
66
|
|
|
64
67
|
<br/>
|
|
65
68
|
|
|
@@ -86,7 +89,7 @@ pip install tardis-dev
|
|
|
86
89
|
|
|
87
90
|
- [Quickstart](https://docs.tardis.dev/python-client/quickstart)
|
|
88
91
|
- [Replaying Historical Data](https://docs.tardis.dev/python-client/replaying-historical-data)
|
|
89
|
-
- [
|
|
92
|
+
- [CSV Dataset Downloads](https://docs.tardis.dev/python-client/quickstart#csv-dataset-downloads)
|
|
90
93
|
- [Migration Notice](https://docs.tardis.dev/python-client/migration-notice)
|
|
91
94
|
|
|
92
95
|
<br/>
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
<br/>
|
|
8
8
|
|
|
9
|
-
Python `tardis-dev` package provides convenient access to tick-level historical cryptocurrency market data in exchange-native format. It focuses on two primary workflows: replaying historical market data and downloading CSV
|
|
9
|
+
Python `tardis-dev` package provides convenient access to tick-level historical cryptocurrency market data in exchange-native format. It focuses on two primary workflows: replaying historical market data and downloading historical market data as CSV files. If you need normalized replay or real-time streaming, use the Node.js client or Tardis Machine.
|
|
10
|
+
|
|
11
|
+
`replay()` accepts ISO date strings or Python `datetime` values. Naive datetimes are treated as UTC.
|
|
10
12
|
|
|
11
13
|
<br/>
|
|
12
14
|
|
|
@@ -37,7 +39,7 @@ asyncio.run(main())
|
|
|
37
39
|
|
|
38
40
|
<br/>
|
|
39
41
|
|
|
40
|
-
- [
|
|
42
|
+
- [historical market data downloads as CSV files](https://docs.tardis.dev/python-client/quickstart#csv-dataset-downloads)
|
|
41
43
|
|
|
42
44
|
<br/>
|
|
43
45
|
|
|
@@ -64,7 +66,7 @@ pip install tardis-dev
|
|
|
64
66
|
|
|
65
67
|
- [Quickstart](https://docs.tardis.dev/python-client/quickstart)
|
|
66
68
|
- [Replaying Historical Data](https://docs.tardis.dev/python-client/replaying-historical-data)
|
|
67
|
-
- [
|
|
69
|
+
- [CSV Dataset Downloads](https://docs.tardis.dev/python-client/quickstart#csv-dataset-downloads)
|
|
68
70
|
- [Migration Notice](https://docs.tardis.dev/python-client/migration-notice)
|
|
69
71
|
|
|
70
72
|
<br/>
|
|
@@ -9,7 +9,7 @@ markers = [
|
|
|
9
9
|
|
|
10
10
|
[tool.poetry]
|
|
11
11
|
name = "tardis-dev"
|
|
12
|
-
version = "3.
|
|
12
|
+
version = "3.2.0"
|
|
13
13
|
description = "Python client for tardis.dev - historical tick-level cryptocurrency market data replay API."
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
homepage = "https://github.com/tardis-dev/tardis-python"
|
|
@@ -30,6 +30,7 @@ python = ">=3.9,<4.0"
|
|
|
30
30
|
aiohttp = ">=3.10.0"
|
|
31
31
|
python-dateutil = "^2.8.2"
|
|
32
32
|
aiofiles = "^24.1.0"
|
|
33
|
+
zstandard = ">=0.19.0"
|
|
33
34
|
|
|
34
35
|
[tool.poetry.group.dev.dependencies]
|
|
35
36
|
pytest = "^8.3.5"
|
|
@@ -14,11 +14,11 @@ import aiohttp
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
async def create_session(api_key: str, timeout: int) -> aiohttp.ClientSession:
|
|
17
|
+
async def create_session(api_key: str, timeout: int, accept_encoding: str = "gzip") -> aiohttp.ClientSession:
|
|
18
18
|
from tardis_dev import __version__
|
|
19
19
|
|
|
20
20
|
headers = {
|
|
21
|
-
"Accept-Encoding":
|
|
21
|
+
"Accept-Encoding": accept_encoding,
|
|
22
22
|
"User-Agent": f"tardis-dev/{__version__} (+https://github.com/tardis-dev/tardis-python)",
|
|
23
23
|
}
|
|
24
24
|
if api_key:
|
|
@@ -38,14 +38,20 @@ async def reliable_download(
|
|
|
38
38
|
dest_path: str,
|
|
39
39
|
http_proxy: Optional[str] = None,
|
|
40
40
|
max_attempts: int = 30,
|
|
41
|
-
|
|
41
|
+
append_content_encoding_extension: bool = False,
|
|
42
|
+
) -> str:
|
|
42
43
|
attempts = 0
|
|
43
44
|
|
|
44
45
|
while True:
|
|
45
46
|
attempts += 1
|
|
46
47
|
try:
|
|
47
|
-
await _download(
|
|
48
|
-
|
|
48
|
+
return await _download(
|
|
49
|
+
session,
|
|
50
|
+
_get_retry_url(url, attempts),
|
|
51
|
+
dest_path,
|
|
52
|
+
http_proxy,
|
|
53
|
+
append_content_encoding_extension=append_content_encoding_extension,
|
|
54
|
+
)
|
|
49
55
|
except asyncio.CancelledError:
|
|
50
56
|
raise
|
|
51
57
|
except Exception as exc:
|
|
@@ -99,12 +105,30 @@ async def _download(
|
|
|
99
105
|
url: str,
|
|
100
106
|
dest_path: str,
|
|
101
107
|
http_proxy: Optional[str],
|
|
102
|
-
|
|
108
|
+
*,
|
|
109
|
+
append_content_encoding_extension: bool,
|
|
110
|
+
) -> str:
|
|
103
111
|
async with session.get(url, proxy=http_proxy) as response:
|
|
104
112
|
if response.status != 200:
|
|
105
113
|
error_text = await response.text()
|
|
106
114
|
raise urllib.error.HTTPError(url, code=response.status, msg=error_text, hdrs=None, fp=None)
|
|
107
115
|
|
|
116
|
+
final_path = dest_path
|
|
117
|
+
if append_content_encoding_extension:
|
|
118
|
+
content_encoding = response.headers.get("Content-Encoding")
|
|
119
|
+
if content_encoding == "zstd":
|
|
120
|
+
final_path = f"{dest_path}.zst"
|
|
121
|
+
elif content_encoding == "gzip":
|
|
122
|
+
final_path = f"{dest_path}.gz"
|
|
123
|
+
else:
|
|
124
|
+
raise urllib.error.HTTPError(
|
|
125
|
+
url,
|
|
126
|
+
code=400,
|
|
127
|
+
msg=f"Unsupported data feed content encoding: {content_encoding}",
|
|
128
|
+
hdrs=None,
|
|
129
|
+
fp=None,
|
|
130
|
+
)
|
|
131
|
+
|
|
108
132
|
pathlib.Path(dest_path).parent.mkdir(parents=True, exist_ok=True)
|
|
109
133
|
temp_path = f"{dest_path}{secrets.token_hex(8)}.unconfirmed"
|
|
110
134
|
|
|
@@ -113,7 +137,8 @@ async def _download(
|
|
|
113
137
|
async for chunk in response.content.iter_any():
|
|
114
138
|
await temp_file.write(chunk)
|
|
115
139
|
|
|
116
|
-
os.replace(temp_path,
|
|
140
|
+
os.replace(temp_path, final_path)
|
|
141
|
+
return final_path
|
|
117
142
|
finally:
|
|
118
143
|
if os.path.exists(temp_path):
|
|
119
144
|
os.remove(temp_path)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import gzip
|
|
3
3
|
import hashlib
|
|
4
|
+
import io
|
|
4
5
|
import json as json_module
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
@@ -11,6 +12,7 @@ from time import time
|
|
|
11
12
|
from typing import Any, AsyncIterator, Dict, List, NamedTuple, Optional, Sequence, Union
|
|
12
13
|
|
|
13
14
|
import dateutil.parser
|
|
15
|
+
import zstandard
|
|
14
16
|
|
|
15
17
|
from tardis_dev._http import create_session, reliable_download
|
|
16
18
|
from tardis_dev._options import DEFAULT_CACHE_DIR, DEFAULT_ENDPOINT
|
|
@@ -29,8 +31,8 @@ class Response(NamedTuple):
|
|
|
29
31
|
|
|
30
32
|
async def replay(
|
|
31
33
|
exchange: str,
|
|
32
|
-
from_date: str,
|
|
33
|
-
to_date: str,
|
|
34
|
+
from_date: Union[str, datetime],
|
|
35
|
+
to_date: Union[str, datetime],
|
|
34
36
|
filters: Optional[Sequence[Channel]] = None,
|
|
35
37
|
*,
|
|
36
38
|
api_key: str = "",
|
|
@@ -84,7 +86,15 @@ async def replay(
|
|
|
84
86
|
|
|
85
87
|
while current_slice_path is None:
|
|
86
88
|
await asyncio.sleep(0)
|
|
87
|
-
|
|
89
|
+
zstd_path = _get_slice_cache_path(
|
|
90
|
+
cache_dir,
|
|
91
|
+
exchange,
|
|
92
|
+
current_slice_date,
|
|
93
|
+
normalized_filters,
|
|
94
|
+
filters_hash=filters_hash,
|
|
95
|
+
content_encoding="zstd",
|
|
96
|
+
)
|
|
97
|
+
gzip_path = _get_slice_cache_path(
|
|
88
98
|
cache_dir,
|
|
89
99
|
exchange,
|
|
90
100
|
current_slice_date,
|
|
@@ -95,26 +105,27 @@ async def replay(
|
|
|
95
105
|
if fetch_data_task.done() and fetch_data_task.exception():
|
|
96
106
|
raise fetch_data_task.exception()
|
|
97
107
|
|
|
98
|
-
if os.path.isfile(
|
|
99
|
-
current_slice_path =
|
|
108
|
+
if os.path.isfile(zstd_path):
|
|
109
|
+
current_slice_path = zstd_path
|
|
110
|
+
elif os.path.isfile(gzip_path):
|
|
111
|
+
current_slice_path = gzip_path
|
|
100
112
|
else:
|
|
101
113
|
await asyncio.sleep(0.1)
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
continue
|
|
115
|
+
for line in _iterate_slice_lines(current_slice_path):
|
|
116
|
+
if len(line) <= 1:
|
|
117
|
+
if with_disconnects and not last_message_was_disconnect:
|
|
118
|
+
last_message_was_disconnect = True
|
|
119
|
+
yield None
|
|
120
|
+
continue
|
|
110
121
|
|
|
111
|
-
|
|
122
|
+
last_message_was_disconnect = False
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
if decode_response:
|
|
125
|
+
timestamp = datetime.fromisoformat(line[0 : DATE_MESSAGE_SPLIT_INDEX - 2].decode("utf-8"))
|
|
126
|
+
yield Response(timestamp, json.loads(line[DATE_MESSAGE_SPLIT_INDEX + 1 :]))
|
|
127
|
+
else:
|
|
128
|
+
yield Response(line[0:DATE_MESSAGE_SPLIT_INDEX], line[DATE_MESSAGE_SPLIT_INDEX + 1 :])
|
|
118
129
|
|
|
119
130
|
if auto_cleanup:
|
|
120
131
|
_remove_processed_slice(current_slice_path)
|
|
@@ -167,7 +178,7 @@ async def _fetch_data_to_replay(
|
|
|
167
178
|
if minutes_diff <= 0:
|
|
168
179
|
return
|
|
169
180
|
|
|
170
|
-
async with await create_session(api_key, timeout) as session:
|
|
181
|
+
async with await create_session(api_key, timeout, "zstd, gzip") as session:
|
|
171
182
|
fetch_data_tasks = set()
|
|
172
183
|
try:
|
|
173
184
|
prefetch_offsets = [minutes_diff - 1]
|
|
@@ -231,9 +242,23 @@ async def _fetch_slice_if_not_cached(
|
|
|
231
242
|
filters_hash: str,
|
|
232
243
|
) -> None:
|
|
233
244
|
slice_date = from_date + timedelta(minutes=offset)
|
|
234
|
-
|
|
245
|
+
cache_zstd_path = _get_slice_cache_path(
|
|
246
|
+
cache_dir,
|
|
247
|
+
exchange,
|
|
248
|
+
slice_date,
|
|
249
|
+
filters,
|
|
250
|
+
filters_hash=filters_hash,
|
|
251
|
+
content_encoding="zstd",
|
|
252
|
+
)
|
|
253
|
+
cache_gzip_path = _get_slice_cache_path(
|
|
254
|
+
cache_dir,
|
|
255
|
+
exchange,
|
|
256
|
+
slice_date,
|
|
257
|
+
filters,
|
|
258
|
+
filters_hash=filters_hash,
|
|
259
|
+
)
|
|
235
260
|
|
|
236
|
-
if os.path.isfile(
|
|
261
|
+
if os.path.isfile(cache_zstd_path) or os.path.isfile(cache_gzip_path):
|
|
237
262
|
return
|
|
238
263
|
|
|
239
264
|
fetch_url = f"{endpoint}/data-feeds/{exchange}?from={_format_replay_query_date(from_date)}&offset={offset}"
|
|
@@ -242,11 +267,14 @@ async def _fetch_slice_if_not_cached(
|
|
|
242
267
|
filters_url_encoded = urllib.parse.quote(filters_serialized, safe="~()*!.'")
|
|
243
268
|
fetch_url += f"&filters={filters_url_encoded}"
|
|
244
269
|
|
|
270
|
+
cache_base_path = cache_gzip_path.removesuffix(".gz")
|
|
271
|
+
|
|
245
272
|
await reliable_download(
|
|
246
273
|
session=session,
|
|
247
274
|
url=fetch_url,
|
|
248
|
-
dest_path=
|
|
275
|
+
dest_path=cache_base_path,
|
|
249
276
|
http_proxy=http_proxy,
|
|
277
|
+
append_content_encoding_extension=True,
|
|
250
278
|
)
|
|
251
279
|
|
|
252
280
|
|
|
@@ -310,20 +338,28 @@ def _validate_replay_args(
|
|
|
310
338
|
raise ValueError(f"Invalid 'symbols[]' argument: {channel.symbols}. Please provide list of symbol strings.")
|
|
311
339
|
|
|
312
340
|
|
|
313
|
-
def _parse_date(name: str, value: str) -> datetime:
|
|
341
|
+
def _parse_date(name: str, value: Union[str, datetime]) -> datetime:
|
|
314
342
|
if value is None:
|
|
315
343
|
raise ValueError(
|
|
316
344
|
f"Invalid '{name}' argument: {value}. Please provide valid ISO date string. "
|
|
317
345
|
"https://docs.python.org/3/library/datetime.html#datetime.date.fromisoformat"
|
|
318
346
|
)
|
|
319
347
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
348
|
+
if isinstance(value, datetime):
|
|
349
|
+
parsed = value
|
|
350
|
+
else:
|
|
351
|
+
try:
|
|
352
|
+
parsed = dateutil.parser.isoparse(value)
|
|
353
|
+
except (TypeError, ValueError) as exc:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
f"Invalid '{name}' argument: {value}. Please provide valid ISO date string. "
|
|
356
|
+
"https://docs.python.org/3/library/datetime.html#datetime.date.fromisoformat"
|
|
357
|
+
) from exc
|
|
358
|
+
|
|
359
|
+
if parsed.tzinfo is None:
|
|
360
|
+
return parsed.replace(tzinfo=timezone.utc)
|
|
361
|
+
|
|
362
|
+
return parsed.astimezone(timezone.utc)
|
|
327
363
|
|
|
328
364
|
|
|
329
365
|
def _get_slice_cache_path(
|
|
@@ -333,16 +369,14 @@ def _get_slice_cache_path(
|
|
|
333
369
|
filters: Optional[Sequence[Channel]],
|
|
334
370
|
*,
|
|
335
371
|
filters_hash: Optional[str] = None,
|
|
372
|
+
content_encoding: Optional[str] = None,
|
|
336
373
|
) -> str:
|
|
337
|
-
return (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
_format_date_to_path(date),
|
|
344
|
-
)
|
|
345
|
-
+ ".json.gz"
|
|
374
|
+
return os.path.join(
|
|
375
|
+
cache_dir,
|
|
376
|
+
"feeds",
|
|
377
|
+
exchange,
|
|
378
|
+
filters_hash if filters_hash is not None else _get_filters_hash(filters),
|
|
379
|
+
f"{_format_date_to_path(date)}.json{'.zst' if content_encoding == 'zstd' else '.gz'}",
|
|
346
380
|
)
|
|
347
381
|
|
|
348
382
|
|
|
@@ -397,6 +431,18 @@ def _remove_processed_slice(path: str) -> None:
|
|
|
397
431
|
os.remove(path)
|
|
398
432
|
|
|
399
433
|
|
|
434
|
+
def _iterate_slice_lines(path: str):
|
|
435
|
+
if path.endswith(".zst"):
|
|
436
|
+
with open(path, "rb") as compressed_file:
|
|
437
|
+
with zstandard.ZstdDecompressor().stream_reader(compressed_file) as file:
|
|
438
|
+
with io.BufferedReader(file) as buffered_file:
|
|
439
|
+
yield from buffered_file
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
with gzip.open(path, "rb") as file:
|
|
443
|
+
yield from file
|
|
444
|
+
|
|
445
|
+
|
|
400
446
|
def _clear_replay_cache_range(
|
|
401
447
|
*,
|
|
402
448
|
cache_dir: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|