dataquery-sdk 0.1.5__tar.gz → 0.2.0__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 (50) hide show
  1. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/CHANGELOG.md +4 -1
  2. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/CONTRIBUTING.md +2 -2
  3. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/PKG-INFO +162 -37
  4. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/README.md +160 -34
  5. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery/__init__.py +7 -7
  6. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery/cli.py +42 -10
  7. dataquery_sdk-0.2.0/dataquery/config/__init__.py +27 -0
  8. dataquery_sdk-0.2.0/dataquery/config/env.py +364 -0
  9. dataquery_sdk-0.2.0/dataquery/constants/__init__.py +85 -0
  10. dataquery_sdk-0.2.0/dataquery/constants/api.py +35 -0
  11. dataquery_sdk-0.2.0/dataquery/constants/download.py +51 -0
  12. dataquery_sdk-0.2.0/dataquery/constants/rate_limit.py +6 -0
  13. dataquery_sdk-0.2.0/dataquery/constants/sse.py +9 -0
  14. dataquery_sdk-0.2.0/dataquery/core/__init__.py +5 -0
  15. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/core}/_mixins.py +319 -21
  16. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/core}/client.py +357 -354
  17. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery/dataquery.py +372 -334
  18. dataquery_sdk-0.2.0/dataquery/download/__init__.py +14 -0
  19. dataquery_sdk-0.2.0/dataquery/download/parallel.py +701 -0
  20. dataquery_sdk-0.1.5/dataquery/_download_utils.py → dataquery_sdk-0.2.0/dataquery/download/utils.py +36 -9
  21. dataquery_sdk-0.2.0/dataquery/sse/__init__.py +23 -0
  22. dataquery_sdk-0.2.0/dataquery/sse/client.py +473 -0
  23. dataquery_sdk-0.2.0/dataquery/sse/event_store.py +241 -0
  24. dataquery_sdk-0.2.0/dataquery/sse/subscriber.py +653 -0
  25. dataquery_sdk-0.2.0/dataquery/transport/__init__.py +61 -0
  26. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/transport}/auth.py +6 -7
  27. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/transport}/connection_pool.py +1 -4
  28. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/transport}/rate_limiter.py +19 -28
  29. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/transport}/retry.py +2 -3
  30. dataquery_sdk-0.2.0/dataquery/types/__init__.py +79 -0
  31. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/types}/exceptions.py +22 -0
  32. {dataquery_sdk-0.1.5/dataquery → dataquery_sdk-0.2.0/dataquery/types}/models.py +49 -31
  33. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery/utils.py +3 -6
  34. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery_sdk.egg-info/PKG-INFO +162 -37
  35. dataquery_sdk-0.2.0/dataquery_sdk.egg-info/SOURCES.txt +43 -0
  36. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/pyproject.toml +3 -4
  37. dataquery_sdk-0.1.5/dataquery/_parallel_download.py +0 -300
  38. dataquery_sdk-0.1.5/dataquery/config.py +0 -564
  39. dataquery_sdk-0.1.5/dataquery/sse_client.py +0 -280
  40. dataquery_sdk-0.1.5/dataquery/sse_subscriber.py +0 -437
  41. dataquery_sdk-0.1.5/dataquery_sdk.egg-info/SOURCES.txt +0 -31
  42. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/LICENSE +0 -0
  43. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/MANIFEST.in +0 -0
  44. /dataquery_sdk-0.1.5/dataquery/logging_config.py → /dataquery_sdk-0.2.0/dataquery/config/logging.py +0 -0
  45. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery/py.typed +0 -0
  46. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery_sdk.egg-info/dependency_links.txt +0 -0
  47. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery_sdk.egg-info/entry_points.txt +0 -0
  48. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery_sdk.egg-info/requires.txt +0 -0
  49. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/dataquery_sdk.egg-info/top_level.txt +0 -0
  50. {dataquery_sdk-0.1.5 → dataquery_sdk-0.2.0}/setup.cfg +0 -0
@@ -26,4 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
26
26
  ## [0.1.4] - 2026-03-23
27
27
  - Introduced circuit breaker environment variable (`DATAQUERY_CIRCUIT_BREAKER_THRESHOLD`)
28
28
  ## [0.1.5] - 2026-04-16
29
- - Introduced file-group-id to the group downloads
29
+ - Introduced file-group-id to the group downloads
30
+ ## [0.2.0] - 2026-05-06
31
+ - SSE auto-download with cross-process event replay and multi-group support
32
+ - Python 3.11+ only
@@ -1,4 +1,4 @@
1
- # Contributing to DataQuery SDK
1
+ # Contributing to DataQuery SDK
2
2
 
