zpdatafetch 2.1.0__tar.gz → 2.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.
- {zpdatafetch-2.1.0/src/zpdatafetch.egg-info → zpdatafetch-2.1.2}/PKG-INFO +29 -5
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/README.md +27 -3
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/pyproject.toml +2 -6
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/cli.py +4 -3
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/validation.py +40 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/cli.py +6 -5
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zp.py +0 -1
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpcyclistfetch.py +1 -1
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpprimesfetch.py +3 -4
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpraceresult.py +10 -5
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2/src/zpdatafetch.egg-info}/PKG-INFO +29 -5
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/requires.txt +1 -1
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/cli.py +34 -10
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrraceresult.py +12 -5
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrteamroster.py +12 -5
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/LICENSE +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/setup.cfg +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/__init__.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/error_helpers.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/exceptions.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/http_client.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/json_helpers.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/logging.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/__init__.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/activity.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/auth.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/cli.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/followers.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/profile.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/rideons.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/ridersinworld.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/worlds.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/__init__.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/async_zp.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zp_obj.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zp_utils.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpcyclist.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpleague.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpleaguefetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpprime.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracefinish.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracelog.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracesignup.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracesprint.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpresultfetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpsignupfetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpsprintsfetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpteam.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpteamfetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/SOURCES.txt +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/dependency_links.txt +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/entry_points.txt +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/top_level.txt +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/__init__.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/async_zr.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/rate_limiter.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zr.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zr_utils.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrresultfetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrrider.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrriderfetch.py +0 -0
- {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrteamfetch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zpdatafetch
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: A package for fetching data from Zwiftpower and Zwiftracing.app
|
|
5
5
|
Author-email: Doug Morris <doug@mhost.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: anyio>=4.4.0
|
|
15
15
|
Requires-Dist: beautifulsoup4>=4.12.3
|
|
16
|
-
Requires-Dist: cryptography>=
|
|
16
|
+
Requires-Dist: cryptography>=46.0.5
|
|
17
17
|
Requires-Dist: httpx>=0.28.0
|
|
18
18
|
Requires-Dist: keyring>=25.2.0
|
|
19
19
|
Requires-Dist: keyrings-alt
|
|
@@ -295,7 +295,7 @@ including rider ratings, race results, and team rosters.
|
|
|
295
295
|
|
|
296
296
|
```sh
|
|
297
297
|
usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--v1fetch] [--noaction] [--sync]
|
|
298
|
-
[--batch] [--batch-file FILE] [--premium]
|
|
298
|
+
[--batch] [--batch-file FILE] [--premium] [--at DATETIME]
|
|
299
299
|
[{config,rider,result,team}] [id ...]
|
|
300
300
|
|
|
301
301
|
Module for fetching Zwiftracing data using the Zwiftracing API
|
|
@@ -317,6 +317,7 @@ options:
|
|
|
317
317
|
--batch use batch POST endpoint for multiple IDs (rider command only)
|
|
318
318
|
--batch-file FILE read IDs from file (one per line) for batch request (rider command only)
|
|
319
319
|
--premium use premium tier rate limits (higher request quotas)
|
|
320
|
+
--at DATETIME fetch historical ratings at a date/time in UTC (rider command only)
|
|
320
321
|
```
|
|
321
322
|
|
|
322
323
|
**Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
|
|
@@ -349,6 +350,28 @@ zrdata config
|
|
|
349
350
|
zrdata config # Will prompt for authorization header
|
|
350
351
|
```
|
|
351
352
|
|
|
353
|
+
### Historical Ratings
|
|
354
|
+
|
|
355
|
+
Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
|
|
356
|
+
date/time strings, interpreted as UTC:
|
|
357
|
+
|
|
358
|
+
```sh
|
|
359
|
+
# Fetch ratings as of a specific date
|
|
360
|
+
zrdata rider --at 2024-06-15 12345
|
|
361
|
+
|
|
362
|
+
# Fetch ratings at a specific date and time
|
|
363
|
+
zrdata rider --at "2024-06-15 14:30" 12345
|
|
364
|
+
|
|
365
|
+
# ISO 8601 with T separator
|
|
366
|
+
zrdata rider --at 2024-06-15T14:30:00 12345
|
|
367
|
+
|
|
368
|
+
# Combine with batch
|
|
369
|
+
zrdata rider --batch --at 2024-06-15 12345 67890 11111
|
|
370
|
+
|
|
371
|
+
# Preview what would be fetched
|
|
372
|
+
zrdata rider --noaction --at 2024-06-15 12345
|
|
373
|
+
```
|
|
374
|
+
|
|
352
375
|
### Advanced Options
|
|
353
376
|
|
|
354
377
|
```sh
|
|
@@ -735,10 +758,11 @@ async def main():
|
|
|
735
758
|
for zwift_id, rider in riders.items():
|
|
736
759
|
print(f"{rider.name}: {rider.current_rating}")
|
|
737
760
|
|
|
738
|
-
# Batch fetch with historical data
|
|
761
|
+
# Batch fetch with historical data (epoch is a Unix timestamp)
|
|
762
|
+
from shared.validation import parse_datetime_to_epoch
|
|
739
763
|
historical = await ZRRider.afetch_batch(
|
|
740
764
|
12345, 67890,
|
|
741
|
-
epoch=
|
|
765
|
+
epoch=parse_datetime_to_epoch('2024-01-01'),
|
|
742
766
|
zr=zr
|
|
743
767
|
)
|
|
744
768
|
|
|
@@ -269,7 +269,7 @@ including rider ratings, race results, and team rosters.
|
|
|
269
269
|
|
|
270
270
|
```sh
|
|
271
271
|
usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--v1fetch] [--noaction] [--sync]
|
|
272
|
-
[--batch] [--batch-file FILE] [--premium]
|
|
272
|
+
[--batch] [--batch-file FILE] [--premium] [--at DATETIME]
|
|
273
273
|
[{config,rider,result,team}] [id ...]
|
|
274
274
|
|
|
275
275
|
Module for fetching Zwiftracing data using the Zwiftracing API
|
|
@@ -291,6 +291,7 @@ options:
|
|
|
291
291
|
--batch use batch POST endpoint for multiple IDs (rider command only)
|
|
292
292
|
--batch-file FILE read IDs from file (one per line) for batch request (rider command only)
|
|
293
293
|
--premium use premium tier rate limits (higher request quotas)
|
|
294
|
+
--at DATETIME fetch historical ratings at a date/time in UTC (rider command only)
|
|
294
295
|
```
|
|
295
296
|
|
|
296
297
|
**Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
|
|
@@ -323,6 +324,28 @@ zrdata config
|
|
|
323
324
|
zrdata config # Will prompt for authorization header
|
|
324
325
|
```
|
|
325
326
|
|
|
327
|
+
### Historical Ratings
|
|
328
|
+
|
|
329
|
+
Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
|
|
330
|
+
date/time strings, interpreted as UTC:
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
# Fetch ratings as of a specific date
|
|
334
|
+
zrdata rider --at 2024-06-15 12345
|
|
335
|
+
|
|
336
|
+
# Fetch ratings at a specific date and time
|
|
337
|
+
zrdata rider --at "2024-06-15 14:30" 12345
|
|
338
|
+
|
|
339
|
+
# ISO 8601 with T separator
|
|
340
|
+
zrdata rider --at 2024-06-15T14:30:00 12345
|
|
341
|
+
|
|
342
|
+
# Combine with batch
|
|
343
|
+
zrdata rider --batch --at 2024-06-15 12345 67890 11111
|
|
344
|
+
|
|
345
|
+
# Preview what would be fetched
|
|
346
|
+
zrdata rider --noaction --at 2024-06-15 12345
|
|
347
|
+
```
|
|
348
|
+
|
|
326
349
|
### Advanced Options
|
|
327
350
|
|
|
328
351
|
```sh
|
|
@@ -709,10 +732,11 @@ async def main():
|
|
|
709
732
|
for zwift_id, rider in riders.items():
|
|
710
733
|
print(f"{rider.name}: {rider.current_rating}")
|
|
711
734
|
|
|
712
|
-
# Batch fetch with historical data
|
|
735
|
+
# Batch fetch with historical data (epoch is a Unix timestamp)
|
|
736
|
+
from shared.validation import parse_datetime_to_epoch
|
|
713
737
|
historical = await ZRRider.afetch_batch(
|
|
714
738
|
12345, 67890,
|
|
715
|
-
epoch=
|
|
739
|
+
epoch=parse_datetime_to_epoch('2024-01-01'),
|
|
716
740
|
zr=zr
|
|
717
741
|
)
|
|
718
742
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "zpdatafetch"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.2"
|
|
4
4
|
description = "A package for fetching data from Zwiftpower and Zwiftracing.app"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -19,7 +19,7 @@ dependencies = [
|
|
|
19
19
|
# Async support (anyio provides backend-agnostic async utilities)
|
|
20
20
|
"anyio>=4.4.0",
|
|
21
21
|
"beautifulsoup4>=4.12.3",
|
|
22
|
-
"cryptography>=
|
|
22
|
+
"cryptography>=46.0.5",
|
|
23
23
|
# Note: httpx 0.28.0 depends on httpcore 1.1.x which has a compatibility
|
|
24
24
|
# issue with Python 3.14's typing.Union. This causes test failures on 3.14.
|
|
25
25
|
# Workaround: test_shared_client_connection_pooling is marked as xfail on 3.14.
|
|
@@ -124,10 +124,6 @@ select = [
|
|
|
124
124
|
"UP", # pyupgrade
|
|
125
125
|
"ANN", # flake8-annotations (type hints)
|
|
126
126
|
]
|
|
127
|
-
ignore = [
|
|
128
|
-
"ANN101", # Missing type annotation for self
|
|
129
|
-
"ANN102", # Missing type annotation for cls
|
|
130
|
-
]
|
|
131
127
|
|
|
132
128
|
# Per-file ignores for justified Any usage
|
|
133
129
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -5,6 +5,7 @@ functionality for both zpdatafetch and zrdatafetch CLIs.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import sys
|
|
8
9
|
from argparse import ArgumentParser, Namespace
|
|
9
10
|
from collections.abc import Callable
|
|
10
11
|
from importlib.metadata import PackageNotFoundError, version
|
|
@@ -216,7 +217,7 @@ def validate_command_name(
|
|
|
216
217
|
True if valid, False if invalid.
|
|
217
218
|
"""
|
|
218
219
|
if cmd not in valid_commands:
|
|
219
|
-
print(f'Error: Unknown command "{cmd}"')
|
|
220
|
+
print(f'Error: Unknown command "{cmd}"', file=sys.stderr)
|
|
220
221
|
return False
|
|
221
222
|
return True
|
|
222
223
|
|
|
@@ -235,7 +236,7 @@ def validate_ids_provided(
|
|
|
235
236
|
True if IDs provided, False otherwise.
|
|
236
237
|
"""
|
|
237
238
|
if not ids:
|
|
238
|
-
print(f'Error: {cmd} command requires one or more IDs')
|
|
239
|
+
print(f'Error: {cmd} command requires one or more IDs', file=sys.stderr)
|
|
239
240
|
return False
|
|
240
241
|
return True
|
|
241
242
|
|
|
@@ -271,5 +272,5 @@ def read_ids_from_file(filepath: str) -> list[str] | None:
|
|
|
271
272
|
with open(filepath) as f:
|
|
272
273
|
return [line.strip() for line in f if line.strip()]
|
|
273
274
|
except OSError as e:
|
|
274
|
-
print(f'Error reading batch file: {e}')
|
|
275
|
+
print(f'Error reading batch file: {e}', file=sys.stderr)
|
|
275
276
|
return None
|
|
@@ -15,7 +15,9 @@ These ranges are based on Python's native integer limits and observed
|
|
|
15
15
|
API behavior.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
import calendar
|
|
18
19
|
import sys
|
|
20
|
+
from datetime import datetime, timezone
|
|
19
21
|
from typing import Literal
|
|
20
22
|
|
|
21
23
|
# ID Range Constants
|
|
@@ -178,6 +180,44 @@ def validate_epoch(value: int) -> int:
|
|
|
178
180
|
return value
|
|
179
181
|
|
|
180
182
|
|
|
183
|
+
def parse_datetime_to_epoch(value: str) -> int:
|
|
184
|
+
"""Parse a date/time string to a validated Unix epoch timestamp (UTC).
|
|
185
|
+
|
|
186
|
+
Accepts ISO 8601 formats:
|
|
187
|
+
- '2024-06-15' (date only, midnight UTC)
|
|
188
|
+
- '2024-06-15 14:30' (date and time, UTC)
|
|
189
|
+
- '2024-06-15T14:30:00' (ISO 8601, UTC)
|
|
190
|
+
- '2024-06-15T14:30:00Z' (ISO 8601 with Z suffix)
|
|
191
|
+
- '2024-06-15T14:30:00+00:00' (ISO 8601 with timezone)
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
value: Date/time string to parse
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Unix epoch timestamp (int, UTC)
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ValidationError: If the string cannot be parsed or the resulting
|
|
201
|
+
epoch is out of range
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
# Replace trailing 'Z' with '+00:00' for Python 3.10 compatibility
|
|
205
|
+
normalized = value.replace('Z', '+00:00') if value.endswith('Z') else value
|
|
206
|
+
dt = datetime.fromisoformat(normalized)
|
|
207
|
+
except ValueError as e:
|
|
208
|
+
raise ValidationError(
|
|
209
|
+
f"Invalid date/time '{value}': use ISO 8601 format "
|
|
210
|
+
f"(e.g. '2024-06-15' or '2024-06-15T14:30:00')",
|
|
211
|
+
) from e
|
|
212
|
+
|
|
213
|
+
# Assume UTC if no timezone specified
|
|
214
|
+
if dt.tzinfo is None:
|
|
215
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
216
|
+
|
|
217
|
+
epoch = calendar.timegm(dt.utctimetuple())
|
|
218
|
+
return validate_epoch(epoch)
|
|
219
|
+
|
|
220
|
+
|
|
181
221
|
def validate_batch_size(count: int, max_size: int = MAX_BATCH_SIZE) -> None:
|
|
182
222
|
"""Validate batch request size.
|
|
183
223
|
|
|
@@ -7,7 +7,8 @@ team rosters, and prime data.
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import sys
|
|
10
|
-
from
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from typing import Any, cast
|
|
11
12
|
|
|
12
13
|
from shared.cli import (
|
|
13
14
|
configure_logging_from_args,
|
|
@@ -267,7 +268,7 @@ Module for fetching zwiftpower data using the Zwifpower API
|
|
|
267
268
|
case 'team':
|
|
268
269
|
x = ZPTeamFetch()
|
|
269
270
|
case _:
|
|
270
|
-
print(f'Unknown command: {args.cmd}')
|
|
271
|
+
print(f'Unknown command: {args.cmd}', file=sys.stderr)
|
|
271
272
|
return 1
|
|
272
273
|
|
|
273
274
|
x.fetch(*args.id)
|
|
@@ -308,7 +309,7 @@ Module for fetching zwiftpower data using the Zwifpower API
|
|
|
308
309
|
# Check for excluded in each item if it's a collection
|
|
309
310
|
if hasattr(value, '__iter__') and not isinstance(value, str):
|
|
310
311
|
try:
|
|
311
|
-
for item in value:
|
|
312
|
+
for item in cast(Iterable[Any], value):
|
|
312
313
|
if hasattr(item, 'excluded'):
|
|
313
314
|
item_excluded = item.excluded()
|
|
314
315
|
if item_excluded:
|
|
@@ -330,7 +331,7 @@ Module for fetching zwiftpower data using the Zwifpower API
|
|
|
330
331
|
# Check for extras in each item if it's a collection
|
|
331
332
|
if hasattr(value, '__iter__') and not isinstance(value, str):
|
|
332
333
|
try:
|
|
333
|
-
for item in value:
|
|
334
|
+
for item in cast(Iterable[Any], value):
|
|
334
335
|
if hasattr(item, 'extras'):
|
|
335
336
|
item_extras = item.extras()
|
|
336
337
|
if item_extras:
|
|
@@ -381,7 +382,7 @@ Module for fetching zwiftpower data using the Zwifpower API
|
|
|
381
382
|
):
|
|
382
383
|
print() # Newline after key for collections
|
|
383
384
|
try:
|
|
384
|
-
for item in value:
|
|
385
|
+
for item in cast(Iterable[Any], value):
|
|
385
386
|
print_collection(item, indent=1)
|
|
386
387
|
except (TypeError, AttributeError):
|
|
387
388
|
pass
|
|
@@ -20,7 +20,7 @@ else:
|
|
|
20
20
|
)
|
|
21
21
|
except ImportError:
|
|
22
22
|
# Fallback - catch the anyio exception type
|
|
23
|
-
BaseExceptionGroup = Exception
|
|
23
|
+
BaseExceptionGroup: type[BaseException] = Exception
|
|
24
24
|
|
|
25
25
|
from shared.json_helpers import parse_json_safe
|
|
26
26
|
from shared.validation import ValidationError, validate_id_list
|
|
@@ -136,11 +136,10 @@ class ZPPrimesFetch(ZP_obj):
|
|
|
136
136
|
await async_zp.init_client()
|
|
137
137
|
|
|
138
138
|
# Copy authentication state from sync to async client
|
|
139
|
-
if self._zp_sync._client and async_zp._client:
|
|
140
|
-
# Share the cookies - this preserves the login session
|
|
141
|
-
assert async_zp._client is not None
|
|
142
139
|
assert self._zp_sync._client is not None
|
|
143
|
-
async_zp._client
|
|
140
|
+
assert async_zp._client is not None
|
|
141
|
+
# Share the cookies - this preserves the login session
|
|
142
|
+
async_zp._client.cookies = self._zp_sync._client.cookies
|
|
144
143
|
logger.debug('Copied authentication cookies from sync to async session')
|
|
145
144
|
|
|
146
145
|
# Don't store this - create fresh each time to avoid lifecycle issues
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from collections.abc import Iterator, Sequence
|
|
5
5
|
from dataclasses import asdict, dataclass, field
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, overload
|
|
7
7
|
|
|
8
8
|
from zpdatafetch.zp_utils import (
|
|
9
9
|
convert_gender,
|
|
@@ -511,14 +511,19 @@ class ZPRaceResult(Sequence):
|
|
|
511
511
|
"""
|
|
512
512
|
return len(self._riders)
|
|
513
513
|
|
|
514
|
-
|
|
515
|
-
|
|
514
|
+
@overload
|
|
515
|
+
def __getitem__(self, index: int) -> ZPRiderFinish: ...
|
|
516
|
+
@overload
|
|
517
|
+
def __getitem__(self, index: slice) -> Sequence[ZPRiderFinish]: ...
|
|
518
|
+
|
|
519
|
+
def __getitem__(self, index: int | slice) -> ZPRiderFinish | Sequence[ZPRiderFinish]:
|
|
520
|
+
"""Access rider finish by index or slice.
|
|
516
521
|
|
|
517
522
|
Args:
|
|
518
|
-
index: Integer index
|
|
523
|
+
index: Integer index or slice
|
|
519
524
|
|
|
520
525
|
Returns:
|
|
521
|
-
Single rider
|
|
526
|
+
Single rider or sequence of riders
|
|
522
527
|
|
|
523
528
|
Raises:
|
|
524
529
|
IndexError: If index out of range
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zpdatafetch
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: A package for fetching data from Zwiftpower and Zwiftracing.app
|
|
5
5
|
Author-email: Doug Morris <doug@mhost.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: anyio>=4.4.0
|
|
15
15
|
Requires-Dist: beautifulsoup4>=4.12.3
|
|
16
|
-
Requires-Dist: cryptography>=
|
|
16
|
+
Requires-Dist: cryptography>=46.0.5
|
|
17
17
|
Requires-Dist: httpx>=0.28.0
|
|
18
18
|
Requires-Dist: keyring>=25.2.0
|
|
19
19
|
Requires-Dist: keyrings-alt
|
|
@@ -295,7 +295,7 @@ including rider ratings, race results, and team rosters.
|
|
|
295
295
|
|
|
296
296
|
```sh
|
|
297
297
|
usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--v1fetch] [--noaction] [--sync]
|
|
298
|
-
[--batch] [--batch-file FILE] [--premium]
|
|
298
|
+
[--batch] [--batch-file FILE] [--premium] [--at DATETIME]
|
|
299
299
|
[{config,rider,result,team}] [id ...]
|
|
300
300
|
|
|
301
301
|
Module for fetching Zwiftracing data using the Zwiftracing API
|
|
@@ -317,6 +317,7 @@ options:
|
|
|
317
317
|
--batch use batch POST endpoint for multiple IDs (rider command only)
|
|
318
318
|
--batch-file FILE read IDs from file (one per line) for batch request (rider command only)
|
|
319
319
|
--premium use premium tier rate limits (higher request quotas)
|
|
320
|
+
--at DATETIME fetch historical ratings at a date/time in UTC (rider command only)
|
|
320
321
|
```
|
|
321
322
|
|
|
322
323
|
**Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
|
|
@@ -349,6 +350,28 @@ zrdata config
|
|
|
349
350
|
zrdata config # Will prompt for authorization header
|
|
350
351
|
```
|
|
351
352
|
|
|
353
|
+
### Historical Ratings
|
|
354
|
+
|
|
355
|
+
Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
|
|
356
|
+
date/time strings, interpreted as UTC:
|
|
357
|
+
|
|
358
|
+
```sh
|
|
359
|
+
# Fetch ratings as of a specific date
|
|
360
|
+
zrdata rider --at 2024-06-15 12345
|
|
361
|
+
|
|
362
|
+
# Fetch ratings at a specific date and time
|
|
363
|
+
zrdata rider --at "2024-06-15 14:30" 12345
|
|
364
|
+
|
|
365
|
+
# ISO 8601 with T separator
|
|
366
|
+
zrdata rider --at 2024-06-15T14:30:00 12345
|
|
367
|
+
|
|
368
|
+
# Combine with batch
|
|
369
|
+
zrdata rider --batch --at 2024-06-15 12345 67890 11111
|
|
370
|
+
|
|
371
|
+
# Preview what would be fetched
|
|
372
|
+
zrdata rider --noaction --at 2024-06-15 12345
|
|
373
|
+
```
|
|
374
|
+
|
|
352
375
|
### Advanced Options
|
|
353
376
|
|
|
354
377
|
```sh
|
|
@@ -735,10 +758,11 @@ async def main():
|
|
|
735
758
|
for zwift_id, rider in riders.items():
|
|
736
759
|
print(f"{rider.name}: {rider.current_rating}")
|
|
737
760
|
|
|
738
|
-
# Batch fetch with historical data
|
|
761
|
+
# Batch fetch with historical data (epoch is a Unix timestamp)
|
|
762
|
+
from shared.validation import parse_datetime_to_epoch
|
|
739
763
|
historical = await ZRRider.afetch_batch(
|
|
740
764
|
12345, 67890,
|
|
741
|
-
epoch=
|
|
765
|
+
epoch=parse_datetime_to_epoch('2024-01-01'),
|
|
742
766
|
zr=zr
|
|
743
767
|
)
|
|
744
768
|
|
|
@@ -23,6 +23,7 @@ from shared.cli import (
|
|
|
23
23
|
validate_command_provided,
|
|
24
24
|
validate_ids_provided,
|
|
25
25
|
)
|
|
26
|
+
from shared.validation import ValidationError, parse_datetime_to_epoch
|
|
26
27
|
from zrdatafetch import (
|
|
27
28
|
Config,
|
|
28
29
|
ZRResultFetch,
|
|
@@ -74,6 +75,15 @@ Module for fetching Zwiftracing data using the Zwiftracing API
|
|
|
74
75
|
action='store_true',
|
|
75
76
|
help='use premium tier rate limits (higher request quotas)',
|
|
76
77
|
)
|
|
78
|
+
p.add_argument(
|
|
79
|
+
'--at',
|
|
80
|
+
type=str,
|
|
81
|
+
metavar='DATETIME',
|
|
82
|
+
help=(
|
|
83
|
+
'fetch historical ratings at a given date/time (UTC), '
|
|
84
|
+
"e.g. '2024-06-15' or '2024-06-15T14:30:00' (rider command only)"
|
|
85
|
+
),
|
|
86
|
+
)
|
|
77
87
|
|
|
78
88
|
# Use parse_intermixed_args to handle flags after positional arguments
|
|
79
89
|
# This allows: zrdata rider --noaction 12345 67890
|
|
@@ -117,11 +127,25 @@ Module for fetching Zwiftracing data using the Zwiftracing API
|
|
|
117
127
|
if not validate_ids_provided(args.id, 'rider'):
|
|
118
128
|
return 1
|
|
119
129
|
|
|
130
|
+
# Parse --at to epoch
|
|
131
|
+
epoch: int | None = None
|
|
132
|
+
if args.at:
|
|
133
|
+
try:
|
|
134
|
+
epoch = parse_datetime_to_epoch(args.at)
|
|
135
|
+
except ValidationError as e:
|
|
136
|
+
print(f'Error: {e}', file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
|
|
120
139
|
if args.noaction:
|
|
121
140
|
if args.batch or args.batch_file:
|
|
122
|
-
|
|
141
|
+
msg = f'Would fetch {len(args.id)} riders using batch POST'
|
|
123
142
|
else:
|
|
124
|
-
|
|
143
|
+
msg = f"Would fetch rider data for: {', '.join(args.id)}"
|
|
144
|
+
if args.raw:
|
|
145
|
+
msg += ' (raw output format)'
|
|
146
|
+
if epoch is not None:
|
|
147
|
+
msg += f' at {args.at} (epoch {epoch})'
|
|
148
|
+
print(msg)
|
|
125
149
|
return None
|
|
126
150
|
|
|
127
151
|
try:
|
|
@@ -130,20 +154,20 @@ Module for fetching Zwiftracing data using the Zwiftracing API
|
|
|
130
154
|
|
|
131
155
|
# Handle batch request
|
|
132
156
|
if args.batch or args.batch_file:
|
|
133
|
-
riders = ZRRiderFetch.fetch_batch(*rider_ids)
|
|
157
|
+
riders = ZRRiderFetch.fetch_batch(*rider_ids, epoch=epoch)
|
|
134
158
|
fetcher = None # Batch doesn't use fetcher instance
|
|
135
159
|
else:
|
|
136
160
|
fetcher = ZRRiderFetch()
|
|
137
|
-
riders = fetcher.fetch(*rider_ids)
|
|
161
|
+
riders = fetcher.fetch(*rider_ids, epoch=epoch)
|
|
138
162
|
|
|
139
163
|
# Output results
|
|
140
164
|
_output_results(args, riders, fetcher)
|
|
141
165
|
|
|
142
166
|
except ValueError as e:
|
|
143
|
-
print(f'Error: Invalid Zwift ID: {e}')
|
|
167
|
+
print(f'Error: Invalid Zwift ID: {e}', file=sys.stderr)
|
|
144
168
|
return 1
|
|
145
169
|
except Exception as e:
|
|
146
|
-
print(f'Error fetching rider: {e}')
|
|
170
|
+
print(f'Error fetching rider: {e}', file=sys.stderr)
|
|
147
171
|
return 1
|
|
148
172
|
|
|
149
173
|
case 'result':
|
|
@@ -164,10 +188,10 @@ Module for fetching Zwiftracing data using the Zwiftracing API
|
|
|
164
188
|
_output_results(args, results, fetcher)
|
|
165
189
|
|
|
166
190
|
except ValueError as e:
|
|
167
|
-
print(f'Error: Invalid race ID: {e}')
|
|
191
|
+
print(f'Error: Invalid race ID: {e}', file=sys.stderr)
|
|
168
192
|
return 1
|
|
169
193
|
except Exception as e:
|
|
170
|
-
print(f'Error fetching result: {e}')
|
|
194
|
+
print(f'Error fetching result: {e}', file=sys.stderr)
|
|
171
195
|
return 1
|
|
172
196
|
|
|
173
197
|
case 'team':
|
|
@@ -188,10 +212,10 @@ Module for fetching Zwiftracing data using the Zwiftracing API
|
|
|
188
212
|
_output_results(args, teams, fetcher)
|
|
189
213
|
|
|
190
214
|
except ValueError as e:
|
|
191
|
-
print(f'Error: Invalid team ID: {e}')
|
|
215
|
+
print(f'Error: Invalid team ID: {e}', file=sys.stderr)
|
|
192
216
|
return 1
|
|
193
217
|
except Exception as e:
|
|
194
|
-
print(f'Error fetching team: {e}')
|
|
218
|
+
print(f'Error fetching team: {e}', file=sys.stderr)
|
|
195
219
|
return 1
|
|
196
220
|
|
|
197
221
|
case _:
|
|
@@ -6,7 +6,7 @@ any fetch logic. Fetching is handled by ZRResultFetch.
|
|
|
6
6
|
|
|
7
7
|
from collections.abc import Iterator, Sequence
|
|
8
8
|
from dataclasses import asdict, dataclass, field
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, overload
|
|
10
10
|
|
|
11
11
|
from zrdatafetch.logging_config import get_logger
|
|
12
12
|
from zrdatafetch.zr_utils import safe_float, safe_int, safe_str
|
|
@@ -236,14 +236,21 @@ class ZRRaceResult(Sequence):
|
|
|
236
236
|
"""
|
|
237
237
|
return len(self._results)
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
@overload
|
|
240
|
+
def __getitem__(self, index: int) -> ZRRiderResult: ...
|
|
241
|
+
@overload
|
|
242
|
+
def __getitem__(self, index: slice) -> Sequence[ZRRiderResult]: ...
|
|
243
|
+
|
|
244
|
+
def __getitem__(
|
|
245
|
+
self, index: int | slice,
|
|
246
|
+
) -> ZRRiderResult | Sequence[ZRRiderResult]:
|
|
247
|
+
"""Access rider result by index or slice.
|
|
241
248
|
|
|
242
249
|
Args:
|
|
243
|
-
index: Integer index
|
|
250
|
+
index: Integer index or slice
|
|
244
251
|
|
|
245
252
|
Returns:
|
|
246
|
-
ZRRiderResult object
|
|
253
|
+
ZRRiderResult object or sequence of results
|
|
247
254
|
|
|
248
255
|
Raises:
|
|
249
256
|
IndexError: If index out of range
|
|
@@ -6,7 +6,7 @@ any fetch logic. Fetching is handled by ZRTeamFetch.
|
|
|
6
6
|
|
|
7
7
|
from collections.abc import Iterator, Sequence
|
|
8
8
|
from dataclasses import asdict, dataclass, field
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, overload
|
|
10
10
|
|
|
11
11
|
from zrdatafetch.logging_config import get_logger
|
|
12
12
|
from zrdatafetch.zr_utils import (
|
|
@@ -358,14 +358,21 @@ class ZRTeamRoster(Sequence):
|
|
|
358
358
|
"""
|
|
359
359
|
return len(self._members)
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
@overload
|
|
362
|
+
def __getitem__(self, index: int) -> ZRTeamMember: ...
|
|
363
|
+
@overload
|
|
364
|
+
def __getitem__(self, index: slice) -> Sequence[ZRTeamMember]: ...
|
|
365
|
+
|
|
366
|
+
def __getitem__(
|
|
367
|
+
self, index: int | slice,
|
|
368
|
+
) -> ZRTeamMember | Sequence[ZRTeamMember]:
|
|
369
|
+
"""Access team member by index or slice.
|
|
363
370
|
|
|
364
371
|
Args:
|
|
365
|
-
index: Integer index
|
|
372
|
+
index: Integer index or slice
|
|
366
373
|
|
|
367
374
|
Returns:
|
|
368
|
-
ZRTeamMember object
|
|
375
|
+
ZRTeamMember object or sequence of members
|
|
369
376
|
|
|
370
377
|
Raises:
|
|
371
378
|
IndexError: If index out of range
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|