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.
Files changed (69) hide show
  1. {zpdatafetch-2.1.0/src/zpdatafetch.egg-info → zpdatafetch-2.1.2}/PKG-INFO +29 -5
  2. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/README.md +27 -3
  3. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/pyproject.toml +2 -6
  4. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/cli.py +4 -3
  5. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/validation.py +40 -0
  6. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/cli.py +6 -5
  7. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zp.py +0 -1
  8. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpcyclistfetch.py +1 -1
  9. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpprimesfetch.py +3 -4
  10. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpraceresult.py +10 -5
  11. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2/src/zpdatafetch.egg-info}/PKG-INFO +29 -5
  12. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/requires.txt +1 -1
  13. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/cli.py +34 -10
  14. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrraceresult.py +12 -5
  15. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrteamroster.py +12 -5
  16. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/LICENSE +0 -0
  17. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/setup.cfg +0 -0
  18. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/__init__.py +0 -0
  19. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/config.py +0 -0
  20. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/error_helpers.py +0 -0
  21. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/exceptions.py +0 -0
  22. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/http_client.py +0 -0
  23. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/json_helpers.py +0 -0
  24. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/shared/logging.py +0 -0
  25. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/__init__.py +0 -0
  26. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/activity.py +0 -0
  27. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/auth.py +0 -0
  28. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/cli.py +0 -0
  29. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/config.py +0 -0
  30. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/followers.py +0 -0
  31. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/logging_config.py +0 -0
  32. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/profile.py +0 -0
  33. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/rideons.py +0 -0
  34. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/ridersinworld.py +0 -0
  35. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zdatafetch/worlds.py +0 -0
  36. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/__init__.py +0 -0
  37. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/async_zp.py +0 -0
  38. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/config.py +0 -0
  39. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/logging_config.py +0 -0
  40. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zp_obj.py +0 -0
  41. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zp_utils.py +0 -0
  42. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpcyclist.py +0 -0
  43. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpleague.py +0 -0
  44. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpleaguefetch.py +0 -0
  45. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpprime.py +0 -0
  46. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracefinish.py +0 -0
  47. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracelog.py +0 -0
  48. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracesignup.py +0 -0
  49. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpracesprint.py +0 -0
  50. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpresultfetch.py +0 -0
  51. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpsignupfetch.py +0 -0
  52. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpsprintsfetch.py +0 -0
  53. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpteam.py +0 -0
  54. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch/zpteamfetch.py +0 -0
  55. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/SOURCES.txt +0 -0
  56. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/dependency_links.txt +0 -0
  57. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/entry_points.txt +0 -0
  58. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zpdatafetch.egg-info/top_level.txt +0 -0
  59. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/__init__.py +0 -0
  60. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/async_zr.py +0 -0
  61. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/config.py +0 -0
  62. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/logging_config.py +0 -0
  63. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/rate_limiter.py +0 -0
  64. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zr.py +0 -0
  65. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zr_utils.py +0 -0
  66. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrresultfetch.py +0 -0
  67. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrrider.py +0 -0
  68. {zpdatafetch-2.1.0 → zpdatafetch-2.1.2}/src/zrdatafetch/zrriderfetch.py +0 -0
  69. {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.0
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>=44.0.0
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=1704067200, # Unix timestamp
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=1704067200, # Unix timestamp
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.0"
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>=44.0.0",
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 typing import Any
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: # type: ignore[iteration-not-supported]
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: # type: ignore[iteration-not-supported]
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: # type: ignore[iteration-not-supported]
385
+ for item in cast(Iterable[Any], value):
385
386
  print_collection(item, indent=1)
386
387
  except (TypeError, AttributeError):
387
388
  pass
@@ -489,7 +489,6 @@ def main() -> None:
489
489
  Core module for accessing Zwiftpower API endpoints
490
490
  """
491
491
  zp = ZP()
492
- zp.verbose = True # type: ignore[attr-defined]
493
492
  zp.login()
494
493
  if zp.login_response:
495
494
  print(zp.login_response.status_code)
@@ -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.cookies = self._zp_sync._client.cookies # type: ignore[assignment]
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
- def __getitem__(self, index: int) -> ZPRiderFinish: # type: ignore[override]
515
- """Access rider finish by index.
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.0
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>=44.0.0
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=1704067200, # Unix timestamp
765
+ epoch=parse_datetime_to_epoch('2024-01-01'),
742
766
  zr=zr
743
767
  )
744
768
 
@@ -1,6 +1,6 @@
1
1
  anyio>=4.4.0
2
2
  beautifulsoup4>=4.12.3
3
- cryptography>=44.0.0
3
+ cryptography>=46.0.5
4
4
  httpx>=0.28.0
5
5
  keyring>=25.2.0
6
6
  keyrings-alt
@@ -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
- print(f'Would fetch {len(args.id)} riders using batch POST')
141
+ msg = f'Would fetch {len(args.id)} riders using batch POST'
123
142
  else:
124
- format_noaction_output('rider', args.id, args.raw)
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
- def __getitem__(self, index: int) -> ZRRiderResult: # type: ignore[override]
240
- """Access rider result by index.
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
- def __getitem__(self, index: int) -> ZRTeamMember: # type: ignore[override]
362
- """Access team member by index.
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