zpdatafetch 2.3.2__tar.gz → 2.4.1__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 (87) hide show
  1. {zpdatafetch-2.3.2/src/zpdatafetch.egg-info → zpdatafetch-2.4.1}/PKG-INFO +104 -22
  2. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/README.md +103 -21
  3. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/pyproject.toml +1 -1
  4. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/followers.py +119 -51
  5. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/rideons.py +56 -6
  6. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/cli.py +80 -1
  7. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpcyclistfetch.py +1 -1
  8. zpdatafetch-2.4.1/src/zpdatafetch/zpleague.py +1153 -0
  9. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpleaguefetch.py +248 -47
  10. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1/src/zpdatafetch.egg-info}/PKG-INFO +104 -22
  11. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/SOURCES.txt +2 -0
  12. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/__init__.py +6 -0
  13. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/cli.py +48 -2
  14. zpdatafetch-2.4.1/src/zrdatafetch/zrcategories.py +257 -0
  15. zpdatafetch-2.4.1/src/zrdatafetch/zrcategoriesfetch.py +271 -0
  16. zpdatafetch-2.3.2/src/zpdatafetch/zpleague.py +0 -496
  17. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/LICENSE +0 -0
  18. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/setup.cfg +0 -0
  19. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/__init__.py +0 -0
  20. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/cli.py +0 -0
  21. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/config.py +0 -0
  22. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/error_helpers.py +0 -0
  23. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/exceptions.py +0 -0
  24. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/http_client.py +0 -0
  25. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/json_helpers.py +0 -0
  26. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/logging.py +0 -0
  27. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/shared/validation.py +0 -0
  28. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/__init__.py +0 -0
  29. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/activity.py +0 -0
  30. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/auth.py +0 -0
  31. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/cli.py +0 -0
  32. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/config.py +0 -0
  33. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/logging_config.py +0 -0
  34. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/profile.py +0 -0
  35. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/ridersinworld.py +0 -0
  36. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zdatafetch/worlds.py +0 -0
  37. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/__init__.py +0 -0
  38. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/async_zp.py +0 -0
  39. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/config.py +0 -0
  40. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/logging_config.py +0 -0
  41. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zp.py +0 -0
  42. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zp_obj.py +0 -0
  43. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zp_utils.py +0 -0
  44. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpcyclist.py +0 -0
  45. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpprime.py +0 -0
  46. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpprimesfetch.py +0 -0
  47. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracefinish.py +0 -0
  48. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracelog.py +0 -0
  49. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpraceresult.py +0 -0
  50. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracesignup.py +0 -0
  51. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracesprint.py +0 -0
  52. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpresultfetch.py +0 -0
  53. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpsignupfetch.py +0 -0
  54. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpsprintsfetch.py +0 -0
  55. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpteam.py +0 -0
  56. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch/zpteamfetch.py +0 -0
  57. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/dependency_links.txt +0 -0
  58. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/entry_points.txt +0 -0
  59. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/requires.txt +0 -0
  60. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/top_level.txt +0 -0
  61. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/async_zr.py +0 -0
  62. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/config.py +0 -0
  63. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/logging_config.py +0 -0
  64. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/rate_limiter.py +0 -0
  65. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zr.py +0 -0
  66. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zr_utils.py +0 -0
  67. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zrraceresult.py +0 -0
  68. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zrresultfetch.py +0 -0
  69. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zrrider.py +0 -0
  70. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zrriderfetch.py +0 -0
  71. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zrteamfetch.py +0 -0
  72. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zrdatafetch/zrteamroster.py +0 -0
  73. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/__init__.py +0 -0
  74. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/async_zs.py +0 -0
  75. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/cli.py +0 -0
  76. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/logging_config.py +0 -0
  77. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/__init__.py +0 -0
  78. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/component.py +0 -0
  79. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/incident.py +0 -0
  80. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/maintenance.py +0 -0
  81. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/page.py +0 -0
  82. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/status.py +0 -0
  83. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/models/summary.py +0 -0
  84. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/zs.py +0 -0
  85. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/zsincidentfetch.py +0 -0
  86. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/zsmaintenancefetch.py +0 -0
  87. {zpdatafetch-2.3.2 → zpdatafetch-2.4.1}/src/zsdatafetch/zssummaryfetch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zpdatafetch
