tardis-dev 3.1.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.1.0 → tardis_dev-3.2.0}/PKG-INFO +2 -1
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/pyproject.toml +2 -1
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/_http.py +32 -7
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/replay.py +67 -29
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/LICENSE +0 -0
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/README.md +0 -0
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/__init__.py +0 -0
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/_options.py +0 -0
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/channel.py +0 -0
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/clear_cache.py +0 -0
- {tardis_dev-3.1.0 → tardis_dev-3.2.0}/tardis_dev/download_datasets.py +0 -0
- {tardis_dev-3.1.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
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -341,16 +369,14 @@ def _get_slice_cache_path(
|
|
|
341
369
|
filters: Optional[Sequence[Channel]],
|
|
342
370
|
*,
|
|
343
371
|
filters_hash: Optional[str] = None,
|
|
372
|
+
content_encoding: Optional[str] = None,
|
|
344
373
|
) -> str:
|
|
345
|
-
return (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
_format_date_to_path(date),
|
|
352
|
-
)
|
|
353
|
-
+ ".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'}",
|
|
354
380
|
)
|
|
355
381
|
|
|
356
382
|
|
|
@@ -405,6 +431,18 @@ def _remove_processed_slice(path: str) -> None:
|
|
|
405
431
|
os.remove(path)
|
|
406
432
|
|
|
407
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
|
+
|
|
408
446
|
def _clear_replay_cache_range(
|
|
409
447
|
*,
|
|
410
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
|
|
File without changes
|