dataquery-sdk 0.0.7__py3-none-any.whl
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/__init__.py +215 -0
- dataquery/auth.py +380 -0
- dataquery/auto_download.py +448 -0
- dataquery/cli.py +244 -0
- dataquery/client.py +2825 -0
- dataquery/config.py +571 -0
- dataquery/connection_pool.py +538 -0
- dataquery/dataquery.py +2467 -0
- dataquery/exceptions.py +169 -0
- dataquery/logging_config.py +415 -0
- dataquery/models.py +1303 -0
- dataquery/py.typed +2 -0
- dataquery/rate_limiter.py +520 -0
- dataquery/retry.py +406 -0
- dataquery/utils.py +525 -0
- dataquery_sdk-0.0.7.dist-info/METADATA +996 -0
- dataquery_sdk-0.0.7.dist-info/RECORD +21 -0
- dataquery_sdk-0.0.7.dist-info/WHEEL +5 -0
- dataquery_sdk-0.0.7.dist-info/entry_points.txt +2 -0
- dataquery_sdk-0.0.7.dist-info/licenses/LICENSE +201 -0
- dataquery_sdk-0.0.7.dist-info/top_level.txt +1 -0
dataquery/utils.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for the DATAQUERY SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from .models import ClientConfig
|
|
12
|
+
|
|
13
|
+
# Note: load_dotenv is imported where used to avoid unused import in environments
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_env_template(env_file: Optional[Path] = None) -> Path:
|
|
20
|
+
"""
|
|
21
|
+
Create a .env template file with all available configuration options.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
env_file: Path to the template file (default: .env.template)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to the created template file
|
|
28
|
+
"""
|
|
29
|
+
template_file = env_file or Path(".env.template")
|
|
30
|
+
|
|
31
|
+
# Validate the path
|
|
32
|
+
if not isinstance(template_file, Path):
|
|
33
|
+
template_file = Path(template_file)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Ensure parent directory exists
|
|
37
|
+
template_file.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
template_content = """# DATAQUERY SDK Configuration Template
|
|
40
|
+
# Copy this file to .env and update the values according to your setup
|
|
41
|
+
# cp .env.template .env
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# REQUIRED: API Configuration
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
# Base URL of the DATAQUERY API (REQUIRED)
|
|
48
|
+
# Example: https://api-developer.jpmorgan.com (Production)
|
|
49
|
+
# Example: https://api-staging.dataquery.com
|
|
50
|
+
DATAQUERY_BASE_URL=https://api-developer.jpmorgan.com
|
|
51
|
+
|
|
52
|
+
# Optional: Separate host for file endpoints
|
|
53
|
+
# If set, file availability/list/download will use this host instead of DATAQUERY_BASE_URL
|
|
54
|
+
# DATAQUERY_FILES_BASE_URL=https://files-api.example.com
|
|
55
|
+
# DATAQUERY_FILES_CONTEXT_PATH=/research/dataquery-authe/api/v2
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# AUTHENTICATION: OAuth 2.0 Configuration (Recommended)
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
# Enable OAuth authentication (true/false)
|
|
62
|
+
# Set to false if using Bearer token authentication
|
|
63
|
+
DATAQUERY_OAUTH_ENABLED=true
|
|
64
|
+
|
|
65
|
+
# OAuth token endpoint URL
|
|
66
|
+
# Usually: {BASE_URL}/oauth/token
|
|
67
|
+
DATAQUERY_OAUTH_TOKEN_URL=https://api-developer.jpmorgan.com/oauth/token
|
|
68
|
+
|
|
69
|
+
# OAuth client credentials (REQUIRED if OAuth enabled)
|
|
70
|
+
# Get these from your DATAQUERY account dashboard
|
|
71
|
+
DATAQUERY_CLIENT_ID=your_client_id_here
|
|
72
|
+
DATAQUERY_CLIENT_SECRET=your_client_secret_here
|
|
73
|
+
|
|
74
|
+
# OAuth scope removed
|
|
75
|
+
|
|
76
|
+
# OAuth audience (optional)
|
|
77
|
+
# Example: api://default or a full audience URI
|
|
78
|
+
DATAQUERY_OAUTH_AUD=
|
|
79
|
+
|
|
80
|
+
# OAuth grant type (usually client_credentials)
|
|
81
|
+
DATAQUERY_GRANT_TYPE=client_credentials
|
|
82
|
+
|
|
83
|
+
# =============================================================================
|
|
84
|
+
# AUTHENTICATION: Bearer Token Configuration (Alternative)
|
|
85
|
+
# =============================================================================
|
|
86
|
+
|
|
87
|
+
# Bearer token for direct API access (alternative to OAuth)
|
|
88
|
+
# Use this if you already have a Bearer token
|
|
89
|
+
# Leave empty if using OAuth authentication
|
|
90
|
+
DATAQUERY_BEARER_TOKEN=
|
|
91
|
+
|
|
92
|
+
# Token refresh threshold in seconds (default: 300 = 5 minutes)
|
|
93
|
+
# Refresh token when it expires within this many seconds
|
|
94
|
+
DATAQUERY_TOKEN_REFRESH_THRESHOLD=300
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# HTTP Configuration
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
# Request timeout in seconds (default: 600.0)
|
|
101
|
+
DATAQUERY_TIMEOUT=600.0
|
|
102
|
+
|
|
103
|
+
# Maximum retry attempts for failed requests (default: 3)
|
|
104
|
+
DATAQUERY_MAX_RETRIES=3
|
|
105
|
+
|
|
106
|
+
# Delay between retries in seconds (default: 1.0)
|
|
107
|
+
DATAQUERY_RETRY_DELAY=1.0
|
|
108
|
+
|
|
109
|
+
# Number of connection pools (default: 10)
|
|
110
|
+
DATAQUERY_POOL_CONNECTIONS=10
|
|
111
|
+
|
|
112
|
+
# Maximum connections per pool (default: 20)
|
|
113
|
+
DATAQUERY_POOL_MAXSIZE=20
|
|
114
|
+
|
|
115
|
+
# =============================================================================
|
|
116
|
+
# Rate Limiting Configuration
|
|
117
|
+
# =============================================================================
|
|
118
|
+
|
|
119
|
+
# Requests per minute limit (default: 100)
|
|
120
|
+
DATAQUERY_REQUESTS_PER_MINUTE=100
|
|
121
|
+
|
|
122
|
+
# Burst capacity for rate limiting (default: 20)
|
|
123
|
+
DATAQUERY_BURST_CAPACITY=20
|
|
124
|
+
|
|
125
|
+
# =============================================================================
|
|
126
|
+
# Logging Configuration
|
|
127
|
+
# =============================================================================
|
|
128
|
+
|
|
129
|
+
# Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
130
|
+
# Default: INFO
|
|
131
|
+
DATAQUERY_LOG_LEVEL=INFO
|
|
132
|
+
|
|
133
|
+
# Enable debug logging (true/false)
|
|
134
|
+
# Default: false
|
|
135
|
+
DATAQUERY_ENABLE_DEBUG_LOGGING=false
|
|
136
|
+
|
|
137
|
+
# =============================================================================
|
|
138
|
+
# Download Configuration
|
|
139
|
+
# =============================================================================
|
|
140
|
+
|
|
141
|
+
# Base download directory (default: ./downloads)
|
|
142
|
+
# All downloaded files will be saved to this directory
|
|
143
|
+
DATAQUERY_DOWNLOAD_DIR=./downloads
|
|
144
|
+
|
|
145
|
+
# Create parent directories if they don't exist (true/false)
|
|
146
|
+
# Default: true
|
|
147
|
+
DATAQUERY_CREATE_DIRECTORIES=true
|
|
148
|
+
|
|
149
|
+
# Overwrite existing files (true/false)
|
|
150
|
+
# Default: false
|
|
151
|
+
DATAQUERY_OVERWRITE_EXISTING=false
|
|
152
|
+
|
|
153
|
+
# =============================================================================
|
|
154
|
+
# Download Subdirectories (Optional)
|
|
155
|
+
# =============================================================================
|
|
156
|
+
|
|
157
|
+
# Workflow downloads subdirectory (default: workflow)
|
|
158
|
+
DATAQUERY_WORKFLOW_DIR=workflow
|
|
159
|
+
|
|
160
|
+
# Groups downloads subdirectory (default: groups)
|
|
161
|
+
DATAQUERY_GROUPS_DIR=groups
|
|
162
|
+
|
|
163
|
+
# Availability downloads subdirectory (default: availability)
|
|
164
|
+
DATAQUERY_AVAILABILITY_DIR=availability
|
|
165
|
+
|
|
166
|
+
# Default downloads subdirectory (default: files)
|
|
167
|
+
DATAQUERY_DEFAULT_DIR=files
|
|
168
|
+
|
|
169
|
+
# =============================================================================
|
|
170
|
+
# Advanced Configuration (Optional)
|
|
171
|
+
# =============================================================================
|
|
172
|
+
|
|
173
|
+
# User agent string for HTTP requests
|
|
174
|
+
# Default: DATAQUERY-SDK/1.0.0
|
|
175
|
+
DATAQUERY_USER_AGENT=DATAQUERY-SDK/1.0.0
|
|
176
|
+
|
|
177
|
+
# Enable HTTP/2 support (true/false)
|
|
178
|
+
# Default: true
|
|
179
|
+
DATAQUERY_ENABLE_HTTP2=true
|
|
180
|
+
|
|
181
|
+
# Connection keepalive timeout in seconds
|
|
182
|
+
# Default: 30
|
|
183
|
+
DATAQUERY_KEEPALIVE_TIMEOUT=30
|
|
184
|
+
|
|
185
|
+
# Enable connection pooling (true/false)
|
|
186
|
+
# Default: true
|
|
187
|
+
DATAQUERY_ENABLE_CONNECTION_POOLING=true
|
|
188
|
+
|
|
189
|
+
# =============================================================================
|
|
190
|
+
# Development Configuration (Optional)
|
|
191
|
+
# =============================================================================
|
|
192
|
+
|
|
193
|
+
# Enable development mode (true/false)
|
|
194
|
+
# Default: false
|
|
195
|
+
DATAQUERY_DEVELOPMENT_MODE=false
|
|
196
|
+
|
|
197
|
+
# Development API base URL (used when development mode is enabled)
|
|
198
|
+
DATAQUERY_DEV_BASE_URL=https://api-dev.dataquery.com
|
|
199
|
+
|
|
200
|
+
# Enable request/response logging (true/false)
|
|
201
|
+
# Default: false
|
|
202
|
+
DATAQUERY_LOG_REQUESTS=false
|
|
203
|
+
|
|
204
|
+
# =============================================================================
|
|
205
|
+
# Example Configurations
|
|
206
|
+
# =============================================================================
|
|
207
|
+
|
|
208
|
+
# Bearer Token Configuration Example:
|
|
209
|
+
# DATAQUERY_BASE_URL=https://api.dataquery.com
|
|
210
|
+
# DATAQUERY_OAUTH_ENABLED=false
|
|
211
|
+
# DATAQUERY_BEARER_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
template_file.write_text(template_content)
|
|
215
|
+
|
|
216
|
+
logger.info("Created .env template", file=str(template_file))
|
|
217
|
+
return template_file
|
|
218
|
+
|
|
219
|
+
except (OSError, IOError) as e:
|
|
220
|
+
logger.error(
|
|
221
|
+
"Failed to create .env template", file=str(template_file), error=str(e)
|
|
222
|
+
)
|
|
223
|
+
raise
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error("Unexpected error creating .env template", error=str(e))
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def save_config_to_env(config: ClientConfig, env_file: Optional[Path] = None) -> Path:
|
|
230
|
+
"""
|
|
231
|
+
Save configuration to .env file.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
config: Client configuration to save
|
|
235
|
+
env_file: Path to the .env file (default: .env)
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Path to the saved .env file
|
|
239
|
+
"""
|
|
240
|
+
env_file = env_file or Path(".env")
|
|
241
|
+
|
|
242
|
+
# Ensure env_file is a Path object
|
|
243
|
+
if not isinstance(env_file, Path):
|
|
244
|
+
env_file = Path(env_file)
|
|
245
|
+
|
|
246
|
+
env_content = f"""# DATAQUERY SDK Configuration
|
|
247
|
+
|
|
248
|
+
# API Configuration
|
|
249
|
+
DATAQUERY_BASE_URL={config.base_url}
|
|
250
|
+
|
|
251
|
+
# OAuth Configuration
|
|
252
|
+
DATAQUERY_OAUTH_ENABLED={str(config.oauth_enabled).lower()}
|
|
253
|
+
DATAQUERY_OAUTH_TOKEN_URL={config.oauth_token_url or ''}
|
|
254
|
+
DATAQUERY_CLIENT_ID={config.client_id or ''}
|
|
255
|
+
DATAQUERY_CLIENT_SECRET={config.client_secret or ''}
|
|
256
|
+
## scope removed
|
|
257
|
+
DATAQUERY_OAUTH_AUD={getattr(config, 'aud', '') or ''}
|
|
258
|
+
DATAQUERY_GRANT_TYPE={config.grant_type}
|
|
259
|
+
|
|
260
|
+
# Bearer Token Configuration
|
|
261
|
+
DATAQUERY_BEARER_TOKEN={config.bearer_token or ''}
|
|
262
|
+
DATAQUERY_TOKEN_REFRESH_THRESHOLD={config.token_refresh_threshold}
|
|
263
|
+
|
|
264
|
+
# HTTP Configuration
|
|
265
|
+
DATAQUERY_TIMEOUT={config.timeout}
|
|
266
|
+
DATAQUERY_MAX_RETRIES={config.max_retries}
|
|
267
|
+
DATAQUERY_RETRY_DELAY={config.retry_delay}
|
|
268
|
+
DATAQUERY_POOL_CONNECTIONS={config.pool_connections}
|
|
269
|
+
DATAQUERY_POOL_MAXSIZE={config.pool_maxsize}
|
|
270
|
+
|
|
271
|
+
# Rate Limiting
|
|
272
|
+
DATAQUERY_REQUESTS_PER_MINUTE={config.requests_per_minute}
|
|
273
|
+
DATAQUERY_BURST_CAPACITY={config.burst_capacity}
|
|
274
|
+
|
|
275
|
+
# Logging
|
|
276
|
+
DATAQUERY_LOG_LEVEL={config.log_level}
|
|
277
|
+
DATAQUERY_ENABLE_DEBUG_LOGGING={str(config.enable_debug_logging).lower()}
|
|
278
|
+
|
|
279
|
+
# Download Configuration
|
|
280
|
+
DATAQUERY_DOWNLOAD_DIR={config.download_dir}
|
|
281
|
+
DATAQUERY_CREATE_DIRECTORIES={str(config.create_directories).lower()}
|
|
282
|
+
DATAQUERY_OVERWRITE_EXISTING={str(config.overwrite_existing).lower()}
|
|
283
|
+
|
|
284
|
+
# Download Subdirectories
|
|
285
|
+
DATAQUERY_WORKFLOW_DIR=workflow
|
|
286
|
+
DATAQUERY_GROUPS_DIR=groups
|
|
287
|
+
DATAQUERY_AVAILABILITY_DIR=availability
|
|
288
|
+
DATAQUERY_DEFAULT_DIR=files
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
env_file.write_text(env_content)
|
|
292
|
+
|
|
293
|
+
logger.info("Saved configuration to .env file", file=str(env_file))
|
|
294
|
+
return env_file
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def load_env_file(env_file: Optional[Path] = None) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Load environment variables from .env file.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
env_file: Path to the .env file (default: .env)
|
|
303
|
+
"""
|
|
304
|
+
try:
|
|
305
|
+
from dotenv import load_dotenv # pylint: disable=import-outside-toplevel
|
|
306
|
+
except ImportError:
|
|
307
|
+
logger.warning("python-dotenv not installed, skipping .env file loading")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
env_file = env_file or Path(".env")
|
|
311
|
+
|
|
312
|
+
# Ensure env_file is a Path object
|
|
313
|
+
if not isinstance(env_file, Path):
|
|
314
|
+
env_file = Path(env_file)
|
|
315
|
+
|
|
316
|
+
# For compatibility with tests: only call loader if file exists
|
|
317
|
+
# Use Path.exists as an unbound method to cooperate with tests that patch pathlib.Path.exists
|
|
318
|
+
if Path.exists(env_file):
|
|
319
|
+
try:
|
|
320
|
+
_ = load_dotenv(env_file)
|
|
321
|
+
except Exception:
|
|
322
|
+
logger.warning("Failed to load .env file", file=str(env_file))
|
|
323
|
+
return
|
|
324
|
+
logger.info("Loaded environment variables from .env file", file=str(env_file))
|
|
325
|
+
else:
|
|
326
|
+
logger.warning("No .env file found", file=str(env_file))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_env_value(key: str, default: Optional[str] = None) -> Optional[str]:
|
|
330
|
+
"""
|
|
331
|
+
Get environment variable value with optional default.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
key: Environment variable name
|
|
335
|
+
default: Default value if not found
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Environment variable value or default
|
|
339
|
+
"""
|
|
340
|
+
return os.getenv(key, default)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def set_env_value(key: str, value: str) -> None:
|
|
344
|
+
"""
|
|
345
|
+
Set environment variable value.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
key: Environment variable name
|
|
349
|
+
value: Value to set
|
|
350
|
+
"""
|
|
351
|
+
os.environ[key] = value
|
|
352
|
+
logger.debug("Set environment variable", key=key, value=value)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def validate_env_config() -> None:
|
|
356
|
+
"""
|
|
357
|
+
Validate that required environment variables are set.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
ValueError: If required variables are missing or invalid
|
|
361
|
+
"""
|
|
362
|
+
# Validate numeric values if present
|
|
363
|
+
timeout = get_env_value("DATAQUERY_TIMEOUT")
|
|
364
|
+
if timeout:
|
|
365
|
+
try:
|
|
366
|
+
float(timeout)
|
|
367
|
+
except ValueError:
|
|
368
|
+
raise ValueError(f"Invalid timeout value: {timeout}")
|
|
369
|
+
|
|
370
|
+
max_retries = get_env_value("DATAQUERY_MAX_RETRIES")
|
|
371
|
+
if max_retries:
|
|
372
|
+
try:
|
|
373
|
+
int(max_retries)
|
|
374
|
+
except ValueError:
|
|
375
|
+
raise ValueError(f"Invalid max retries value: {max_retries}")
|
|
376
|
+
|
|
377
|
+
# Validate boolean values if present
|
|
378
|
+
oauth_enabled_val = get_env_value("DATAQUERY_OAUTH_ENABLED")
|
|
379
|
+
if oauth_enabled_val and oauth_enabled_val.lower() not in ("true", "false"):
|
|
380
|
+
raise ValueError(f"Invalid OAuth enabled value: {oauth_enabled_val}")
|
|
381
|
+
|
|
382
|
+
# Check required variables - BASE_URL is always required
|
|
383
|
+
base_url = get_env_value("DATAQUERY_BASE_URL")
|
|
384
|
+
if not base_url or not base_url.startswith(("http://", "https://")):
|
|
385
|
+
raise ValueError("DATAQUERY_BASE_URL is required")
|
|
386
|
+
|
|
387
|
+
# Validate OAuth configuration
|
|
388
|
+
oauth_enabled_val = get_env_value("DATAQUERY_OAUTH_ENABLED", "false")
|
|
389
|
+
oauth_enabled = (oauth_enabled_val or "false").lower() == "true"
|
|
390
|
+
if oauth_enabled:
|
|
391
|
+
client_id = get_env_value("DATAQUERY_CLIENT_ID")
|
|
392
|
+
client_secret = get_env_value("DATAQUERY_CLIENT_SECRET")
|
|
393
|
+
if (
|
|
394
|
+
not client_id
|
|
395
|
+
or not client_secret
|
|
396
|
+
or client_id.strip() == ""
|
|
397
|
+
or client_secret.strip() == ""
|
|
398
|
+
):
|
|
399
|
+
raise ValueError("OAuth credentials are required")
|
|
400
|
+
|
|
401
|
+
# Check if either OAuth or Bearer token is configured
|
|
402
|
+
if not oauth_enabled:
|
|
403
|
+
bearer_token = get_env_value("DATAQUERY_BEARER_TOKEN")
|
|
404
|
+
if not bearer_token or bearer_token.strip() == "":
|
|
405
|
+
# Only require authentication if OAuth is explicitly enabled
|
|
406
|
+
# If OAuth is disabled and no bearer token, that's okay for testing
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
logger.info("Environment configuration validation passed")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def format_file_size(size_bytes: int) -> str:
|
|
413
|
+
"""Format file size in human-readable format (bytes no decimals, KB+ one decimal)."""
|
|
414
|
+
if size_bytes == 0:
|
|
415
|
+
return "0 B"
|
|
416
|
+
|
|
417
|
+
# Handle negative values
|
|
418
|
+
if size_bytes < 0:
|
|
419
|
+
abs_size = abs(size_bytes)
|
|
420
|
+
size_names = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
|
|
421
|
+
i = 0
|
|
422
|
+
size_float = float(abs_size)
|
|
423
|
+
while size_float >= 1024 and i < len(size_names) - 1:
|
|
424
|
+
size_float /= 1024.0
|
|
425
|
+
i += 1
|
|
426
|
+
|
|
427
|
+
if i == 0: # Bytes
|
|
428
|
+
return f"-{size_float:.0f} {size_names[i]}"
|
|
429
|
+
else:
|
|
430
|
+
return f"-{size_float:.1f} {size_names[i]}"
|
|
431
|
+
|
|
432
|
+
size_names = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
|
|
433
|
+
i = 0
|
|
434
|
+
size_float = float(size_bytes)
|
|
435
|
+
while size_float >= 1024 and i < len(size_names) - 1:
|
|
436
|
+
size_float /= 1024.0
|
|
437
|
+
i += 1
|
|
438
|
+
|
|
439
|
+
if i == 0: # Bytes
|
|
440
|
+
return f"{size_float:.0f} {size_names[i]}"
|
|
441
|
+
else:
|
|
442
|
+
return f"{size_float:.1f} {size_names[i]}"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def format_duration(seconds: float) -> str:
|
|
446
|
+
"""Format duration with verbose style: Xm Ys or Xh Ym Zs where applicable."""
|
|
447
|
+
if seconds == 0:
|
|
448
|
+
return "0s"
|
|
449
|
+
|
|
450
|
+
# Handle negative values
|
|
451
|
+
if seconds < 0:
|
|
452
|
+
abs_seconds = abs(seconds)
|
|
453
|
+
if abs_seconds < 60:
|
|
454
|
+
return f"-{abs_seconds:.1f}s"
|
|
455
|
+
elif abs_seconds < 3600:
|
|
456
|
+
minutes = int(abs_seconds // 60)
|
|
457
|
+
remaining_seconds = int(abs_seconds % 60)
|
|
458
|
+
if remaining_seconds == 0:
|
|
459
|
+
return f"-{minutes}m"
|
|
460
|
+
else:
|
|
461
|
+
return f"-{minutes}m {remaining_seconds}s"
|
|
462
|
+
else:
|
|
463
|
+
hours = int(abs_seconds // 3600)
|
|
464
|
+
remaining_minutes = int((abs_seconds % 3600) // 60)
|
|
465
|
+
remaining_seconds = int(abs_seconds % 60)
|
|
466
|
+
if remaining_minutes == 0 and remaining_seconds == 0:
|
|
467
|
+
return f"-{hours}h"
|
|
468
|
+
elif remaining_seconds == 0:
|
|
469
|
+
return f"-{hours}h {remaining_minutes}m"
|
|
470
|
+
else:
|
|
471
|
+
return f"-{hours}h {remaining_minutes}m {remaining_seconds}s"
|
|
472
|
+
|
|
473
|
+
if seconds < 60:
|
|
474
|
+
return f"{seconds:.1f}s"
|
|
475
|
+
elif seconds < 3600:
|
|
476
|
+
minutes = int(seconds // 60)
|
|
477
|
+
remaining_seconds = int(seconds % 60)
|
|
478
|
+
if remaining_seconds == 0:
|
|
479
|
+
return f"{minutes}m"
|
|
480
|
+
else:
|
|
481
|
+
return f"{minutes}m {remaining_seconds}s"
|
|
482
|
+
else:
|
|
483
|
+
hours = int(seconds // 3600)
|
|
484
|
+
remaining_minutes = int((seconds % 3600) // 60)
|
|
485
|
+
remaining_seconds = int(seconds % 60)
|
|
486
|
+
if remaining_minutes == 0 and remaining_seconds == 0:
|
|
487
|
+
return f"{hours}h"
|
|
488
|
+
elif remaining_seconds == 0:
|
|
489
|
+
return f"{hours}h {remaining_minutes}m"
|
|
490
|
+
else:
|
|
491
|
+
return f"{hours}h {remaining_minutes}m {remaining_seconds}s"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def ensure_directory(path) -> Path:
|
|
495
|
+
"""Ensure directory exists and return the path."""
|
|
496
|
+
# Convert string to Path if needed
|
|
497
|
+
if not isinstance(path, Path):
|
|
498
|
+
path = Path(path)
|
|
499
|
+
|
|
500
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
501
|
+
return path
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def get_download_paths(base_dir: Optional[Path] = None) -> dict:
|
|
505
|
+
"""Get download paths from environment variables with defaults."""
|
|
506
|
+
import os
|
|
507
|
+
from pathlib import Path
|
|
508
|
+
|
|
509
|
+
if base_dir is None:
|
|
510
|
+
base_download_dir = Path(os.getenv("DATAQUERY_DOWNLOAD_DIR", "./downloads"))
|
|
511
|
+
else:
|
|
512
|
+
# Convert string to Path if needed
|
|
513
|
+
if not isinstance(base_dir, Path):
|
|
514
|
+
base_download_dir = Path(base_dir)
|
|
515
|
+
else:
|
|
516
|
+
base_download_dir = base_dir
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
"base": base_download_dir,
|
|
520
|
+
"workflow": base_download_dir / os.getenv("DATAQUERY_WORKFLOW_DIR", "workflow"),
|
|
521
|
+
"groups": base_download_dir / os.getenv("DATAQUERY_GROUPS_DIR", "groups"),
|
|
522
|
+
"availability": base_download_dir
|
|
523
|
+
/ os.getenv("DATAQUERY_AVAILABILITY_DIR", "availability"),
|
|
524
|
+
"default": base_download_dir / os.getenv("DATAQUERY_DEFAULT_DIR", "files"),
|
|
525
|
+
}
|