3
- Version: 2.3.2
3
+ Version: 2.4.1
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
@@ -58,7 +58,7 @@ This package provides four command-line tools:
58
58
  | Tool | API | Purpose | Data Types |
59
59
  | ------------ | ------------ | ------------------------------------------ | --------------------------------------------------------- |
60
60
  | **`zpdata`** | ZwiftPower | Race rankings, signups, results | Cyclist, Primes, Results, Signups, Sprints, Teams, League |
61
- | **`zrdata`** | Zwiftracing | Rider ratings, race results, rosters | Rider Ratings, Race Results, Team Rosters |
61
+ | **`zrdata`** | Zwiftracing | Rider ratings, race results, rosters | Rider Ratings, Race Results, Team Rosters, Category Ranges |
62
62
  | **`zdata`** | Zwift | Profiles, followers, activities, worlds | Profile, Followers, RideOns, Activity, Worlds, Riders |
63
63
  | **`zsdata`** | Zwift Status | Service status, incidents, maintenance | Summary, Components, Incidents, Maintenance |
64
64
 
@@ -77,7 +77,7 @@ required).
77
77
  - **Primes** - Prime results for Fastest Through Segment (FTS) and First Across the Line (FAL)
78
78
  - **Sprints** - Sprint data including sprint details and positions
79
79
  - **Team data** - Team rosters and member information
80
- - **League data** - League standings and Zwift Racing Score (ZRS) information
80
+ - **League data** - League standings, events, team standings, and league metadata (name, categories, contact)
81
81
 
82
82
  ### For zrdata (Zwiftracing)
83
83
 
@@ -312,7 +312,25 @@ available classes are as follows:
312
312
  - Signup: fetch signups for a particular event by event id
313
313
  - Sprints: fetch sprints from one or more races using event id
314
314
  - Team: fetch team data by team id
315
- - League: fetch league standings by league id
315
+ - League: fetch league standings, events, team standings, team-event results and metadata by league id
316
+
317
+ **League data example:**
318
+
319
+ ```python
320
+ from zpdatafetch import ZPLeagueFetch
321
+
322
+ lf = ZPLeagueFetch()
323
+ league = lf.fetch(3379)[3379]
324
+
325
+ league.info() # ZPLeagueInfo: name, contact, categories, ...
326
+ league.events() # list[ZPLeagueEvent]: event_id, title, start_datetime
327
+ league.team_standings() # list[ZPLeagueTeamStanding]
328
+ league.team_event_results() # list[ZPLeagueTeamEventResult]
329
+ print(league.json()) # all collections as JSON (keys omitted when empty)
330
+ ```
331
+
332
+ Each source is fetched independently: a league without a standings file (e.g.
333
+ 3388) still returns its events and metadata, and vice versa.
316
334
 
317
335
  ## Zwiftracing Data (zrdata)
318
336
 
@@ -322,30 +340,39 @@ including rider ratings, race results, and team rosters.
322
340
  ### Command-line usage
323
341
 
