dataquery-sdk 0.1.1__tar.gz → 0.1.2__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.1 → dataquery_sdk-0.1.2}/CHANGELOG.md +3 -1
  2. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/PKG-INFO +3 -7
  3. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/__init__.py +2 -2
  4. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/auth.py +7 -25
  5. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/auto_download.py +22 -31
  6. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/cli.py +11 -33
  7. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/client.py +101 -255
  8. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/config.py +6 -16
  9. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/connection_pool.py +17 -61
  10. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/dataquery.py +82 -227
  11. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/exceptions.py +6 -18
  12. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/logging_config.py +6 -18
  13. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/models.py +113 -334
  14. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/rate_limiter.py +5 -16
  15. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/retry.py +8 -25
  16. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/utils.py +11 -25
  17. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/PKG-INFO +3 -7
  18. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/requires.txt +2 -6
  19. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/pyproject.toml +23 -35
  20. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/CONTRIBUTING.md +0 -0
  21. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/LICENSE +0 -0
  22. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/MANIFEST.in +0 -0
  23. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/README.md +0 -0
  24. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery/py.typed +0 -0
  25. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/SOURCES.txt +0 -0
  26. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
  27. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/entry_points.txt +0 -0
  28. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/top_level.txt +0 -0
  29. {dataquery_sdk-0.1.1 → dataquery_sdk-0.1.2}/setup.cfg +0 -0
@@ -18,4 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
18
  ## [0.1.0] - 2025-12-10
19
19
  - Time series data made optional
20
20
  ## [0.1.1] - 2026-02-18
21
- - Non blocking IO
21
+ - Non blocking IO
22
+ ## [0.1.2] - 2026-02-18
23
+ - File already exists status added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataquery-sdk
3
- Version: 0.1.1
3
+ Version: 0.1.2
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
@@ -34,10 +34,8 @@ Provides-Extra: dev
34
34
  Requires-Dist: pytest>=7.0.0; extra == "dev"
35
35
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
36
36
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
37
- Requires-Dist: black>=23.0.0; extra == "dev"
38
- Requires-Dist: flake8>=6.0.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
39
38
  Requires-Dist: mypy>=1.5.0; extra == "dev"
40
- Requires-Dist: isort>=5.12.0; extra == "dev"
41
39
  Requires-Dist: pre-commit>=3.0.0; extra == "dev"
42
40
  Requires-Dist: pip-audit>=2.7.0; extra == "dev"
43
41
  Requires-Dist: build>=1.3.0; extra == "dev"
@@ -53,10 +51,8 @@ Provides-Extra: all
53
51
  Requires-Dist: pytest>=7.0.0; extra == "all"
54
52
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
55
53
  Requires-Dist: pytest-cov>=4.0.0; extra == "all"
56
- Requires-Dist: black>=23.0.0; extra == "all"
57
- Requires-Dist: flake8>=6.0.0; extra == "all"
54
+ Requires-Dist: ruff>=0.4.0; extra == "all"
58
55
  Requires-Dist: mypy>=1.5.0; extra == "all"
59
- Requires-Dist: isort>=5.12.0; extra == "all"
60
56
  Requires-Dist: pre-commit>=3.0.0; extra == "all"
61
57
  Requires-Dist: mkdocs>=1.5.0; extra == "all"
62
58
  Requires-Dist: mkdocs-material>=9.0.0; extra == "all"
@@ -14,9 +14,9 @@ Quick Start:
14
14
  For more information, visit: https://github.com/dataquery/dataquery-sdk
