dataquery-sdk 0.1.2__tar.gz → 0.1.3__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.
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/CHANGELOG.md +3 -1
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/PKG-INFO +1 -1
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/__init__.py +1 -1
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/dataquery.py +225 -6
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/PKG-INFO +1 -1
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/CONTRIBUTING.md +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/LICENSE +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/MANIFEST.in +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/README.md +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/auth.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/auto_download.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/cli.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/client.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/config.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/connection_pool.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/exceptions.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/logging_config.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/models.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/py.typed +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/rate_limiter.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/retry.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/utils.py +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/SOURCES.txt +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/entry_points.txt +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/requires.txt +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/top_level.txt +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/pyproject.toml +0 -0
- {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/setup.cfg +0 -0
|
@@ -20,4 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
20
|
## [0.1.1] - 2026-02-18
|
|
21
21
|
- Non blocking IO
|
|
22
22
|
## [0.1.2] - 2026-02-18
|
|
23
|
-
- File already exists status added
|
|
23
|
+
- File already exists status added
|
|
24
|
+
## [0.1.3] - 2026-02-21
|
|
25
|
+
- Historical file download added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataquery-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Python SDK for DATAQUERY Data API - Query, download, and check availability of economic data files
|
|
5
5
|
Author-email: DATAQUERY SDK Team <support@dataquery.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dataquery/dataquery-sdk
|
|
@@ -7,8 +7,10 @@ for all API interactions, encapsulating the client and providing high-level oper
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import time
|
|
10
|
+
from calendar import monthrange
|
|
11
|
+
from datetime import date, datetime
|
|
10
12
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
12
14
|
|
|
13
15
|
import structlog
|
|
14
16
|
from dotenv import load_dotenv
|
|
@@ -778,7 +780,7 @@ class DataQuery:
|
|
|
778
780
|
DownloadOptions(
|
|
779
781
|
destination_path=destination_path,
|
|
780
782
|
create_directories=True,
|
|
781
|
-
overwrite_existing=
|
|
783
|
+
overwrite_existing=self.client_config.overwrite_existing,
|
|
782
784
|
chunk_size=8192,
|
|
783
785
|
max_retries=3,
|
|
784
786
|
retry_delay=1.0,
|
|
@@ -1020,7 +1022,7 @@ class DataQuery:
|
|
|
1020
1022
|
result
|
|
1021
1023
|
and hasattr(result, "status")
|
|
1022
1024
|
and hasattr(result, "file_group_id")
|
|
1023
|
-
and result.status.value
|
|
1025
|
+
and result.status.value in ("completed", "already_exists")
|
|
1024
1026
|
):
|
|
1025
1027
|
successful.append(result)
|
|
1026
1028
|
else:
|
|
@@ -1057,7 +1059,7 @@ class DataQuery:
|
|
|
1057
1059
|
result
|
|
1058
1060
|
and hasattr(result, "status")
|
|
1059
1061
|
and hasattr(result, "file_group_id")
|
|
1060
|
-
and result.status.value
|
|
1062
|
+
and result.status.value in ("completed", "already_exists")
|
|
1061
1063
|
):
|
|
1062
1064
|
successful.append(result)
|
|
1063
1065
|
logger.info(
|
|
@@ -1162,6 +1164,188 @@ class DataQuery:
|
|
|
1162
1164
|
)
|
|
1163
1165
|
raise
|
|
1164
1166
|
|
|
1167
|
+
@staticmethod
|
|
1168
|
+
def _split_into_monthly_ranges(from_date: str, to_date: str) -> List[Tuple[str, str]]:
|
|
1169
|
+
"""Split a date range into monthly chunks.
|
|
1170
|
+
|
|
1171
|
+
Args:
|
|
1172
|
+
from_date: Start date in YYYYMMDD format
|
|
1173
|
+
to_date: End date in YYYYMMDD format
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
List of (start_date, end_date) tuples, each covering at most one month.
|
|
1177
|
+
"""
|
|
1178
|
+
start = datetime.strptime(from_date, "%Y%m%d").date()
|
|
1179
|
+
end = datetime.strptime(to_date, "%Y%m%d").date()
|
|
1180
|
+
|
|
1181
|
+
ranges: List[Tuple[str, str]] = []
|
|
1182
|
+
current = start
|
|
1183
|
+
|
|
1184
|
+
while current <= end:
|
|
1185
|
+
last_day = monthrange(current.year, current.month)[1]
|
|
1186
|
+
month_end = date(current.year, current.month, last_day)
|
|
1187
|
+
chunk_end = min(month_end, end)
|
|
1188
|
+
ranges.append((current.strftime("%Y%m%d"), chunk_end.strftime("%Y%m%d")))
|
|
1189
|
+
|
|
1190
|
+
if current.month == 12:
|
|
1191
|
+
current = date(current.year + 1, 1, 1)
|
|
1192
|
+
else:
|
|
1193
|
+
current = date(current.year, current.month + 1, 1)
|
|
1194
|
+
|
|
1195
|
+
return ranges
|
|
1196
|
+
|
|
1197
|
+
async def download_historical_async(
|
|
1198
|
+
self,
|
|
1199
|
+
group_id: str,
|
|
1200
|
+
start_date: str,
|
|
1201
|
+
end_date: str,
|
|
1202
|
+
destination_dir: Path = Path("./downloads"),
|
|
1203
|
+
max_concurrent: int = 5,
|
|
1204
|
+
num_parts: int = 1,
|
|
1205
|
+
delay_between_downloads: float = 0.04,
|
|
1206
|
+
max_retries: int = 3,
|
|
1207
|
+
progress_callback: Optional[Callable] = None,
|
|
1208
|
+
chunk_delay: float = 2.0,
|
|
1209
|
+
) -> Dict[str, Any]:
|
|
1210
|
+
"""
|
|
1211
|
+
Download files for a group across a large date range by splitting into monthly chunks.
|
|
1212
|
+
|
|
1213
|
+
If the date range is within a single month it downloads directly.
|
|
1214
|
+
If the range spans multiple months it splits into monthly chunks and downloads
|
|
1215
|
+
each month sequentially, with a configurable delay between chunks to allow
|
|
1216
|
+
rate limits to recover.
|
|
1217
|
+
|
|
1218
|
+
Args:
|
|
1219
|
+
group_id: Group ID to download files from
|
|
1220
|
+
start_date: Start date in YYYYMMDD format (e.g. "20240101")
|
|
1221
|
+
end_date: End date in YYYYMMDD format (e.g. "20251231")
|
|
1222
|
+
destination_dir: Destination directory for downloads
|
|
1223
|
+
max_concurrent: Maximum concurrent file downloads per chunk (default: 5)
|
|
1224
|
+
num_parts: Number of parallel parts per file download (default: 1)
|
|
1225
|
+
delay_between_downloads: Delay in seconds between file downloads (default: 0.04 for 25 TPS)
|
|
1226
|
+
max_retries: Maximum retry attempts for failed downloads (default: 3)
|
|
1227
|
+
progress_callback: Optional progress callback for individual files
|
|
1228
|
+
chunk_delay: Delay in seconds between monthly chunks (default: 2.0)
|
|
1229
|
+
|
|
1230
|
+
Returns:
|
|
1231
|
+
Dictionary with aggregated download results and per-chunk details.
|
|
1232
|
+
"""
|
|
1233
|
+
operation_start = time.time()
|
|
1234
|
+
|
|
1235
|
+
monthly_ranges = self._split_into_monthly_ranges(start_date, end_date)
|
|
1236
|
+
|
|
1237
|
+
logger.info(
|
|
1238
|
+
"Starting historical download with monthly chunking",
|
|
1239
|
+
group_id=group_id,
|
|
1240
|
+
start_date=start_date,
|
|
1241
|
+
end_date=end_date,
|
|
1242
|
+
monthly_chunks=len(monthly_ranges),
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
chunk_results: List[Dict[str, Any]] = []
|
|
1246
|
+
total_files = 0
|
|
1247
|
+
total_success = 0
|
|
1248
|
+
total_failed = 0
|
|
1249
|
+
|
|
1250
|
+
for i, (chunk_start, chunk_end) in enumerate(monthly_ranges):
|
|
1251
|
+
logger.info(
|
|
1252
|
+
f"Downloading chunk {i + 1}/{len(monthly_ranges)}",
|
|
1253
|
+
chunk_start=chunk_start,
|
|
1254
|
+
chunk_end=chunk_end,
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
chunk_time = time.time()
|
|
1258
|
+
try:
|
|
1259
|
+
report = await self.run_group_download_async(
|
|
1260
|
+
group_id=group_id,
|
|
1261
|
+
start_date=chunk_start,
|
|
1262
|
+
end_date=chunk_end,
|
|
1263
|
+
destination_dir=destination_dir,
|
|
1264
|
+
max_concurrent=max_concurrent,
|
|
1265
|
+
num_parts=num_parts,
|
|
1266
|
+
delay_between_downloads=delay_between_downloads,
|
|
1267
|
+
max_retries=max_retries,
|
|
1268
|
+
progress_callback=progress_callback,
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
chunk_elapsed = time.time() - chunk_time
|
|
1272
|
+
success = report.get("successful_downloads", 0)
|
|
1273
|
+
failed = report.get("failed_downloads", 0)
|
|
1274
|
+
files = report.get("total_files", 0)
|
|
1275
|
+
|
|
1276
|
+
total_files += files
|
|
1277
|
+
total_success += success
|
|
1278
|
+
total_failed += failed
|
|
1279
|
+
|
|
1280
|
+
chunk_results.append(
|
|
1281
|
+
{
|
|
1282
|
+
"chunk": i + 1,
|
|
1283
|
+
"start_date": chunk_start,
|
|
1284
|
+
"end_date": chunk_end,
|
|
1285
|
+
"total_files": files,
|
|
1286
|
+
"successful_downloads": success,
|
|
1287
|
+
"failed_downloads": failed,
|
|
1288
|
+
"elapsed_seconds": round(chunk_elapsed, 2),
|
|
1289
|
+
"report": report,
|
|
1290
|
+
}
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
logger.info(
|
|
1294
|
+
f"Chunk {i + 1}/{len(monthly_ranges)} completed",
|
|
1295
|
+
success=success,
|
|
1296
|
+
failed=failed,
|
|
1297
|
+
total=files,
|
|
1298
|
+
elapsed=f"{chunk_elapsed:.1f}s",
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
except Exception as e:
|
|
1302
|
+
chunk_elapsed = time.time() - chunk_time
|
|
1303
|
+
logger.error(
|
|
1304
|
+
f"Chunk {i + 1}/{len(monthly_ranges)} failed",
|
|
1305
|
+
chunk_start=chunk_start,
|
|
1306
|
+
chunk_end=chunk_end,
|
|
1307
|
+
error=str(e),
|
|
1308
|
+
)
|
|
1309
|
+
chunk_results.append(
|
|
1310
|
+
{
|
|
1311
|
+
"chunk": i + 1,
|
|
1312
|
+
"start_date": chunk_start,
|
|
1313
|
+
"end_date": chunk_end,
|
|
1314
|
+
"error": str(e),
|
|
1315
|
+
"elapsed_seconds": round(chunk_elapsed, 2),
|
|
1316
|
+
}
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
# Delay between chunks to let rate limits recover
|
|
1320
|
+
if i < len(monthly_ranges) - 1:
|
|
1321
|
+
await asyncio.sleep(chunk_delay)
|
|
1322
|
+
|
|
1323
|
+
total_elapsed = time.time() - operation_start
|
|
1324
|
+
total_minutes = total_elapsed / 60.0
|
|
1325
|
+
|
|
1326
|
+
chunks_with_errors = [f"{c['start_date']}-{c['end_date']}" for c in chunk_results if "error" in c]
|
|
1327
|
+
|
|
1328
|
+
summary = {
|
|
1329
|
+
"group_id": group_id,
|
|
1330
|
+
"start_date": start_date,
|
|
1331
|
+
"end_date": end_date,
|
|
1332
|
+
"monthly_chunks": len(monthly_ranges),
|
|
1333
|
+
"total_files": total_files,
|
|
1334
|
+
"successful_downloads": total_success,
|
|
1335
|
+
"failed_downloads": total_failed,
|
|
1336
|
+
"success_rate": (total_success / total_files * 100) if total_files else 0.0,
|
|
1337
|
+
"total_time_seconds": round(total_elapsed, 2),
|
|
1338
|
+
"total_time_minutes": round(total_minutes, 2),
|
|
1339
|
+
"total_time_formatted": (
|
|
1340
|
+
f"{int(total_minutes)}m {int(total_elapsed % 60)}s" if total_minutes >= 1 else f"{total_elapsed:.1f}s"
|
|
1341
|
+
),
|
|
1342
|
+
"chunks_with_errors": chunks_with_errors,
|
|
1343
|
+
"chunk_results": chunk_results,
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
logger.info("Historical download completed", **{k: v for k, v in summary.items() if k != "chunk_results"})
|
|
1347
|
+
return summary
|
|
1348
|
+
|
|
1165
1349
|
async def _download_file_parallel(
|
|
1166
1350
|
self,
|
|
1167
1351
|
file_group_id: str,
|
|
@@ -1214,7 +1398,10 @@ class DataQuery:
|
|
|
1214
1398
|
return await client.download_file_async(
|
|
1215
1399
|
file_group_id=file_group_id,
|
|
1216
1400
|
file_datetime=file_datetime,
|
|
1217
|
-
options=DownloadOptions(
|
|
1401
|
+
options=DownloadOptions(
|
|
1402
|
+
destination_path=destination_path,
|
|
1403
|
+
overwrite_existing=client.config.overwrite_existing,
|
|
1404
|
+
),
|
|
1218
1405
|
progress_callback=progress_callback,
|
|
1219
1406
|
)
|
|
1220
1407
|
|
|
@@ -1224,7 +1411,10 @@ class DataQuery:
|
|
|
1224
1411
|
params["file-datetime"] = file_datetime
|
|
1225
1412
|
|
|
1226
1413
|
# Determine destination directory
|
|
1227
|
-
download_options = DownloadOptions(
|
|
1414
|
+
download_options = DownloadOptions(
|
|
1415
|
+
destination_path=destination_path,
|
|
1416
|
+
overwrite_existing=client.config.overwrite_existing,
|
|
1417
|
+
)
|
|
1228
1418
|
if download_options.destination_path:
|
|
1229
1419
|
resolved_path = Path(download_options.destination_path)
|
|
1230
1420
|
if resolved_path.suffix:
|
|
@@ -2241,6 +2431,35 @@ class DataQuery:
|
|
|
2241
2431
|
)
|
|
2242
2432
|
)
|
|
2243
2433
|
|
|
2434
|
+
def download_historical(
|
|
2435
|
+
self,
|
|
2436
|
+
group_id: str,
|
|
2437
|
+
start_date: str,
|
|
2438
|
+
end_date: str,
|
|
2439
|
+
destination_dir: Path = Path("./downloads"),
|
|
2440
|
+
max_concurrent: int = 5,
|
|
2441
|
+
num_parts: int = 1,
|
|
2442
|
+
delay_between_downloads: float = 0.04,
|
|
2443
|
+
max_retries: int = 3,
|
|
2444
|
+
progress_callback: Optional[Callable] = None,
|
|
2445
|
+
chunk_delay: float = 2.0,
|
|
2446
|
+
) -> Dict[str, Any]:
|
|
2447
|
+
"""Synchronous wrapper for download_historical_async."""
|
|
2448
|
+
return self._run_sync(
|
|
2449
|
+
self.download_historical_async(
|
|
2450
|
+
group_id=group_id,
|
|
2451
|
+
start_date=start_date,
|
|
2452
|
+
end_date=end_date,
|
|
2453
|
+
destination_dir=destination_dir,
|
|
2454
|
+
max_concurrent=max_concurrent,
|
|
2455
|
+
num_parts=num_parts,
|
|
2456
|
+
delay_between_downloads=delay_between_downloads,
|
|
2457
|
+
max_retries=max_retries,
|
|
2458
|
+
progress_callback=progress_callback,
|
|
2459
|
+
chunk_delay=chunk_delay,
|
|
2460
|
+
)
|
|
2461
|
+
)
|
|
2462
|
+
|
|
2244
2463
|
def cleanup(self):
|
|
2245
2464
|
"""Synchronous cleanup resources and ensure proper shutdown."""
|
|
2246
2465
|
if self._client:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataquery-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Python SDK for DATAQUERY Data API - Query, download, and check availability of economic data files
|
|
5
5
|
Author-email: DATAQUERY SDK Team <support@dataquery.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/dataquery/dataquery-sdk
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|