324
342
  ```sh
325
- usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--v1fetch] [--noaction] [--sync]
343
+ usage: zrdata [-h] [--version] [-v] [-vv] [--log-file PATH] [-r] [--json]
344
+ [--noaction] [--sync] [--extras] [--excluded] [--v1fetch]
326
345
  [--batch] [--batch-file FILE] [--premium] [--at DATETIME]
327
- [{config,rider,result,team}] [id ...]
346
+ [CMD] [id ...]
328
347
 
329
348
  Module for fetching Zwiftracing data using the Zwiftracing API
330
349
 
331
350
  positional arguments:
332
- {config,rider,result,team}
333
- which command to run
334
- id the id to search for
351
+ CMD command to execute: {config,rider,result,team,categories}
352
+ id ID(s) for the command
335
353
 
336
354
  options:
337
- -h, --help show this help message and exit
338
- -v, --verbose enable INFO level logging to console
339
- -vv, --debug enable DEBUG level logging to console
340
- --log-file PATH path to log file (enables file logging)
341
- -r, --raw print the raw response text from the server
342
- --v1fetch output fetched data in v1.8 format (backward compatibility)
343
- --noaction report what would be done without actually fetching data
344
- --sync use synchronous (non-parallel) requests for debugging
345
- --batch use batch POST endpoint for multiple IDs (rider command only)
346
- --batch-file FILE read IDs from file (one per line) for batch request (rider command only)
347
- --premium use premium tier rate limits (higher request quotas)
348
- --at DATETIME fetch historical ratings at a date/time in UTC (rider command only)
355
+ -h, --help show this help message and exit
356
+ --version show program's version number and exit
357
+ -v, --verbose enable verbose output (INFO level logging)
358
+ -vv, --debug enable debug output (DEBUG level logging)
359
+ --log-file PATH write logging output to file
360
+ -r, --raw print raw result data as received from the server
361
+ --json output fetched data as JSON (default: object repr)
362
+ --noaction show what would be done without actually fetching data
363
+ --sync use synchronous (non-parallel) requests
364
+ --extras report recently added fields not handled natively
365
+ --excluded report recognized fields not yet explicitly handled
366
+ --v1fetch output fetched data in v1.8 format (for backward
367
+ compatibility)
368
+ --batch use batch POST endpoint for multiple IDs (rider command
369
+ only)
370
+ --batch-file FILE read IDs from file (one per line) for batch request
371
+ (rider command only)
372
+ --premium use premium tier rate limits (higher request quotas)
373
+ --at DATETIME fetch historical ratings at a given date/time (UTC), e.g.
374
+ '2024-06-15' or '2024-06-15T14:30:00' (rider command
375
+ only)
349
376
  ```
350
377
 
351
378
  **Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
@@ -371,6 +398,9 @@ zrdata result 3590800
371
398
  # Fetch team roster
372
399
  zrdata team 456
373
400
 
401
+ # Fetch vELO2 category ranges
402
+ zrdata categories
403
+
374
404
  # View current configuration
375
405
  zrdata config
376
406
 
@@ -378,6 +408,50 @@ zrdata config
378
408
  zrdata config # Will prompt for authorization header
379
409
  ```
380
410
 
411
+ ### Category Ranges (vELO2)
412
+
413
+ Fetch the vELO2 category ranges used to bucket riders by rating:
414
+
415
+ ```sh
416
+ zrdata categories
417
+ # Output: the full repr — every entry with number, name, and range:
418
+ # ZRCategories(scale='1-1000', categories=[ZRvELOCategory(
419
+ # number=1, name='Diamond', min=920, max=None), ZRvELOCategory(
420
+ # number=2, name='Ruby', min=840, max=919), ...])
421
+ ```
422
+
423
+ Use `--json` for the full mapping:
424
+
425
+ ```sh
426
+ zrdata categories --json
427
+ # Output:
428
+ # {
429
+ # "scale": "1-1000",
430
+ # "categories": [
431
+ # {"number": 1, "name": "Diamond", "min": 920, "max": null},
432
+ # {"number": 2, "name": "Ruby", "min": 840, "max": 919},
433
+ # ...
434
+ # {"number": 10, "name": "Copper", "min": 0, "max": 359}
435
+ # ]
436
+ # }
437
+ ```
438
+
439
+ `--raw` prints the unmodified API response; `--noaction` previews the fetch.
440
+
441
+ ```python
442
+ # Library usage
443
+ from zrdatafetch import ZRCategoriesFetch
444
+
445
+ categories = ZRCategoriesFetch().fetch()
446
+ for entry in categories: # iterate in API order (0 = Diamond)
447
+ print(f"{entry.number}: {entry.name} {entry.min}-{entry.max}")
448
+
449
+ silver = categories['Silver'] # lookup by exact name
450
+ diamond = categories[0] # positional access
451
+ 'Silver' in categories # membership -> True
452
+ len(categories) # 10
453
+ ```
454
+
381
455
  ### Historical Ratings
382
456
 
383
457
  Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
@@ -1237,7 +1311,7 @@ profile = ZwiftProfile()
1237
1311
  profile.fetch(550564)
1238
1312
  print(profile.json())
1239
1313
 
1240
- # Fetch followers
1314
+ # Fetch followers (all pages; the API paginates at 200 entries per page)
1241
1315
  followers = ZwiftFollowers()
1242
1316
  followers.fetch(550564)
1243
1317
  print(f"Followers: {followers.follower_count()}")
@@ -1251,7 +1325,15 @@ worlds = ZwiftWorlds()
1251
1325
  worlds.fetch()
1252
1326
 
1253
1327
  # Give a RideOn
1328
+ # rider_id = activity owner; your own id is resolved automatically
1329
+ # via GET /api/profiles/me and sent as {"profileId": <your id>}
1254
1330
  ZwiftRideOns.give_rideon(550564, 12345678)
1331
+
1332
+ # Check who gave RideOns on an activity
1333
+ rideons = ZwiftRideOns()
1334
+ rideons.fetch(550564, 12345678)
1335
+ print(rideons.rideon_ids()) # rider IDs of RideOn givers
1336
+ print(rideons.has_rideon_from(766087)) # did this rider give a RideOn?
1255
1337
  ```
