dataquery-sdk 0.1.0__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.
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/CHANGELOG.md +6 -2
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/PKG-INFO +4 -8
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/README.md +1 -1
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/__init__.py +5 -4
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/auth.py +7 -26
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/auto_download.py +50 -55
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/cli.py +11 -33
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/client.py +223 -351
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/config.py +14 -24
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/connection_pool.py +17 -61
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/dataquery.py +315 -394
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/exceptions.py +12 -20
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/logging_config.py +6 -18
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/models.py +121 -455
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/rate_limiter.py +7 -56
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/retry.py +8 -25
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/utils.py +15 -30
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/PKG-INFO +4 -8
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/requires.txt +2 -6
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/pyproject.toml +23 -35
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/CONTRIBUTING.md +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/LICENSE +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/MANIFEST.in +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery/py.typed +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/SOURCES.txt +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/entry_points.txt +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/dataquery_sdk.egg-info/top_level.txt +0 -0
- {dataquery_sdk-0.1.0 → dataquery_sdk-0.1.2}/setup.cfg +0 -0
|
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
14
|
## [0.0.8] - 2025-10-21
|
|
15
15
|
- Made label and expression parameters optional in attribute api response
|
|
16
16
|
## [0.0.9] - 2025-12-09
|
|
17
|
-
-
|
|
17
|
+
- Performance optimizations
|
|
18
18
|
## [0.1.0] - 2025-12-10
|
|
19
|
-
Time
|
|
19
|
+
- Time series data made optional
|
|
20
|
+
## [0.1.1] - 2026-02-18
|
|
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.
|
|
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:
|
|
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:
|
|
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"
|
|
@@ -279,7 +275,7 @@ DATAQUERY_CLIENT_SECRET=your_client_secret
|
|
|
279
275
|
|
|
280
276
|
# Optional - API Endpoints
|
|
281
277
|
DATAQUERY_BASE_URL=https://api-developer.jpmorgan.com
|
|
282
|
-
DATAQUERY_FILES_BASE_URL=https://api-
|
|
278
|
+
DATAQUERY_FILES_BASE_URL=https://api-dataquery.jpmchase.com
|
|
283
279
|
|
|
284
280
|
# Optional - Performance
|
|
285
281
|
DATAQUERY_MAX_RETRIES=3
|
|
@@ -210,7 +210,7 @@ DATAQUERY_CLIENT_SECRET=your_client_secret
|
|
|
210
210
|
|
|
211
211
|
# Optional - API Endpoints
|
|
212
212
|
DATAQUERY_BASE_URL=https://api-developer.jpmorgan.com
|
|
213
|
-
DATAQUERY_FILES_BASE_URL=https://api-
|
|
213
|
+
DATAQUERY_FILES_BASE_URL=https://api-dataquery.jpmchase.com
|
|
214
214
|
|
|
215
215
|
# Optional - Performance
|
|
216
216
|
DATAQUERY_MAX_RETRIES=3
|
|
@@ -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.
|
|
17
|
+
__version__ = "0.1.2"
|
|
18
18
|
__author__ = "DATAQUERY SDK Team"
|
|
19
|
-
__email__ = "
|
|
19
|
+
__email__ = "dataquery_support@jpmorgan.com"
|
|
20
20
|
__license__ = "MIT"
|
|
21
21
|
__url__ = "https://github.com/jpmorganchase/dataquery-sdk"
|
|
22
22
|
|
|
@@ -43,7 +43,7 @@ from .connection_pool import (
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
# Core imports
|
|
46
|
-
from .dataquery import DataQuery
|
|
46
|
+
from .dataquery import DataQuery
|
|
47
47
|
from .exceptions import (
|
|
48
48
|
AuthenticationError,
|
|
49
49
|
AvailabilityError,
|
|
@@ -52,6 +52,7 @@ from .exceptions import (
|
|
|
52
52
|
DateRangeError,
|
|
53
53
|
DownloadError,
|
|
54
54
|
FileNotFoundError,
|
|
55
|
+
FileNotFoundInGroupError,
|
|
55
56
|
FileTypeError,
|
|
56
57
|
GroupNotFoundError,
|
|
57
58
|
NetworkError,
|
|
@@ -124,7 +125,6 @@ from .utils import (
|
|
|
124
125
|
__all__ = [
|
|
125
126
|
# Core
|
|
126
127
|
"DataQuery",
|
|
127
|
-
"setup_logging",
|
|
128
128
|
# Models
|
|
129
129
|
"ClientConfig",
|
|
130
130
|
"Group",
|
|
@@ -153,6 +153,7 @@ __all__ = [
|
|
|
153
153
|
"AvailabilityError",
|
|
154
154
|
"GroupNotFoundError",
|
|
155
155
|
"FileNotFoundError",
|
|
156
|
+
"FileNotFoundInGroupError",
|
|
156
157
|
"DateRangeError",
|
|
157
158
|
"FileTypeError",
|
|
158
159
|
"WorkflowError",
|
|
@@ -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,16 +95,13 @@ 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(
|
|
106
102
|
grant_type=self.config.grant_type,
|
|
107
103
|
client_id=self.config.client_id,
|
|
108
104
|
client_secret=self.config.client_secret,
|
|
109
|
-
# scope removed
|
|
110
105
|
aud=getattr(self.config, "aud", None),
|
|
111
106
|
)
|
|
112
107
|
|
|
@@ -143,9 +138,7 @@ class TokenManager:
|
|
|
143
138
|
status=response.status,
|
|
144
139
|
error=error_data,
|
|
145
140
|
)
|
|
146
|
-
raise AuthenticationError(
|
|
147
|
-
f"OAuth token request failed: {response.status}"
|
|
148
|
-
)
|
|
141
|
+
raise AuthenticationError(f"OAuth token request failed: {response.status}")
|
|
149
142
|
|
|
150
143
|
except Exception as e:
|
|
151
144
|
logger.error("Error getting OAuth token", error=str(e))
|
|
@@ -221,9 +214,7 @@ class TokenManager:
|
|
|
221
214
|
|
|
222
215
|
# Convert timestamp back to datetime
|
|
223
216
|
if "issued_at" in token_data:
|
|
224
|
-
token_data["issued_at"] = datetime.fromisoformat(
|
|
225
|
-
token_data["issued_at"]
|
|
226
|
-
)
|
|
217
|
+
token_data["issued_at"] = datetime.fromisoformat(token_data["issued_at"])
|
|
227
218
|
|
|
228
219
|
self.current_token = OAuthToken(**token_data)
|
|
229
220
|
|
|
@@ -233,9 +224,7 @@ class TokenManager:
|
|
|
233
224
|
self.current_token = None
|
|
234
225
|
return None
|
|
235
226
|
|
|
236
|
-
logger.info(
|
|
237
|
-
"Token loaded from storage", expires_at=self.current_token.expires_at
|
|
238
|
-
)
|
|
227
|
+
logger.info("Token loaded from storage", expires_at=self.current_token.expires_at)
|
|
239
228
|
return self.current_token
|
|
240
229
|
|
|
241
230
|
except Exception as e:
|
|
@@ -298,16 +287,8 @@ class TokenManager:
|
|
|
298
287
|
return {
|
|
299
288
|
"status": self.current_token.status.value,
|
|
300
289
|
"token_type": self.current_token.token_type,
|
|
301
|
-
"issued_at": (
|
|
302
|
-
|
|
303
|
-
if self.current_token.issued_at
|
|
304
|
-
else None
|
|
305
|
-
),
|
|
306
|
-
"expires_at": (
|
|
307
|
-
self.current_token.expires_at.isoformat()
|
|
308
|
-
if self.current_token.expires_at
|
|
309
|
-
else None
|
|
310
|
-
),
|
|
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),
|
|
311
292
|
"is_expired": self.current_token.is_expired,
|
|
312
293
|
"has_refresh_token": self.current_token.refresh_token is not None,
|
|
313
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
|
|
@@ -166,7 +163,7 @@ class AutoDownloadManager:
|
|
|
166
163
|
self.stats["last_check_time"] = datetime.now()
|
|
167
164
|
|
|
168
165
|
except Exception as e:
|
|
169
|
-
self.logger.error(
|
|
166
|
+
self.logger.error("Error in monitoring loop: %s", e)
|
|
170
167
|
self.stats["errors"].append(
|
|
171
168
|
{
|
|
172
169
|
"timestamp": datetime.now().isoformat(),
|
|
@@ -180,13 +177,11 @@ class AutoDownloadManager:
|
|
|
180
177
|
try:
|
|
181
178
|
await self._safe_call_callback(self.error_callback, e)
|
|
182
179
|
except Exception as cb_error:
|
|
183
|
-
self.logger.error(
|
|
180
|
+
self.logger.error("Error in error callback: %s", cb_error)
|
|
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:
|
|
@@ -196,7 +191,7 @@ class AutoDownloadManager:
|
|
|
196
191
|
except asyncio.CancelledError:
|
|
197
192
|
self.logger.info("Monitoring loop cancelled")
|
|
198
193
|
except Exception as e:
|
|
199
|
-
self.logger.error(
|
|
194
|
+
self.logger.error("Fatal error in monitoring loop: %s", e)
|
|
200
195
|
finally:
|
|
201
196
|
self._running = False
|
|
202
197
|
|
|
@@ -220,38 +215,34 @@ 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]] = [] # (
|
|
224
|
+
eligible: List[tuple[str, str, str]] = [] # (file_group_id, date_str, file_key)
|
|
234
225
|
for item in available_files:
|
|
235
226
|
if not self._running:
|
|
236
227
|
break
|
|
237
|
-
|
|
228
|
+
file_group_id = item.get("file-group-id")
|
|
238
229
|
date_str = item.get("file-datetime")
|
|
239
230
|
avail_flag = item.get("is-available")
|
|
240
231
|
is_available = bool(avail_flag)
|
|
241
|
-
if not
|
|
232
|
+
if not file_group_id or not date_str or not is_available:
|
|
242
233
|
continue
|
|
243
234
|
if self.file_filter and not self.file_filter(item):
|
|
244
235
|
continue
|
|
245
|
-
file_key = f"{
|
|
236
|
+
file_key = f"{file_group_id}_{date_str}"
|
|
246
237
|
if file_key in self._downloaded_files:
|
|
247
238
|
continue
|
|
248
239
|
if self._failed_files.get(file_key, 0) >= self.max_retries:
|
|
249
240
|
continue
|
|
250
|
-
if self._file_exists_locally(
|
|
241
|
+
if self._file_exists_locally(file_group_id, date_str):
|
|
251
242
|
self.stats["files_skipped"] += 1
|
|
252
243
|
self._downloaded_files.add(file_key)
|
|
253
244
|
continue
|
|
254
|
-
eligible.append((
|
|
245
|
+
eligible.append((file_group_id, date_str, file_key))
|
|
255
246
|
|
|
256
247
|
if not eligible:
|
|
257
248
|
return
|
|
@@ -265,20 +256,13 @@ class AutoDownloadManager:
|
|
|
265
256
|
async with semaphore:
|
|
266
257
|
await self._download_file(fid, dstr, fkey)
|
|
267
258
|
|
|
268
|
-
tasks = [
|
|
269
|
-
asyncio.create_task(worker(fid, dstr, fkey))
|
|
270
|
-
for fid, dstr, fkey in eligible
|
|
271
|
-
]
|
|
259
|
+
tasks = [asyncio.create_task(worker(fid, dstr, fkey)) for fid, dstr, fkey in eligible]
|
|
272
260
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
273
261
|
|
|
274
262
|
except Exception as e:
|
|
275
|
-
self.logger.error(
|
|
263
|
+
self.logger.error("Error checking available files: %s", e)
|
|
276
264
|
raise
|
|
277
265
|
|
|
278
|
-
async def _process_file(self, file_info):
|
|
279
|
-
"""Deprecated path; available-files flow supersedes this."""
|
|
280
|
-
return
|
|
281
|
-
|
|
282
266
|
def _get_dates_to_check(self) -> List[str]:
|
|
283
267
|
"""Get list of dates to check for files."""
|
|
284
268
|
if self.check_current_date_only:
|
|
@@ -293,11 +277,7 @@ class AutoDownloadManager:
|
|
|
293
277
|
dates.append(date.strftime("%Y%m%d"))
|
|
294
278
|
return dates
|
|
295
279
|
|
|
296
|
-
|
|
297
|
-
"""Deprecated path; available-files flow supersedes this."""
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
def _file_exists_locally(self, file_id: str, date_str: str) -> bool:
|
|
280
|
+
def _file_exists_locally(self, file_group_id: str, date_str: str) -> bool:
|
|
301
281
|
"""Check if file already exists in the destination directory."""
|
|
302
282
|
# Look for files that might match this file_group_id and date
|
|
303
283
|
# This is a heuristic approach since we don't know the exact filename
|
|
@@ -306,14 +286,14 @@ class AutoDownloadManager:
|
|
|
306
286
|
if file_path.is_file():
|
|
307
287
|
filename = file_path.name
|
|
308
288
|
# Check if filename contains the file_group_id and date
|
|
309
|
-
if
|
|
289
|
+
if file_group_id in filename and date_str in filename:
|
|
310
290
|
return True
|
|
311
291
|
|
|
312
292
|
return False
|
|
313
293
|
|
|
314
|
-
async def _download_file(self,
|
|
294
|
+
async def _download_file(self, file_group_id: str, date_str: str, file_key: str):
|
|
315
295
|
"""Download a file."""
|
|
316
|
-
self.logger.info(
|
|
296
|
+
self.logger.info("Downloading '%s' for %s", file_group_id, date_str)
|
|
317
297
|
|
|
318
298
|
# Create progress callback that updates statistics
|
|
319
299
|
def progress_wrapper(progress: DownloadProgress):
|
|
@@ -331,17 +311,18 @@ class AutoDownloadManager:
|
|
|
331
311
|
else:
|
|
332
312
|
self.progress_callback(progress)
|
|
333
313
|
except Exception as e:
|
|
334
|
-
self.logger.error(
|
|
314
|
+
self.logger.error("Error in progress callback: %s", e)
|
|
335
315
|
|
|
336
316
|
try:
|
|
337
317
|
result = await self.client.download_file_async(
|
|
338
|
-
file_group_id=
|
|
318
|
+
file_group_id=file_group_id,
|
|
339
319
|
file_datetime=date_str,
|
|
340
320
|
options=self.download_options,
|
|
341
321
|
progress_callback=progress_wrapper,
|
|
342
322
|
)
|
|
343
323
|
|
|
344
324
|
succeeded = False
|
|
325
|
+
already_exists = False
|
|
345
326
|
if result is not None and getattr(result, "status", None) is not None:
|
|
346
327
|
try:
|
|
347
328
|
from .models import ( # import inside to avoid cycles in some tools
|
|
@@ -349,19 +330,29 @@ class AutoDownloadManager:
|
|
|
349
330
|
)
|
|
350
331
|
|
|
351
332
|
succeeded = result.status == DownloadStatus.COMPLETED
|
|
333
|
+
already_exists = result.status == DownloadStatus.ALREADY_EXISTS
|
|
352
334
|
except Exception:
|
|
353
335
|
succeeded = False
|
|
354
336
|
|
|
355
|
-
if
|
|
356
|
-
self.logger.info(
|
|
337
|
+
if already_exists:
|
|
338
|
+
self.logger.info(
|
|
339
|
+
"File already exists for '%s' for %s, skipping",
|
|
340
|
+
file_group_id,
|
|
341
|
+
date_str,
|
|
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)
|
|
357
350
|
self.stats["files_downloaded"] += 1
|
|
358
351
|
self._downloaded_files.add(file_key)
|
|
359
352
|
# Accumulate total bytes if provided
|
|
360
353
|
try:
|
|
361
354
|
if getattr(result, "file_size", None):
|
|
362
|
-
self.stats["total_bytes_downloaded"] += (
|
|
363
|
-
int(result.file_size) or 0
|
|
364
|
-
)
|
|
355
|
+
self.stats["total_bytes_downloaded"] += int(result.file_size) or 0
|
|
365
356
|
except Exception:
|
|
366
357
|
pass
|
|
367
358
|
|
|
@@ -370,19 +361,23 @@ class AutoDownloadManager:
|
|
|
370
361
|
del self._failed_files[file_key]
|
|
371
362
|
|
|
372
363
|
else:
|
|
373
|
-
error_msg = (
|
|
374
|
-
getattr(result, "error_message", "Unknown error")
|
|
375
|
-
if result
|
|
376
|
-
else "No result"
|
|
377
|
-
)
|
|
364
|
+
error_msg = getattr(result, "error_message", "Unknown error") if result else "No result"
|
|
378
365
|
self.logger.error(
|
|
379
|
-
|
|
366
|
+
"Download failed for '%s' for %s: %s",
|
|
367
|
+
file_group_id,
|
|
368
|
+
date_str,
|
|
369
|
+
error_msg,
|
|
380
370
|
)
|
|
381
371
|
self._failed_files[file_key] = self._failed_files.get(file_key, 0) + 1
|
|
382
372
|
self.stats["download_failures"] += 1
|
|
383
373
|
|
|
384
374
|
except Exception as e:
|
|
385
|
-
self.logger.error(
|
|
375
|
+
self.logger.error(
|
|
376
|
+
"Exception downloading '%s' for %s: %s",
|
|
377
|
+
file_group_id,
|
|
378
|
+
date_str,
|
|
379
|
+
e,
|
|
380
|
+
)
|
|
386
381
|
self._failed_files[file_key] = self._failed_files.get(file_key, 0) + 1
|
|
387
382
|
self.stats["download_failures"] += 1
|
|
388
383
|
raise
|
|
@@ -395,7 +390,7 @@ class AutoDownloadManager:
|
|
|
395
390
|
else:
|
|
396
391
|
callback(*args, **kwargs)
|
|
397
392
|
except Exception as e:
|
|
398
|
-
self.logger.error(
|
|
393
|
+
self.logger.error("Error in callback: %s", e)
|
|
399
394
|
|
|
400
395
|
def get_stats(self) -> Dict[str, Any]:
|
|
401
396
|
"""Get auto-download statistics."""
|
|
@@ -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
|
-
|
|
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
|
|