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.
- {zpdatafetch-2.4.0/src/zpdatafetch.egg-info → zpdatafetch-2.4.1}/PKG-INFO +84 -20
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/README.md +83 -19
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/pyproject.toml +1 -1
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/followers.py +119 -51
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/rideons.py +56 -6
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpcyclistfetch.py +1 -1
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1/src/zpdatafetch.egg-info}/PKG-INFO +84 -20
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/SOURCES.txt +2 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/__init__.py +6 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/cli.py +48 -2
- zpdatafetch-2.4.1/src/zrdatafetch/zrcategories.py +257 -0
- zpdatafetch-2.4.1/src/zrdatafetch/zrcategoriesfetch.py +271 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/LICENSE +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/setup.cfg +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/__init__.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/cli.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/error_helpers.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/exceptions.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/http_client.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/json_helpers.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/logging.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/shared/validation.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/__init__.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/activity.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/auth.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/cli.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/profile.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/ridersinworld.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zdatafetch/worlds.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/__init__.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/async_zp.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/cli.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zp.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zp_obj.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zp_utils.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpcyclist.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpleague.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpleaguefetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpprime.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpprimesfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracefinish.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracelog.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpraceresult.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracesignup.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpracesprint.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpresultfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpsignupfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpsprintsfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpteam.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch/zpteamfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/dependency_links.txt +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/entry_points.txt +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/requires.txt +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zpdatafetch.egg-info/top_level.txt +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/async_zr.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/rate_limiter.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zr.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zr_utils.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrraceresult.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrresultfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrrider.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrriderfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrteamfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zrdatafetch/zrteamroster.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/__init__.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/async_zs.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/cli.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/logging_config.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/__init__.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/component.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/incident.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/maintenance.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/page.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/status.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/models/summary.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/zs.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/zsincidentfetch.py +0 -0
- {zpdatafetch-2.4.0 → zpdatafetch-2.4.1}/src/zsdatafetch/zsmaintenancefetch.py +0 -0
- {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.
|
|
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] [--
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
|
|
359
|
-
-
|
|
360
|
-
--
|
|
361
|
-
--
|
|
362
|
-
--
|
|
363
|
-
--
|
|
364
|
-
--
|
|
365
|
-
--
|
|
366
|
-
--
|
|
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] [--
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
-
|
|
329
|
-
|
|
330
|
-
-
|
|
331
|
-
--
|
|
332
|
-
--
|
|
333
|
-
--
|
|
334
|
-
--
|
|
335
|
-
--
|
|
336
|
-
--
|
|
337
|
-
--
|
|
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,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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 ( #
|
|
18
|
+
from exceptiongroup import ( # ty: ignore[unresolved-import]
|
|
19
19
|
BaseExceptionGroup,
|
|
20
20
|
)
|
|
21
21
|
except ImportError:
|