zpdatafetch 2.4.0__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 (86) hide show
  1. {zpdatafetch-2.4.0/src/zpdatafetch.egg-info → zpdatafetch-2.4.1}/PKG-INFO +84 -20
  2. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/README.md +83 -19
  3. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/pyproject.toml +1 -1
  4. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/followers.py +119 -51
  5. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/rideons.py +56 -6
  6. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpcyclistfetch.py +1 -1
  7. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1/src/zpdatafetch.egg-info}/PKG-INFO +84 -20
  8. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/SOURCES.txt +2 -0
  9. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/__init__.py +6 -0
  10. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/cli.py +48 -2
  11. zpdatafetch-2.4.1/src/zrdatafetch/zrcategories.py +257 -0
  12. zpdatafetch-2.4.1/src/zrdatafetch/zrcategoriesfetch.py +271 -0
  13. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/LICENSE +0 -0
  14. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/setup.cfg +0 -0
  15. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/__init__.py +0 -0
  16. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/cli.py +0 -0
  17. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/config.py +0 -0
  18. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/error_helpers.py +0 -0
  19. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/exceptions.py +0 -0
  20. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/http_client.py +0 -0
  21. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/json_helpers.py +0 -0
  22. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/logging.py +0 -0
  23. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/validation.py +0 -0
  24. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/__init__.py +0 -0
  25. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/activity.py +0 -0
  26. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/auth.py +0 -0
  27. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/cli.py +0 -0
  28. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/config.py +0 -0
  29. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/logging_config.py +0 -0
  30. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/profile.py +0 -0
  31. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/ridersinworld.py +0 -0
  32. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/worlds.py +0 -0
  33. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/__init__.py +0 -0
  34. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/async_zp.py +0 -0
  35. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/cli.py +0 -0
  36. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/config.py +0 -0
  37. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/logging_config.py +0 -0
  38. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zp.py +0 -0
  39. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zp_obj.py +0 -0
  40. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zp_utils.py +0 -0
  41. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpcyclist.py +0 -0
  42. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpleague.py +0 -0
  43. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpleaguefetch.py +0 -0
  44. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpprime.py +0 -0
  45. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpprimesfetch.py +0 -0
  46. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracefinish.py +0 -0
  47. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracelog.py +0 -0
  48. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpraceresult.py +0 -0
  49. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracesignup.py +0 -0
  50. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracesprint.py +0 -0
  51. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpresultfetch.py +0 -0
  52. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpsignupfetch.py +0 -0
  53. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpsprintsfetch.py +0 -0
  54. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpteam.py +0 -0
  55. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpteamfetch.py +0 -0
  56. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/dependency_links.txt +0 -0
  57. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/entry_points.txt +0 -0
  58. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/requires.txt +0 -0
  59. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/top_level.txt +0 -0
  60. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/async_zr.py +0 -0
  61. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/config.py +0 -0
  62. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/logging_config.py +0 -0
  63. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/rate_limiter.py +0 -0
  64. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zr.py +0 -0
  65. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zr_utils.py +0 -0
  66. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrraceresult.py +0 -0
  67. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrresultfetch.py +0 -0
  68. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrrider.py +0 -0
  69. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrriderfetch.py +0 -0
  70. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrteamfetch.py +0 -0
  71. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrteamroster.py +0 -0
  72. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/__init__.py +0 -0
  73. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/async_zs.py +0 -0
  74. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/cli.py +0 -0
  75. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/logging_config.py +0 -0
  76. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/__init__.py +0 -0
  77. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/component.py +0 -0
  78. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/incident.py +0 -0
  79. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/maintenance.py +0 -0
  80. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/page.py +0 -0
  81. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/status.py +0 -0
  82. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/summary.py +0 -0
  83. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/zs.py +0 -0
  84. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/zsincidentfetch.py +0 -0
  85. {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/zsmaintenancefetch.py +0 -0
  86. {zpdatafetch-2.4.0 → 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.4.0
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
 
@@ -340,30 +340,39 @@ including rider ratings, race results, and team rosters.
340
340
  ### Command-line usage
341
341
 
342
342
  ```sh
343
- 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]
344
345
  [--batch] [--batch-file FILE] [--premium] [--at DATETIME]
345
- [{config,rider,result,team}] [id ...]
346
+ [CMD] [id ...]
346
347
 
347
348
  Module for fetching Zwiftracing data using the Zwiftracing API
348
349
 
349
350
  positional arguments:
350
- {config,rider,result,team}
351
- which command to run
352
- id the id to search for
351
+ CMD command to execute: {config,rider,result,team,categories}
352
+ id ID(s) for the command
353
353
 
354
354
  options:
355
- -h, --help show this help message and exit
356
- -v, --verbose enable INFO level logging to console
357
- -vv, --debug enable DEBUG level logging to console
358
- --log-file PATH path to log file (enables file logging)
359
- -r, --raw print the raw response text from the server
360
- --v1fetch output fetched data in v1.8 format (backward compatibility)
361
- --noaction report what would be done without actually fetching data
362
- --sync use synchronous (non-parallel) requests for debugging
363
- --batch use batch POST endpoint for multiple IDs (rider command only)
364
- --batch-file FILE read IDs from file (one per line) for batch request (rider command only)
365
- --premium use premium tier rate limits (higher request quotas)
366
- --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)
367
376
  ```
368
377
 
369
378
  **Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
@@ -389,6 +398,9 @@ zrdata result 3590800
389
398
  # Fetch team roster
390
399
  zrdata team 456
391
400
 
401
+ # Fetch vELO2 category ranges
402
+ zrdata categories
403
+
392
404
  # View current configuration
393
405
  zrdata config
394
406
 
@@ -396,6 +408,50 @@ zrdata config
396
408
  zrdata config # Will prompt for authorization header
397
409
  ```
398
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
+
399
455
  ### Historical Ratings
400
456
 
401
457
  Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
@@ -1255,7 +1311,7 @@ profile = ZwiftProfile()
1255
1311
  profile.fetch(550564)
1256
1312
  print(profile.json())
1257
1313
 
1258
- # Fetch followers
1314
+ # Fetch followers (all pages; the API paginates at 200 entries per page)
1259
1315
  followers = ZwiftFollowers()
1260
1316
  followers.fetch(550564)
1261
1317
  print(f"Followers: {followers.follower_count()}")
@@ -1269,7 +1325,15 @@ worlds = ZwiftWorlds()
1269
1325
  worlds.fetch()
1270
1326
 
1271
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>}
1272
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?
1273
1337
  ```
1274
1338
 
1275
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
 
@@ -311,30 +311,39 @@ including rider ratings, race results, and team rosters.
311
311
  ### Command-line usage
312
312
 
313
313
  ```sh
314
- 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]
315
316
  [--batch] [--batch-file FILE] [--premium] [--at DATETIME]
316
- [{config,rider,result,team}] [id ...]
317
+ [CMD] [id ...]
317
318
 
318
319
  Module for fetching Zwiftracing data using the Zwiftracing API
319
320
 
320
321
  positional arguments:
321
- {config,rider,result,team}
322
- which command to run
323
- id the id to search for
322
+ CMD command to execute: {config,rider,result,team,categories}
323
+ id ID(s) for the command
324
324
 
325
325
  options:
326
- -h, --help show this help message and exit
327
- -v, --verbose enable INFO level logging to console
328
- -vv, --debug enable DEBUG level logging to console
329
- --log-file PATH path to log file (enables file logging)
330
- -r, --raw print the raw response text from the server
331
- --v1fetch output fetched data in v1.8 format (backward compatibility)
332
- --noaction report what would be done without actually fetching data
333
- --sync use synchronous (non-parallel) requests for debugging
334
- --batch use batch POST endpoint for multiple IDs (rider command only)
335
- --batch-file FILE read IDs from file (one per line) for batch request (rider command only)
336
- --premium use premium tier rate limits (higher request quotas)
337
- --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)
338
347
  ```
339
348
 
340
349
  **Note:** All objects support both synchronous (`fetch()`) and asynchronous (`afetch()`) methods. See the Async API section below for details.
@@ -360,6 +369,9 @@ zrdata result 3590800
360
369
  # Fetch team roster
361
370
  zrdata team 456
362
371
 
372
+ # Fetch vELO2 category ranges
373
+ zrdata categories
374
+
363
375
  # View current configuration
364
376
  zrdata config
365
377
 
@@ -367,6 +379,50 @@ zrdata config
367
379
  zrdata config # Will prompt for authorization header
368
380
  ```
369
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
+
370
426
  ### Historical Ratings
371
427
 
372
428
  Use `--at` to fetch rider ratings at a specific point in time. Accepts ISO 8601
@@ -1226,7 +1282,7 @@ profile = ZwiftProfile()
1226
1282
  profile.fetch(550564)
1227
1283
  print(profile.json())
1228
1284
 
1229
- # Fetch followers
1285
+ # Fetch followers (all pages; the API paginates at 200 entries per page)
1230
1286
  followers = ZwiftFollowers()
1231
1287
  followers.fetch(550564)
1232
1288
  print(f"Followers: {followers.follower_count()}")
@@ -1240,7 +1296,15 @@ worlds = ZwiftWorlds()
1240
1296
  worlds.fetch()
1241
1297
 
1242
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>}
1243
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?
1244
1308
  ```
1245
1309
 
1246
1310
  ## Zwift Status Data (zsdata)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "zpdatafetch"
3
- version = "2.4.0"
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()
@@ -226,7 +226,10 @@ class ZwiftRideOns:
226
226
  def give_rideon(rider_id: int, activity_id: int) -> bool:
227
227
  """Give a RideOn to an activity.
228
228
 
229
- Loads credentials, authenticates, and posts a RideOn.
229
+ Loads credentials, authenticates, resolves the authenticated
230
+ rider's id via GET /api/profiles/me, and posts a RideOn with the
231
+ JSON payload required by the API:
232
+ {"profileId": <authenticated rider id>}.
230
233
 
231
234
  Args:
232
235
  rider_id: Zwift rider ID who owns the activity
@@ -257,12 +260,47 @@ class ZwiftRideOns:
257
260
  token = auth.get_access_token()
258
261
  headers = {'Authorization': f'Bearer {token}', 'Accept': 'application/json'}
259
262
 
260
- # POST RideOn
261
- url = f'{ZwiftRideOns.BASE_URL}/api/profiles/{rider_id}/activities/{activity_id}/rideon'
262
-
263
263
  try:
264
264
  with httpx2.Client() as client:
265
- response = client.post(url, headers=headers, timeout=30.0)
265
+ # Resolve the authenticated rider's id; the API requires it in
266
+ # the RideOn payload (issue #9).
267
+ me_response = client.get(
268
+ f'{ZwiftRideOns.BASE_URL}/api/profiles/me',
269
+ headers=headers,
270
+ timeout=30.0,
271
+ )
272
+
273
+ if me_response.status_code != 200:
274
+ logger.error(
275
+ f'Failed to resolve authenticated rider id: '
276
+ f'HTTP {me_response.status_code}',
277
+ )
278
+ return False
279
+
280
+ me_data = parse_json_safe(me_response.text, context='rideons')
281
+ if not isinstance(me_data, dict):
282
+ logger.error(
283
+ 'Could not determine authenticated rider id from '
284
+ '/api/profiles/me response',
285
+ )
286
+ return False
287
+ try:
288
+ me_id = int(me_data['id'])
289
+ except (KeyError, TypeError, ValueError):
290
+ logger.error(
291
+ 'Could not determine authenticated rider id from '
292
+ '/api/profiles/me response',
293
+ )
294
+ return False
295
+
296
+ # POST RideOn
297
+ url = f'{ZwiftRideOns.BASE_URL}/api/profiles/{rider_id}/activities/{activity_id}/rideon'
298
+ response = client.post(
299
+ url,
300
+ headers=headers,
301
+ json={'profileId': me_id},
302
+ timeout=30.0,
303
+ )
266
304
 
267
305
  if response.status_code in (200, 201, 204):
268
306
  logger.info(
@@ -328,10 +366,22 @@ class ZwiftRideOns:
328
366
  def rideon_ids(self) -> list[int]:
329
367
  """Return list of rider IDs who gave RideOns.
330
368
 
369
+ Each rideon record carries the rideon's own ``id`` and the giving
370
+ rider's ID in the nested ``profile.id`` field (shape verified against
371
+ the live API). Records without a usable rider ID are skipped.
372
+
331
373
  Returns:
332
374
  List of rider IDs who gave RideOns to this activity
333
375
  """
334
- return [r.get('id', 0) for r in self.rideons if 'id' in r]
376
+ ids: list[int] = []
377
+ for record in self.rideons:
378
+ profile = record.get('profile')
379
+ if not isinstance(profile, dict):
380
+ continue
381
+ rider_id = profile.get('id')
382
+ if isinstance(rider_id, int) and not isinstance(rider_id, bool):
383
+ ids.append(rider_id)
384
+ return ids
335
385
 
336
386
  def has_rideon_from(self, rider_id: int) -> bool:
337
387
  """Check if specific rider gave a RideOn.
@@ -15,7 +15,7 @@ if sys.version_info >= (3, 11):
15
15
  else:
16
16
  # For Python 3.10, anyio provides ExceptionGroup
17
17
  try:
18
- from exceptiongroup import ( # type: ignore[import-untyped]
18
+ from exceptiongroup import ( # ty: ignore[unresolved-import]
19
19
  BaseExceptionGroup,
20
20
  )
21
21
  except ImportError: