dataquery-sdk 0.1.2__tar.gz → 0.1.4__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.4}/CHANGELOG.md +5 -1
  2. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/PKG-INFO +1 -1
  3. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/__init__.py +1 -1
  4. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/client.py +3 -2
  5. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/config.py +3 -0
  6. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/dataquery.py +232 -13
  7. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/models.py +1 -0
  8. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery_sdk.egg-info/PKG-INFO +1 -1
  9. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/CONTRIBUTING.md +0 -0
  10. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/LICENSE +0 -0
  11. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/MANIFEST.in +0 -0
  12. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/README.md +0 -0
  13. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/auth.py +0 -0
  14. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/auto_download.py +0 -0
  15. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/cli.py +0 -0
  16. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/connection_pool.py +0 -0
  17. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/exceptions.py +0 -0
  18. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/logging_config.py +0 -0
  19. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/py.typed +0 -0
  20. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/rate_limiter.py +0 -0
  21. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/retry.py +0 -0
  22. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery/utils.py +0 -0
  23. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery_sdk.egg-info/SOURCES.txt +0 -0
  24. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
  25. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery_sdk.egg-info/entry_points.txt +0 -0
  26. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery_sdk.egg-info/requires.txt +0 -0
  27. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/dataquery_sdk.egg-info/top_level.txt +0 -0
  28. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/pyproject.toml +0 -0
  29. {dataquery_sdk-0.1.2 → dataquery_sdk-0.1.4}/setup.cfg +0 -0