1256
1338
 
1257
1339
  ## Zwift Status Data (zsdata)
@@ -29,7 +29,7 @@ This package provides four command-line tools:
29
29
  | Tool | API | Purpose | Data Types |
30
30
  | ------------ | ------------ | ------------------------------------------ | --------------------------------------------------------- |
31
31
  | **`zpdata`** | ZwiftPower | Race rankings, signups, results | Cyclist, Primes, Results, Signups, Sprints, Teams, League |
32
- | **`zrdata`** | Zwiftracing | Rider ratings, race results, rosters | Rider Ratings, Race Results, Team Rosters |
32
+ | **`zrdata`** | Zwiftracing | Rider ratings, race results, rosters | Rider Ratings, Race Results, Team Rosters, Category Ranges |
33
33
  | **`zdata`** | Zwift | Profiles, followers, activities, worlds | Profile, Followers, RideOns, Activity, Worlds, Riders |
34
34
  | **`zsdata`** | Zwift Status | Service status, incidents, maintenance | Summary, Components, Incidents, Maintenance |
35
35
 
@@ -48,7 +48,7 @@ required).
48
48
  - **Primes** - Prime results for Fastest Through Segment (FTS) and First Across the Line (FAL)
49
49
  - **Sprints** - Sprint data including sprint details and positions
50
50
  - **Team data** - Team rosters and member information
51
- - **League data** - League standings and Zwift Racing Score (ZRS) information
51
+ - **League data** - League standings, events, team standings, and league metadata (name, categories, contact)
52
52
 
53
53
  ### For zrdata (Zwiftracing)
54
54
 
@@ -283,7 +283,25 @@ available classes are as follows:
283
283
  - Signup: fetch signups for a particular event by event id
284
284
  - Sprints: fetch sprints from one or more races using event id
285
285
  - Team: fetch team data by team id
286
- - League: fetch league standings by league id
286
+ - League: fetch league standings, events, team standings, team-event results and metadata by league id
287
+
288
+ **League data example:**
289
+
290
+ ```python
291
+ from zpdatafetch import ZPLeagueFetch
292
+
293
+ lf = ZPLeagueFetch()
294
+ league = lf.fetch(3379)[3379]
295
+
296
+ league.info() # ZPLeagueInfo: name, contact, categories, ...
297
+ league.events() # list[ZPLeagueEvent]: event_id, title, start_datetime
298
+ league.team_standings() # list[ZPLeagueTeamStanding]
299
+ league.team_event_results() # list[ZPLeagueTeamEventResult]
300
+ print(league.json()) # all collections as JSON (keys omitted when empty)
301
+ ```
302
+
303
+ Each source is fetched independently: a league without a standings file (e.g.
304
+ 3388) still returns its events and metadata, and vice versa.
287
305
 
