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.
Files changed (29) hide show
  1. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/CHANGELOG.md +3 -1
  2. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/PKG-INFO +1 -1
  3. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/__init__.py +1 -1
  4. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/dataquery.py +225 -6
  5. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/PKG-INFO +1 -1
  6. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/CONTRIBUTING.md +0 -0
  7. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/LICENSE +0 -0
  8. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/MANIFEST.in +0 -0
  9. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/README.md +0 -0
  10. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/auth.py +0 -0
  11. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/auto_download.py +0 -0
  12. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/cli.py +0 -0
  13. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/client.py +0 -0
  14. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/config.py +0 -0
  15. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/connection_pool.py +0 -0
  16. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/exceptions.py +0 -0
  17. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/logging_config.py +0 -0
  18. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/models.py +0 -0
  19. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/py.typed +0 -0
  20. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/rate_limiter.py +0 -0
  21. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/retry.py +0 -0
  22. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery/utils.py +0 -0
  23. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/SOURCES.txt +0 -0
  24. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
  25. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/entry_points.txt +0 -0
  26. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/requires.txt +0 -0
  27. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/dataquery_sdk.egg-info/top_level.txt +0 -0
  28. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.3}/pyproject.toml +0 -0
  29. {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.2
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
@@ -14,7 +14,7 @@ Quick Start:
14
14
  For more information, visit: https://github.com/dataquery/dataquery-sdk
15
15
  """
16
16
 
17
- __version__ = "0.1.2"
17
+ __version__ = "0.1.3"
18
18
  __author__ = "DATAQUERY SDK Team"
19
19
  __email__ = "dataquery_support@jpmorgan.com"
20
20
  __license__ = "MIT"
@@ -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=False,
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 == "completed"
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 == "completed"
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(destination_path=destination_path),
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(destination_path=destination_path)
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.2
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