@@ -20,4 +20,8 @@ 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
26
+ ## [0.1.4] - 2026-03-23
27
+ - Introduced circuit breaker environment variable (`DATAQUERY_CIRCUIT_BREAKER_THRESHOLD`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataquery-sdk
3
- Version: 0.1.2
3
+ Version: 0.1.4
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.4"
18
18
  __author__ = "DATAQUERY SDK Team"
19
19
  __email__ = "dataquery_support@jpmorgan.com"
20
20
  __license__ = "MIT"
@@ -134,6 +134,7 @@ class DataQueryClient:
134
134
  max_delay=300.0,
135
135
  strategy=RetryStrategy.EXPONENTIAL,
136
136
  enable_circuit_breaker=True,
137
+ circuit_breaker_threshold=self.config.circuit_breaker_threshold,
137
138
  retryable_exceptions=[
138
139
  ConnectionError,
139
140
  TimeoutError,
@@ -786,7 +787,7 @@ class DataQueryClient:
786
787
 
787
788
  # Validate num_parts if provided
788
789
  if num_parts is not None and (num_parts is None or num_parts <= 0):
789
- num_parts = 5
790
+ num_parts = 1
790
791
 
791
792
  params = {"file-group-id": file_group_id}
792
793
  if file_datetime:
@@ -847,7 +848,7 @@ class DataQueryClient:
847
848
  file_group_id: str,
848
849
  file_datetime: Optional[str] = None,
849
850
  options: Optional[DownloadOptions] = None,
850
- num_parts: int = 5,
851
+ num_parts: int = 1,
851
852
  progress_callback: Optional[Callable] = None,
852
853
  ) -> DownloadResult:
853
854
  """
@@ -44,6 +44,7 @@ class EnvConfig:
44
44
  "TIMEOUT": "6000.0",
45
45
  "MAX_RETRIES": "3",
46
46
  "RETRY_DELAY": "1.0",
47
+ "CIRCUIT_BREAKER_THRESHOLD": "5",
47
48
  # Connection Pooling
48
49
  "POOL_CONNECTIONS": "10",
49
50
  "POOL_MAXSIZE": "20",
@@ -116,6 +117,7 @@ class EnvConfig:
116
117
  "timeout": "DATAQUERY_TIMEOUT",
117
118
  "max_retries": "DATAQUERY_MAX_RETRIES",
118
119
  "retry_delay": "DATAQUERY_RETRY_DELAY",
120
+ "circuit_breaker_threshold": "DATAQUERY_CIRCUIT_BREAKER_THRESHOLD",
119
121
  # Connection Pooling
120
122
  "pool_connections": "DATAQUERY_POOL_CONNECTIONS",
121
123
  "pool_maxsize": "DATAQUERY_POOL_MAXSIZE",
@@ -279,6 +281,7 @@ class EnvConfig:
279
281
  timeout=cls.get_float("TIMEOUT"),
280
282
  max_retries=cls.get_int("MAX_RETRIES"),
281
283
  retry_delay=cls.get_float("RETRY_DELAY"),
284
+ circuit_breaker_threshold=cls.get_int("CIRCUIT_BREAKER_THRESHOLD"),
282
285
  # Connection pooling
283
286
  pool_connections=cls.get_int("POOL_CONNECTIONS"),
284
287
  pool_maxsize=cls.get_int("POOL_MAXSIZE"),
@@ -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
@@ -317,7 +319,7 @@ class DataQuery:
317
319
  file_datetime: Optional[str] = None,
318
320
  destination_path: Optional[Path] = None,
319
321
  options: Optional[DownloadOptions] = None,
320
- num_parts: int = 5,
322
+ num_parts: int = 1,
321
323
  progress_callback: Optional[Callable] = None,
322
324
  ) -> DownloadResult:
323
325
  """
@@ -357,7 +359,7 @@ class DataQuery:
357
359
 
358
360
  client = self._ensure_client()
359
361
  # Pass parameters in correct order: file_group_id, file_datetime, options, num_parts, progress_callback
360
- # Use default num_parts=5
362
+ # Use default num_parts=1
361
363
  return await client.download_file_async(file_group_id, file_datetime, options, num_parts, progress_callback)
362
364
 
363
365
  async def list_available_files_async(
@@ -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,
@@ -824,7 +826,7 @@ class DataQuery:
824
826
  end_date: str,
825
827
  destination_dir: Path = Path("./downloads"),
826
828
  max_concurrent: int = 25, # Full 25 TPS capacity
827
- num_parts: int = 5,
829
+ num_parts: int = 1,
828
830
  progress_callback: Optional[Callable] = None,
829
831
  delay_between_downloads: float = 0.04, # 25 TPS (1/25 = 0.04s)
830
832
  max_retries: int = 3,
@@ -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,
@@ -1207,14 +1391,17 @@ class DataQuery:
1207
1391
  validate_file_datetime(file_datetime)
1208
1392
 
1209
1393
  if num_parts is None or num_parts <= 0:
1210
- num_parts = 5
1394
+ num_parts = 1
1211
1395
 
1212
1396
  # Check if range downloads are disabled - use single stream instead
1213
1397
  if not client.config.enable_range_downloads:
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:
@@ -1912,7 +2102,7 @@ class DataQuery:
1912
2102
  file_datetime: Optional[str] = None,
1913
2103
  destination_path: Optional[Path] = None,
1914
2104
  options: Optional[DownloadOptions] = None,
1915
- num_parts: int = 5,
2105
+ num_parts: int = 1,
1916
2106
  progress_callback: Optional[Callable] = None,
1917
2107
  ) -> DownloadResult:
1918
2108
  """
@@ -2223,7 +2413,7 @@ class DataQuery:
2223
2413
  end_date: str,
2224
2414
  destination_dir: Path = Path("./downloads"),
2225
2415
  max_concurrent: int = 5,
2226
- num_parts: int = 5,
2416
+ num_parts: int = 1,
2227
2417
  progress_callback: Optional[Callable] = None,
2228
2418
  delay_between_downloads: float = 1.0,
2229
2419
  ) -> dict:
@@ -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:
@@ -2287,7 +2506,7 @@ class DataQuery:
2287
2506
  file_datetime: Optional[str] = None,
2288
2507
  destination_path: Optional[Path] = None,
2289
2508
  options: Optional[DownloadOptions] = None,
2290
- num_parts: int = 5,
2509
+ num_parts: int = 1,
2291
2510
  progress_callback: Optional[Callable] = None,
2292
2511
  ) -> DownloadResult:
2293
2512
  """Synchronous wrapper for download_file with _sync suffix."""
@@ -74,6 +74,7 @@ class ClientConfig(BaseModel):
74
74
  timeout: float = Field(default=600.0, description="Default request timeout in seconds")
75
75
  max_retries: int = Field(default=3, description="Maximum retry attempts")
76
76
  retry_delay: float = Field(default=1.0, description="Delay between retries in seconds")
77
+ circuit_breaker_threshold: int = Field(default=5, description="Number of failures before circuit breaker opens")
77
78
 
78
79
  # Connection pooling
79
80
  pool_connections: int = Field(default=10, description="Number of connection pools")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataquery-sdk
3
- Version: 0.1.2
3
+ Version: 0.1.4
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