288
306
  ## Zwiftracing Data (zrdata)
289
307
 
@@ -293,30 +311,39 @@ including rider ratings, race results, and team rosters.
293
311
  ### Command-line usage
294
312
 
295
313
  ```sh
296
- usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--v1fetch] [--noaction] [--sync]
314
+ usage: zrdata [-h] [--version] [-v] [-vv] [--log-file PATH] [-r] [--json]
315
+ [--noaction] [--sync] [--extras] [--excluded] [--v1fetch]
297
316
  [--batch] [--batch-file FILE] [--premium] [--at DATETIME]
298
- [{config,rider,result,team}] [id ...]
317
+ [CMD] [id ...]
299
318
 
300
319
  Module for fetching Zwiftracing data using the Zwiftracing API
301
320
 
302
321
  positional arguments:
303
- {config,rider,result,team}
304
- which command to run
305
- id the id to search for
322
+ CMD command to execute: {config,rider,result,team,categories}
323
+ id ID(s) for the command
306
324
 
307
325
  options:
308
- -h, --help show this help message and exit
309
- -v, --verbose enable INFO level logging to console
310
- -vv, --debug enable DEBUG level logging to console
311
- --log-file PATH path to log file (enables file logging)
312
- -r, --raw print the raw response text from the server
313
- --v1fetch output fetched data in v1.8 format (backward compatibility)
314
- --noaction report what would be done without actually fetching data
315
- --sync use synchronous (non-parallel) requests for debugging
316
- --batch use batch POST endpoint for multiple IDs (rider command only)
317
- --batch-file FILE read IDs from file (one per line) for batch request (rider command only)
318
- --premium use premium tier rate limits (higher request quotas)
319
- --at DATETIME fetch historical ratings at a date/time in UTC (rider command only)
326
+ -h, --help show this help message and exit
327
+ --version show program's version number and exit
328
+ -v, --verbose enable verbose output (INFO level logging)
329
+ -vv, --debug enable debug output (DEBUG level logging)
330
+ --log-file PATH write logging output to file
331
+ -r, --raw print raw result data as received from the server
332
+ --json output fetched data as JSON (default: object repr)
333
+ --noaction show what would be done without actually fetching data
334
+ --sync use synchronous (non-parallel) requests
335
+ --extras report recently added fields not handled natively
336
+ --excluded report recognized fields not yet explicitly handled
337
+ --v1fetch output fetched data in v1.8 format (for backward
338
+ compatibility)
339
+ --batch use batch POST endpoint for multiple IDs (rider command
340
+ only)
341
+ --batch-file FILE read IDs from file (one per line) for batch request
342
+ (rider command only)
343
+ --premium use premium tier rate limits (higher request quotas)
344
+ --at DATETIME fetch historical ratings at a given date/time (UTC), e.g.
345
+ '2024-06-15' or '2024-06-15T14:30:00' (rider command
346
+ only)
320
347
  ```
321
348
 
322
349
  **Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
@@ -342,6 +369,9 @@ zrdata result 3590800
342
369
  # Fetch team roster
343
370
  zrdata team 456
344
371
 
372
+ # Fetch vELO2 category ranges
373
+ zrdata categories
374
+
345
375
  # View current configuration
346
376
  zrdata config
347
377
 
@@ -349,6 +379,50 @@ zrdata config
349
379
  zrdata config # Will prompt for authorization header