3
3
  Thank you for your interest in contributing to the DataQuery SDK! This document provides guidelines and information for contributors.
4
4
 
@@ -6,7 +6,7 @@ Thank you for your interest in contributing to the DataQuery SDK! This document
6
6
 
7
7
  ### Prerequisites
8
8
 
9
- - Python 3.10 or higher
9
+ - Python 3.11 or higher
10
10
  - [uv](https://github.com/astral-sh/uv) package manager
11
11
  - Git
12
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataquery-sdk
3
- Version: 0.1.5
3
+ Version: 0.2.0
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 <dataquery_support@jpmorgan.com>
6
6
  Project-URL: Homepage, https://github.com/dataquery/dataquery-sdk
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Science/Research
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
21
20
  Classifier: Programming Language :: Python :: 3.13
@@ -23,7 +22,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
22
  Classifier: Topic :: Office/Business :: Financial
24
23
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
25
24
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
26
- Requires-Python: >=3.10
25
+ Requires-Python: >=3.11
27
26
  Description-Content-Type: text/markdown
28
27
  License-File: LICENSE
29
28
  Requires-Dist: aiohttp<4.0.0,>=3.8.0
@@ -67,15 +66,33 @@ Dynamic: license-file
67
66
 
68
67
  Python SDK for the J.P. Morgan DataQuery API — authenticated file downloads, time-series queries, and real-time notification-driven downloads with OAuth 2.0, rate limiting, and automatic retries built in.
69
68
 
70
- [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
69
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
71
70
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
72
71
  [![Linting: Ruff](https://img.shields.io/badge/linting-ruff-261230.svg)](https://github.com/astral-sh/ruff)
73
72
 
73
+ ## Contents
74
+
75
+ - [Features](#features)
76
+ - [New here? Three steps to your first download](#new-here-three-steps-to-your-first-download)
77
+ - [Installation](#installation)
78
+ - [Configure credentials](#configure-credentials)
79
+ - [Quick start](#quick-start)
80
+ - [Auto-download (coming soon)](#auto-download-coming-soon)
81
+ - [CLI](#cli)
82
+ - [Configuration](#configuration)
83
+ - [Logging](#logging)
84
+ - [Error handling](#error-handling)
85
+ - [Troubleshooting](#troubleshooting)
86
+ - [Date formats](#date-formats)
87
+ - [Performance tuning](#performance-tuning)
88
+ - [API reference (most-used methods)](#api-reference-most-used-methods)
89
+ - [Examples](#examples) · [Development](#development) · [Requirements](#requirements) · [Support](#support)
90
+
74
91
  ## Features
75
92
 
76
- - **Parallel file downloads** — when `num_parts > 1`, HTTP range requests split the file into parallel chunks with bounded concurrency; `num_parts=1` (default) uses a simple streaming GET
93
+ - **Streaming file downloads** — single streaming GET per file
77
94
  - **Historical and date-range downloads** — fetch every file in a group (optionally filtered to one or many `file-group-id`s) between two dates
78
- - **Notification-driven downloads (SSE)** — subscribe to the `/notification` stream and auto-download files as soon as they are published
95
+ - **Notification-driven downloads (SSE)** — subscribe to the `/events/notification` stream and auto-download files as soon as they are published
79
96
  - **Time-series queries** — by expression, by instrument, or by group with attribute / filter projections
80
97
  - **OAuth 2.0 with token caching and refresh** — or supply a bearer token directly
81
98
  - **Token-bucket rate limiter** — 300 rpm / 5 tps defaults (configurable up to API limits)
@@ -84,6 +101,28 @@ Python SDK for the J.P. Morgan DataQuery API — authenticated file downloads, t
84
101
  - **Optional pandas integration** — `to_dataframe(...)` on any response
85
102
  - **CLI** — `dataquery groups | files | availability | download | download-group | auth | config`
86
103
 
104
+ ## New here? Three steps to your first download
105
+
106
+ 1. `pip install dataquery-sdk`
107
+ 2. Put your OAuth credentials in a `.env` file (see [Configure credentials](#configure-credentials))
108
+ 3. Run a one-liner to confirm everything works:
109
+
110
+ ```python
111
+ import asyncio
112
+ from dataquery import DataQuery
113
+
114
+ async def main():
115
+ async with DataQuery() as dq:
116
+ groups = await dq.list_groups_async(limit=5)
117
+ for g in groups:
118
+ print(g.group_id, "—", g.group_name)
119
+
120
+ asyncio.run(main())
121
+ ```
122
+
123
+ If that prints groups, auth and networking are working. From there, jump to
124
+ [Quick start](#quick-start) (date-range download) or the [CLI](#cli).
125
+
87
126
  ## Installation
88
127
 
89
128
  ```bash
@@ -97,7 +136,7 @@ pip install "dataquery-sdk[pandas]"
97
136
  pip install "dataquery-sdk[dev]"
98
137
  ```
99
138
 
100
- Python 3.10+ is required.
139
+ Python 3.11+ is required.
101
140
 
102
141
  ## Configure credentials
103
142
 
@@ -140,9 +179,10 @@ async with DataQuery() as dq:
140
179
  end_date="20250131",
141
180
  destination_dir="./data",
142
181
  )
143
- print(f"{result['successful_downloads']}/{result['total_files']} files downloaded")
182
+ # OperationReport (Pydantic model) — counts/timing/data/details are dicts on it.
183
+ print(f"{result.counts['successful_downloads']}/{result.counts['total_files']} files downloaded")
144
184
 
145
- # sync
185
+ # sync — same arguments, drop the _async suffix
146
186
  with DataQuery() as dq:
147
187
  result = dq.run_group_download(
148
188
  group_id="JPMAQS_GENERIC_RETURNS",
@@ -179,7 +219,6 @@ async with DataQuery() as dq:
179
219
  file_group_id="JPMAQS_GENERIC_RETURNS",
180
220
  file_datetime="20250115",
181
221
  destination_path=Path("./downloads"),
182
- num_parts=5, # parallel chunks
183
222
  )
184
223
  print(f"Downloaded: {result.local_path} ({result.file_size} bytes)")
185
224
  ```
@@ -227,8 +266,8 @@ async with DataQuery() as dq:
227
266
 
228
267
  Real-time notification-driven downloads via the DataQuery SSE stream are under
229
268
  active development and will be available in a future release. Once released,
230
- `auto_download_async` will subscribe to the `/notification` endpoint and
231
- download files automatically as they are published — no polling required.
269
+ `auto_download_async` will subscribe to the notification endpoint and download
270
+ files automatically as they are published — no polling required.
232
271
 
233
272
  ## CLI
234
273
 
@@ -248,17 +287,16 @@ dataquery availability --file-group-id JPMAQS_GENERIC_RETURNS --file-datetime 20
248
287
  # Download a single file
249
288
  dataquery download --file-group-id JPMAQS_GENERIC_RETURNS \
250
289
  --file-datetime 20250115 \
251
- --destination ./downloads \
252
- --num-parts 5
290
+ --destination ./downloads
253
291
 
254
- # Watch a group and download as new files arrive
292
+ # Watch a group and download as new files arrive (calls auto_download_async under the hood —
293
+ # same SSE subscription, same event-replay state files)
255
294
  dataquery download --watch --group-id JPMAQS_GENERIC_RETURNS --destination ./downloads
256
295
 
257
296
  # Download everything in a date range
258
297
  dataquery download-group --group-id JPMAQS_GENERIC_RETURNS \
259
298
  --start-date 20250101 --end-date 20250131 \
260
- --destination ./data \
261
- --max-concurrent 5 --num-parts 4
299
+ --destination ./data
262
300
 
263
301
  # Restrict to one or more file-group-ids
264
302
  dataquery download-group --group-id JPMAQS_GENERIC_RETURNS \
@@ -291,8 +329,6 @@ All environment variables use the `DATAQUERY_` prefix.
291
329
  | `DATAQUERY_CLIENT_SECRET` | _(none)_ | Required for OAuth |
292
330
  | `DATAQUERY_BEARER_TOKEN` | _(none)_ | Alternative to OAuth |
293
331
  | `DATAQUERY_OAUTH_ENABLED` | `true` | Set `false` to use bearer-token mode |
294
- | `DATAQUERY_OAUTH_TOKEN_URL` | `https://authe.jpmorgan.com/as/token.oauth2` | |
295
- | `DATAQUERY_OAUTH_AUD` | `JPMC:URI:RS-06785-DataQueryExternalApi-PROD` | |
296
332
 
297
333
  **API endpoints**
298
334
 
@@ -342,13 +378,57 @@ async with DataQuery(client_id="...", client_secret="...", timeout=60.0) as dq:
342
378
  ...
343
379
  ```
344
380
 
381
+ ## Logging
382
+
383
+ The SDK logs through [structlog](https://www.structlog.org/) and emits
384
+ structured events for requests, retries, rate-limit waits, SSE reconnects, and
385
+ download progress. Two ways to drive it:
386
+
387
+ **Standard Python logging** — works without extra setup:
388
+
389
+ ```python
390
+ import logging
391
+ logging.basicConfig(level=logging.INFO)
392
+ logging.getLogger("dataquery").setLevel(logging.DEBUG) # SDK-only DEBUG
393
+ ```
394
+
395
+ DEBUG includes per-chunk download progress and SSE keepalives — useful while
396
+ debugging, noisy in production.
397
+
398
+ **Structured (JSON) output** — recommended for long-running `auto_download`
399
+ services so a log shipper can parse the events:
400
+
401
+ ```python
402
+ from pathlib import Path
403
+ from dataquery.config.logging import (
404
+ LogFormat, LogLevel, create_logging_config, create_logging_manager,
405
+ )
406
+
407
+ cfg = create_logging_config(
408
+ level=LogLevel.INFO,
409
+ format=LogFormat.JSON, # or LogFormat.CONSOLE for humans
410
+ enable_file=True,
411
+ log_file=Path("./dataquery.log"),
412
+ enable_request_logging=False, # set True to log every HTTP request/response
413
+ )
414
+ create_logging_manager(cfg) # installs handlers on the root logger
415
+ ```
416
+
417
+ `examples/system/enable_request_logging.py` is a runnable version showing
418
+ request/response logging for traffic debugging.
419
+
420
+ **Health snapshots for `auto_download`** — `manager.get_stats()` returns
421
+ notifications received, files downloaded / skipped / failed, the last event
422
+ id, and a bounded ring of recent errors. Wire it into a `/healthz` endpoint
423
+ for daemon-style deployments.
424
+
345
425
  ## Error handling
346
426
 
347
427
  All errors inherit from `DataQueryError`:
348
428
 
349
429
  ```python
350
430
  from dataquery import DataQuery
351
- from dataquery.exceptions import (
431
+ from dataquery.types.exceptions import (
352
432
  DataQueryError,
353
433
  AuthenticationError,
354
434
  NotFoundError,
@@ -375,6 +455,54 @@ async with DataQuery() as dq:
375
455
  ... # any other SDK-level failure
376
456
  ```
377
457
 
458
+ ## Troubleshooting
459
+
460
+ **`AuthenticationError` / HTTP 401 on the first call**
461
+
462
+ - Verify both `DATAQUERY_CLIENT_ID` and `DATAQUERY_CLIENT_SECRET` are set:
463
+ `dataquery config show` will print the resolved config (secrets masked).
464
+ - Confirm OAuth is reaching the right endpoint:
465
+ `dataquery auth test` performs a token exchange and reports the failure mode.
466
+ - If the credentials are correct but the audience is wrong, set
467
+ `DATAQUERY_OAUTH_AUD` to the value provisioned for your client.
468
+
469
+ **`.env` file isn't picked up**
470
+
471
+ - The SDK looks for `.env` in the current working directory at instantiation.
472
+ Either `cd` to the directory containing `.env` before running, or pass the
473
+ path explicitly: `DataQuery(env_file=".env.production")`.
474
+ - Variables already set in the shell environment win over the `.env` file —
475
+ unset them (`unset DATAQUERY_CLIENT_ID`) if you want the file to take effect.
476
+ - The CLI accepts `--env-file PATH` on every subcommand for the same reason.
477
+
478
+ **Connection / proxy / SSL failures**
479
+
480
+ - Behind a corporate proxy, set `DATAQUERY_PROXY_ENABLED=true` and
481
+ `DATAQUERY_PROXY_URL=http://proxy.host:port`. Add `DATAQUERY_PROXY_USERNAME`
482
+ / `DATAQUERY_PROXY_PASSWORD` if auth is required.
483
+ - For self-signed proxy CAs, set `DATAQUERY_PROXY_VERIFY_SSL=false` (insecure;
484
+ prefer pointing `SSL_CERT_FILE` at the corporate root CA bundle).
485
+ - Sporadic `NetworkError` after long idle periods usually means a stateful
486
+ middlebox is dropping the SSE socket — set
487
+ `heartbeat_timeout=90.0` on `auto_download_async` to force a reconnect when
488
+ no bytes arrive within the window.
489
+
490
+ **Rate-limit pauses**
491
+
492
+ - Default is 300 rpm / 5 tps. The SDK self-throttles via the token-bucket
493
+ limiter; if you see long sleeps before requests, lower
494
+ `DATAQUERY_REQUESTS_PER_MINUTE` is not the cure — it's likely working as
495
+ designed. Raise it (up to your provisioned limit) to go faster.
496
+ - `dq.get_rate_limit_info()` shows the current bucket state.
497
+
498
+ **SSE auto-download "missed" events after a restart**
499
+
500
+ - Confirm `enable_event_replay=True` (the default).
501
+ - Replay state lives under `<destination>/.sse_state/sse_<fingerprint>.json` —
502
+ if that directory was wiped, the next start has nothing to resume from. Use
503
+ `manager.clear_event_id()` (or `dataquery download --watch --reset-event-id`)
504
+ only when you intentionally want a clean slate.
505
+
378
506
  ## Date formats
379
507
 
380
508
  ```python
@@ -388,13 +516,9 @@ start_date="TODAY-1Y"
388
516
 
389
517
  ## Performance tuning
390
518
 
391
- `run_group_download_async` uses a flattened concurrency model total concurrent
392
- HTTP requests = `max_concurrent × num_parts`.
393
-
394
- With `num_parts=1` (the default) each file downloads as a single streaming GET.
395
- Set `num_parts > 1` to enable parallel HTTP range requests per file — the SDK
396
- probes the file size first, then downloads byte ranges concurrently. Files under
397
- 10 MB always use a single stream regardless of `num_parts`.
519
+ `run_group_download_async` streams each file as a single GET. The SDK
520
+ automatically inserts delays between file starts so the configured
521
+ `requests_per_minute` is not exceeded.
398
522
 
399
523
  ```python
400
524
  await dq.run_group_download_async(
@@ -402,15 +526,13 @@ await dq.run_group_download_async(
402
526
  start_date="20250101",
403
527
  end_date="20250131",
404
528
  destination_dir="./data",
405
- max_concurrent=5, # parallel files
406
- num_parts=4, # parallel chunks per file (1 = single stream)
407
529
  max_retries=3,
408
530
  )
409
531
  ```
410
532
 
411
- Typical settings: `max_concurrent` 3–5, `num_parts` 2–8. The SDK automatically
412
- inserts delays between file starts so the configured `requests_per_minute` is not
413
- exceeded.
533
+ Tune throughput via `DATAQUERY_REQUESTS_PER_MINUTE` and `DATAQUERY_BURST_CAPACITY`
534
+ (see [Configuration](#environment-variables)) rather than per-call concurrency
535
+ flags.
414
536
 
415
537
  ## API reference (most-used methods)
416
538
 
@@ -437,8 +559,10 @@ exceeded.
437
559
  | | `to_dataframe(response)` | requires `pandas` extra |
438
560
  | | `get_stats()` / `get_pool_stats()` / `get_rate_limit_info()` | diagnostics |
439
561
 
440
- Every async method above has a sync counterpart (without the `_async` suffix, or
441
- with an explicit `_sync` suffix) that runs the coroutine via `asyncio.run`.
562
+ Every async method has a sync counterpart with the same name minus the
563
+ `_async` suffix `list_groups_async` `list_groups`, `download_file_async`
564
+ `download_file`, etc. Sync calls run the coroutine internally via
565
+ `asyncio.run`, so do not call them from inside an existing event loop.
442
566
 
443
567
  ## Examples
444
568
 
@@ -449,13 +573,14 @@ The `examples/` directory is organised by feature:
449
573
  - `examples/instruments/` — instrument discovery + time series
450
574
  - `examples/groups/` and `examples/groups_advanced/` — group discovery and time series
451
575
  - `examples/grid/` — grid data
452
- - `examples/system/` — SSE notification subscriber, diagnostics
576
+ - `examples/system/` — SSE notification subscriber (single + multi-group), diagnostics
453
577
 
454
578
  Run any example directly:
455
579
 
456
580
  ```bash
457
581
  python examples/files/download_file.py
458
- python examples/system/sse_local_server_example.py
582
+ python examples/system/auto_download_example.py # single group
583
+ python examples/system/auto_download_multi_group_example.py # several groups in parallel
459
584
  ```
460
585
 
461
586
  ## Development
@@ -484,7 +609,7 @@ Pytest markers: `slow`, `integration`, `unit`, `asyncio`.
484
609
 
485
610
  ## Requirements
486
611
 
487
- - Python 3.10+
612
+ - Python 3.11+
488
613
  - `aiohttp>=3.8,<4`, `pydantic>=2,<3`, `structlog>=23`, `python-dotenv>=1`
489
614
  - Optional: `pandas>=2` (for `to_dataframe`)
490
615