15
15
  """
16
16
 
17
- __version__ = "0.1.1"
17
+ __version__ = "0.1.2"
18
18
  __author__ = "DATAQUERY SDK Team"
19
- __email__ = "dataquery@jpmorgan.com"
19
+ __email__ = "dataquery_support@jpmorgan.com"
20
20
  __license__ = "MIT"
21
21
  __url__ = "https://github.com/jpmorganchase/dataquery-sdk"
22
22
 
@@ -32,9 +32,7 @@ class TokenManager:
32
32
  # Prefer explicit token_storage_dir only when token storage is enabled,
33
33
  # else fallback to download_dir/.tokens if download_dir is provided.
34
34
  base_dir: Optional[Path] = None
35
- token_storage_enabled = bool(
36
- getattr(self.config, "token_storage_enabled", False)
37
- )
35
+ token_storage_enabled = bool(getattr(self.config, "token_storage_enabled", False))
38
36
  token_storage_dir = getattr(self.config, "token_storage_dir", None)
39
37
  if token_storage_enabled and token_storage_dir:
40
38
  base_dir = Path(token_storage_dir)
@@ -97,9 +95,7 @@ class TokenManager:
97
95
 
98
96
  # Validate required fields
99
97
  if not self.config.client_id or not self.config.client_secret:
100
- raise ConfigurationError(
101
- "client_id and client_secret are required for OAuth"
102
- )
98
+ raise ConfigurationError("client_id and client_secret are required for OAuth")
103
99
 
104
100
  # Create token request
105
101
  token_request = TokenRequest(
@@ -142,9 +138,7 @@ class TokenManager:
142
138
  status=response.status,
143
139
  error=error_data,
144
140
  )
145
- raise AuthenticationError(
146
- f"OAuth token request failed: {response.status}"
147
- )
141
+ raise AuthenticationError(f"OAuth token request failed: {response.status}")
148
142
 
149
143
  except Exception as e:
150
144
  logger.error("Error getting OAuth token", error=str(e))
@@ -220,9 +214,7 @@ class TokenManager:
220
214
 
221
215
  # Convert timestamp back to datetime
222
216
  if "issued_at" in token_data:
223
- token_data["issued_at"] = datetime.fromisoformat(
224
- token_data["issued_at"]
225
- )
217
+ token_data["issued_at"] = datetime.fromisoformat(token_data["issued_at"])
226
218
 
227
219
  self.current_token = OAuthToken(**token_data)
228
220
 
@@ -232,9 +224,7 @@ class TokenManager:
232
224
  self.current_token = None
233
225
  return None
234
226
 
235
- logger.info(
236
- "Token loaded from storage", expires_at=self.current_token.expires_at
237
- )
227
+ logger.info("Token loaded from storage", expires_at=self.current_token.expires_at)
238
228
  return self.current_token
239
229
 
240
230
  except Exception as e:
@@ -297,16 +287,8 @@ class TokenManager:
297
287
  return {
298
288
  "status": self.current_token.status.value,
299
289
  "token_type": self.current_token.token_type,
300
- "issued_at": (
301
- self.current_token.issued_at.isoformat()
302
- if self.current_token.issued_at
303
- else None
304
- ),
305
- "expires_at": (
306
- self.current_token.expires_at.isoformat()
307
- if self.current_token.expires_at
308
- else None
309
- ),
290
+ "issued_at": (self.current_token.issued_at.isoformat() if self.current_token.issued_at else None),
291
+ "expires_at": (self.current_token.expires_at.isoformat() if self.current_token.expires_at else None),
310
292
  "is_expired": self.current_token.is_expired,
311
293
  "has_refresh_token": self.current_token.refresh_token is not None,
312
294
  }
@@ -121,8 +121,7 @@ class AutoDownloadManager:
121
121
  self._stop_event.clear()
122
122
 
123
123
  self.logger.info(
124
- f"Starting auto-download for group '{self.group_id}' "
125
- f"(interval: {self.interval_minutes} minutes)"
124
+ f"Starting auto-download for group '{self.group_id}' (interval: {self.interval_minutes} minutes)"
126
125
  )
127
126
 
128
127
  # Start the monitoring task
@@ -145,9 +144,7 @@ class AutoDownloadManager:
145
144
  try:
146
145
  await asyncio.wait_for(self._task, timeout=5.0)
147
146
  except asyncio.TimeoutError:
148
- self.logger.warning(
149
- "Monitoring task did not stop gracefully, cancelling..."
150
- )
147
+ self.logger.warning("Monitoring task did not stop gracefully, cancelling...")
151
148
  self._task.cancel()
152
149
  try:
153
150
  await self._task
@@ -184,9 +181,7 @@ class AutoDownloadManager:
184
181
 
185
182
  # Wait for the specified interval or until stopped
186
183
  try:
187
- await asyncio.wait_for(
188
- self._stop_event.wait(), timeout=self.interval_minutes * 60
189
- )
184
+ await asyncio.wait_for(self._stop_event.wait(), timeout=self.interval_minutes * 60)
190
185
  # If we reach here, stop was requested
191
186
  break
192
187
  except asyncio.TimeoutError:
@@ -220,19 +215,13 @@ class AutoDownloadManager:
220
215
  )
221
216
 
222
217
  if not isinstance(available_files, list) or not available_files:
223
- self.logger.debug(
224
- f"No available files for group '{self.group_id}' between {start_date} and {end_date}"
225
- )
218
+ self.logger.debug(f"No available files for group '{self.group_id}' between {start_date} and {end_date}")
226
219
  return
227
220
 
228
- self.logger.debug(
229
- f"Found {len(available_files)} available file entries for group '{self.group_id}'"
230
- )
221
+ self.logger.debug(f"Found {len(available_files)} available file entries for group '{self.group_id}'")
231
222
 
232
223
  # Build eligible downloads
233
- eligible: List[tuple[str, str, str]] = (
234
- []
235
- ) # (file_group_id, date_str, file_key)
224
+ eligible: List[tuple[str, str, str]] = [] # (file_group_id, date_str, file_key)
236
225
  for item in available_files:
237
226
  if not self._running:
238
227
  break
@@ -267,10 +256,7 @@ class AutoDownloadManager:
267
256
  async with semaphore:
268
257
  await self._download_file(fid, dstr, fkey)
269
258
 
270
- tasks = [
271
- asyncio.create_task(worker(fid, dstr, fkey))
272
- for fid, dstr, fkey in eligible
273
- ]
259
+ tasks = [asyncio.create_task(worker(fid, dstr, fkey)) for fid, dstr, fkey in eligible]
274
260
  await asyncio.gather(*tasks, return_exceptions=True)
275
261
 
276
262
  except Exception as e:
@@ -336,6 +322,7 @@ class AutoDownloadManager:
336
322
  )
337
323
 
338
324
  succeeded = False
325
+ already_exists = False
339
326
  if result is not None and getattr(result, "status", None) is not None:
340
327
  try:
341
328
  from .models import ( # import inside to avoid cycles in some tools
@@ -343,21 +330,29 @@ class AutoDownloadManager:
343
330
  )
344
331
 
345
332
  succeeded = result.status == DownloadStatus.COMPLETED
333
+ already_exists = result.status == DownloadStatus.ALREADY_EXISTS
346
334
  except Exception:
347
335
  succeeded = False
348
336
 
349
- if succeeded:
337
+ if already_exists:
350
338
  self.logger.info(
351
- "Successfully downloaded '%s' for %s", file_group_id, date_str
339
+ "File already exists for '%s' for %s, skipping",
340
+ file_group_id,
341
+ date_str,
352
342
  )
343
+ self.stats["files_skipped"] += 1
344
+ self._downloaded_files.add(file_key)
345
+ if file_key in self._failed_files:
346
+ del self._failed_files[file_key]
347
+
348
+ elif succeeded:
349
+ self.logger.info("Successfully downloaded '%s' for %s", file_group_id, date_str)
353
350
  self.stats["files_downloaded"] += 1
354
351
  self._downloaded_files.add(file_key)
355
352
  # Accumulate total bytes if provided
356
353
  try:
357
354
  if getattr(result, "file_size", None):
358
- self.stats["total_bytes_downloaded"] += (
359
- int(result.file_size) or 0
360
- )
355
+ self.stats["total_bytes_downloaded"] += int(result.file_size) or 0
361
356
  except Exception:
362
357
  pass
363
358
 
@@ -366,11 +361,7 @@ class AutoDownloadManager:
366
361
  del self._failed_files[file_key]
367
362
 
368
363
  else:
369
- error_msg = (
370
- getattr(result, "error_message", "Unknown error")
371
- if result
372
- else "No result"
373
- )
364
+ error_msg = getattr(result, "error_message", "Unknown error") if result else "No result"
374
365
  self.logger.error(
375
366
  "Download failed for '%s' for %s: %s",
376
367
  file_group_id,
@@ -11,18 +11,14 @@ from dataquery import DataQuery
11
11
 
12
12
  def create_parser() -> argparse.ArgumentParser:
13
13
  """Create the top-level CLI parser with subcommands."""
14
- parser = argparse.ArgumentParser(
15
- description="Command Line Interface for the DataQuery SDK"
16
- )
14
+ parser = argparse.ArgumentParser(description="Command Line Interface for the DataQuery SDK")
17
15
  parser.add_argument("--env-file", type=str, default=None, help="Path to .env file")
18
16
  subparsers = parser.add_subparsers(dest="command")
19
17
 
20
18
  # groups
21
19
  p_groups = subparsers.add_parser("groups", help="List or search groups")
22
20
  p_groups.add_argument("--json", action="store_true", help="Output JSON")
23
- p_groups.add_argument(
24
- "--limit", type=int, default=None, help="Limit number of results"
25
- )
21
+ p_groups.add_argument("--limit", type=int, default=None, help="Limit number of results")
26
22
  p_groups.add_argument("--search", type=str, default=None, help="Search keywords")
27
23
 
28
24
  # files
@@ -43,22 +39,14 @@ def create_parser() -> argparse.ArgumentParser:
43
39
  p_dl.add_argument("--file-group-id", default=None)
44
40
  p_dl.add_argument("--file-datetime", default=None)
45
41
  p_dl.add_argument("--destination", type=str, default=None)
46
- p_dl.add_argument(
47
- "--watch", action="store_true", help="Watch a group for new files"
48
- )
42
+ p_dl.add_argument("--watch", action="store_true", help="Watch a group for new files")
49
43
  p_dl.add_argument("--group-id", default=None, help="Required with --watch")
50
44
  p_dl.add_argument("--json", action="store_true")
51
- p_dl.add_argument(
52
- "--num-parts", type=int, default=5, help="Number of parallel parts"
53
- )
54
- p_dl.add_argument(
55
- "--chunk-size", type=int, default=None, help="Chunk size in bytes"
56
- )
45
+ p_dl.add_argument("--num-parts", type=int, default=5, help="Number of parallel parts")
46
+ p_dl.add_argument("--chunk-size", type=int, default=None, help="Chunk size in bytes")
57
47
 
58
48
  # download-group
59
- p_dlg = subparsers.add_parser(
60
- "download-group", help="Download all files in a group"
61
- )
49
+ p_dlg = subparsers.add_parser("download-group", help="Download all files in a group")
62
50
  p_dlg.add_argument("--group-id", required=True)
63
51
  p_dlg.add_argument("--start-date", required=True)
64
52
  p_dlg.add_argument("--end-date", required=True)
@@ -101,9 +89,7 @@ async def cmd_groups(args: argparse.Namespace) -> int:
101
89
  for g in items:
102
90
  try:
103
91
  d = g.model_dump()
104
- print(
105
- f"{d.get('group_id') or d.get('group-id')}\t{d.get('group_name') or d.get('group-name')}"
106
- )
92
+ print(f"{d.get('group_id') or d.get('group-id')}\t{d.get('group_name') or d.get('group-name')}")
107
93
  except Exception:
108
94
  print(str(g))
109
95
  return 0
@@ -125,9 +111,7 @@ async def cmd_files(args: argparse.Namespace) -> int:
125
111
  for f in files:
126
112
  try:
127
113
  d = f.model_dump()
128
- print(
129
- f"{d.get('file_group_id') or d.get('file-group-id')}\t{d.get('file_type')}"
130
- )
114
+ print(f"{d.get('file_group_id') or d.get('file-group-id')}\t{d.get('file_type')}")
131
115
  except Exception:
132
116
  print(str(f))
133
117
  return 0
@@ -135,9 +119,7 @@ async def cmd_files(args: argparse.Namespace) -> int:
135
119
 
136
120
  async def cmd_availability(args: argparse.Namespace) -> int:
137
121
  async with DataQuery(args.env_file) as dq:
138
- avail = await dq.check_availability_async(
139
- args.file_group_id, args.file_datetime
140
- )
122
+ avail = await dq.check_availability_async(args.file_group_id, args.file_datetime)
141
123
  if args.json:
142
124
  try:
143
125
  print(json.dumps(getattr(avail, "model_dump")(), indent=2))
@@ -210,9 +192,7 @@ async def cmd_download_group(args: argparse.Namespace) -> int:
210
192
  if args.json:
211
193
  print(json.dumps(results, indent=2))
212
194
  else:
213
- print(
214
- f"Downloaded {results.get('successful', 0)} files to {args.destination}"
215
- )
195
+ print(f"Downloaded {results.get('successful', 0)} files to {args.destination}")
216
196
  if results.get("failed", 0) > 0:
217
197
  print(f"Failed: {results.get('failed', 0)}")
218
198
  return 0
@@ -221,9 +201,7 @@ async def cmd_download_group(args: argparse.Namespace) -> int:
221
201
  def cmd_config_show(args: argparse.Namespace) -> int:
222
202
  from dataquery.config import EnvConfig
223
203
 
224
- EnvConfig.create_client_config(
225
- env_file=Path(args.env_file) if getattr(args, "env_file", None) else None
226
- )
204
+ EnvConfig.create_client_config(env_file=Path(args.env_file) if getattr(args, "env_file", None) else None)
227
205
  print("Configuration loaded")
228
206
  return 0
229
207