350
380
  ```
351
381
 
382
+ ### Category Ranges (vELO2)
383
+
384
+ Fetch the vELO2 category ranges used to bucket riders by rating:
385
+
386
+ ```sh
387
+ zrdata categories
388
+ # Output: the full repr — every entry with number, name, and range:
389
+ # ZRCategories(scale='1-1000', categories=[ZRvELOCategory(
390
+ # number=1, name='Diamond', min=920, max=None), ZRvELOCategory(
391
+ # number=2, name='Ruby', min=840, max=919), ...])
392
+ ```
393
+
394
+ Use `--json` for the full mapping:
395
+
396
+ ```sh
397
+ zrdata categories --json
398
+ # Output:
399
+ # {
400
+ # "scale": "1-1000",
401
+ # "categories": [
402
+ # {"number": 1, "name": "Diamond", "min": 920, "max": null},
403
+ # {"number": 2, "name": "Ruby", "min": 840, "max": 919},
404
+ # ...
405
+ # {"number": 10, "name": "Copper", "min": 0, "max": 359}
406
+ # ]
407
+ # }
408
+ ```
409
+
410
+ `--raw` prints the unmodified API response; `--noaction` previews the fetch.
411
+
412
+ ```python
413
+ # Library usage
414
+ from zrdatafetch import ZRCategoriesFetch
415
+
416
+ categories = ZRCategoriesFetch().fetch()
417
+ for entry in categories: # iterate in API order (0 = Diamond)
418
+ print(f"{entry.number}: {entry.name} {entry.min}-{entry.max}")
419
+
420
+ silver = categories['Silver'] # lookup by exact name
421
+ diamond = categories[0] # positional access
422
+ 'Silver' in categories # membership -> True
423
+ len(categories) # 10
424
+ ```
425
+
352
426
  ### Historical Ratings
353
427
 
354
428
  Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
@@ -1208,7 +1282,7 @@ profile = ZwiftProfile()
1208
1282
  profile.fetch(550564)
1209
1283
  print(profile.json())
1210
1284
 
1211
- # Fetch followers
1285
+ # Fetch followers (all pages; the API paginates at 200 entries per page)
1212
1286
  followers = ZwiftFollowers()
1213
1287
  followers.fetch(550564)
1214
1288
  print(f"Followers: {followers.follower_count()}")
@@ -1222,7 +1296,15 @@ worlds = ZwiftWorlds()
1222
1296
  worlds.fetch()
1223
1297
 
1224
1298
  # Give a RideOn
1299
+ # rider_id = activity owner; your own id is resolved automatically
1300
+ # via GET /api/profiles/me and sent as {"profileId": <your id>}
1225
1301
  ZwiftRideOns.give_rideon(550564, 12345678)
1302
+
1303
+ # Check who gave RideOns on an activity
1304
+ rideons = ZwiftRideOns()
1305
+ rideons.fetch(550564, 12345678)
1306
+ print(rideons.rideon_ids()) # rider IDs of RideOn givers
1307
+ print(rideons.has_rideon_from(766087)) # did this rider give a RideOn?
1226
1308
  ```
1227
1309
 
1228
1310
  ## Zwift Status Data (zsdata)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "zpdatafetch"
3
- version = "2.3.2"
3
+ version = "2.4.1"
4
4
  description = "A package for fetching data from Zwiftpower and Zwiftracing.app"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,10 +1,13 @@
1
1
  """Zwift follower and followee data fetching and management.
2
2
 
3
3
  Provides access to follower/followee relationship data from Zwift's unofficial API.
4
+
5
+ Both endpoints are server-paginated (200 entries per page); every page is
6
+ collected so the complete follower/followee lists are returned.
4
7
  """
5
8
 
6
9
  import json
7
- from typing import Any
10
+ from typing import Any, Literal
8
11
 
9
12
  import httpx2
10
13
 
@@ -16,6 +19,89 @@ from zdatafetch.logging_config import get_logger
16
19
 
17
20
  logger = get_logger(__name__)
18
21
 
22
+ PAGE_SIZE = 200
23
+ MAX_PAGES = 500
24
+
25
+
26
+ def _fetch_paginated(
27
+ client: httpx2.Client,
28
+ url: str,
29
+ headers: dict[str, str],
30
+ rider_id: int,
31
+ kind: str,
32
+ mode: Literal['raise', 'partial'],
33
+ ) -> list[dict[str, Any]]:
34
+ """Fetch all pages of a followers/followees endpoint.
35
+
36
+ Zwift caps these endpoints at ``PAGE_SIZE`` entries per request. Each
37
+ request sends ``start``/``limit`` query parameters; pagination stops
38
+ when a page is empty or shorter than ``PAGE_SIZE``.
39
+
40
+ Args:
41
+ client: httpx2 client to request with
42
+ url: Endpoint URL (without start/limit parameters)
43
+ headers: Request headers
44
+ rider_id: Zwift rider ID (for logs and error messages)
45
+ kind: Endpoint label, 'followers' or 'followees'
46
+ mode: 'raise' raises NetworkError on a failed page; 'partial'
47
+ logs a warning and returns the pages collected so far
48
+
49
+ Returns:
50
+ Records merged across all fetched pages
51
+
52
+ Raises:
53
+ NetworkError: In 'raise' mode, if a page does not return 200
54
+ """
55
+ records: list[dict[str, Any]] = []
56
+ for page in range(MAX_PAGES):
57
+ start = page * PAGE_SIZE
58
+ response = client.get(
59
+ url,
60
+ headers=headers,
61
+ params={'start': start, 'limit': PAGE_SIZE},
62
+ timeout=30.0,
63
+ )
64
+
65
+ if response.status_code != 200:
66
+ if mode == 'raise':
67
+ if response.status_code == 404:
68
+ raise NetworkError(f'Rider {rider_id} not found')
69
+ raise NetworkError(
70
+ f'Failed to fetch {kind} for rider {rider_id}: '
71
+ f'HTTP {response.status_code} - {response.text}',
72
+ )
73
+ logger.warning(
74
+ f'Failed to fetch {kind} for rider {rider_id}: '
75
+ f'HTTP {response.status_code}',
76
+ )
77
+ break
78
+
79
+ page_records = parse_json_safe(response.text, context=kind)
80
+ if not isinstance(page_records, list):
81
+ logger.warning(
82
+ f'Unexpected payload for {kind} of rider {rider_id} '
83
+ f'at start={start}: expected list, '
84
+ f'got {type(page_records).__name__}',
85
+ )
86
+ break
87
+
88
+ records.extend(page_records)
89
+ logger.debug(
90
+ f'Fetched {kind} page for rider {rider_id} '
91
+ f'(start={start}): {len(page_records)} records',
92
+ )
93
+
94
+ if len(page_records) < PAGE_SIZE:
95
+ break
96
+ else:
97
+ logger.warning(
98
+ f'Pagination cap of {MAX_PAGES} pages reached for {kind} of '
99
+ f'rider {rider_id}: returning partial data '
100
+ f'({len(records)} records)',
101
+ )
102
+
103
+ return records
104
+
19
105
 
20
106
  class ZwiftFollowers:
21
107
  """Zwift follower and followee data.
@@ -27,6 +113,9 @@ class ZwiftFollowers:
27
113
  GET https://us-or-rly101.zwift.com/api/profiles/{riderId}/followers
28
114
  GET https://us-or-rly101.zwift.com/api/profiles/{riderId}/followees
29
115
 
116
+ Both endpoints are server-paginated (200 entries per page); fetch() and
117
+ fetch_multiple() collect every page so the complete lists are returned.
118
+
30
119
  Documentation: https://github.com/strukturunion-mmw/zwift-api-documentation
31
120
 
32
121
  Synchronous usage:
@@ -74,6 +163,9 @@ class ZwiftFollowers:
74
163
  Loads credentials from Config, authenticates, fetches data,
75
164
  and populates instance attributes.
76
165
 
166
+ Both lists are fetched in full; the API paginates at 200 entries per
167
+ page and all pages are collected.
168
+
77
169
  Args:
78
170
  rider_id: Zwift rider ID
79
171
  include_followers: Whether to fetch followers list
@@ -113,39 +205,23 @@ class ZwiftFollowers:
113
205
 
114
206
  try:
115
207
  with httpx2.Client() as client:
116
- # Fetch followers
208
+ # Fetch followers (all pages; failures are fatal)
117
209
  if include_followers:
118
210
  url = f'{self.BASE_URL}/api/profiles/{rider_id}/followers'
119
- response = client.get(url, headers=headers, timeout=30.0)
120
-
121
- if response.status_code == 404:
122
- raise NetworkError(f'Rider {rider_id} not found')
123
- if response.status_code != 200:
124
- raise NetworkError(
125
- f'Failed to fetch followers for rider {rider_id}: '
126
- f'HTTP {response.status_code} - {response.text}',
127
- )
128
-
129
- raw_data['followers'] = response.text
211
+ records = _fetch_paginated(
212
+ client, url, headers, rider_id, 'followers', mode='raise',
213
+ )
214
+ raw_data['followers'] = json.dumps(records)
130
215
  logger.debug(f'Successfully fetched followers for rider {rider_id}')
131
216
 
132
- # Fetch followees
217
+ # Fetch followees (all pages; failures keep collected data)
133
218
  if include_followees:
134
219
  url = f'{self.BASE_URL}/api/profiles/{rider_id}/followees'
135
- response = client.get(url, headers=headers, timeout=30.0)
136
-
137
- if response.status_code == 404:
138
- raise NetworkError(f'Rider {rider_id} not found')
139
- if response.status_code != 200:
140
- logger.warning(
141
- f'Failed to fetch followees for rider {rider_id}: '
142
- f'HTTP {response.status_code}',
143
- )
144
- # Continue with just followers data
145
- raw_data['followees'] = '[]'
146
- else:
147
- raw_data['followees'] = response.text
148
- logger.debug(f'Successfully fetched followees for rider {rider_id}')
220
+ records = _fetch_paginated(
221
+ client, url, headers, rider_id, 'followees', mode='partial',
222
+ )
223
+ raw_data['followees'] = json.dumps(records)
224
+ logger.debug(f'Successfully fetched followees for rider {rider_id}')
149
225
 
150
226
  # Parse and populate attributes
151
227
  self._parse_response(raw_data)
@@ -172,6 +248,9 @@ class ZwiftFollowers:
172
248
  ) -> dict[int, 'ZwiftFollowers']:
173
249
  """Fetch multiple riders' follower data, returning dict of objects.
174
250
 
251
+ Both lists are fetched in full; the API paginates at 200 entries per
252
+ page and all pages are collected.
253
+
175
254
  Args:
176
255
  *rider_ids: Zwift rider IDs to fetch
177
256
  include_followers: Whether to fetch followers lists
@@ -223,33 +302,22 @@ class ZwiftFollowers:
223
302
  try:
224
303
  raw_data = {}
225
304
 
226
- # Fetch followers
305
+ # Fetch followers (all pages; failure skips the rider via
306
+ # the except below)
227
307
  if include_followers:
228
308
  url = f'{cls.BASE_URL}/api/profiles/{rider_id}/followers'
229
- response = client.get(url, headers=headers, timeout=30.0)
230
-
231
- if response.status_code == 200:
232
- raw_data['followers'] = response.text
233
- else:
234
- logger.warning(
235
- f'Failed to fetch followers for rider {rider_id}: '
236
- f'HTTP {response.status_code}',
237
- )
238
- continue
239
-
240
- # Fetch followees
309
+ records = _fetch_paginated(
310
+ client, url, headers, rider_id, 'followers', mode='raise',
311
+ )
312
+ raw_data['followers'] = json.dumps(records)
313
+
314
+ # Fetch followees (all pages; failures keep collected data)
241
315
  if include_followees:
242
316
  url = f'{cls.BASE_URL}/api/profiles/{rider_id}/followees'
243
- response = client.get(url, headers=headers, timeout=30.0)
244
-
245
- if response.status_code == 200:
246
- raw_data['followees'] = response.text
247
- else:
248
- logger.warning(
249
- f'Failed to fetch followees for rider {rider_id}: '
250
- f'HTTP {response.status_code}',
251
- )
252
- raw_data['followees'] = '[]'
317
+ records = _fetch_paginated(
318
+ client, url, headers, rider_id, 'followees', mode='partial',
319
+ )
320
+ raw_data['followees'] = json.dumps(records)
253
321
 
254
322
  # Create object and populate
255
323
  followers_obj = cls()