A package for fetching data from Zwiftpower and Zwiftracing.app
Project description
zpdatafetch & zrdatafetch
A python library and command-line tool for fetching data from ZwiftPower.com and Zwiftracing.app APIs.
Installation
pip install zpdatafetch
or
uv add zpdatafetch
This currently works with python versions 3.10 - 3.14 including 3.14t but excluding 3.13t. Build fails on 3.13t and I am unlikely to fix it. If you want free threading, use 3.14t.
Note that while this builds and runs, it'd not yet properly tested to run in a real free-threaded environment. Please do report any issues you find.
Overview
This package provides two main command-line tools:
| Tool | API | Purpose | Data Types |
|---|---|---|---|
zpdata |
ZwiftPower | Race rankings, signups, results | Cyclist, Primes, Results, Signups, Teams |
zrdata |
Zwiftracing | Rider ratings, race results, rosters | Rider Ratings, Race Results, Team Rosters |
Both tools support batch operations, flexible logging, and can be used as standalone CLI tools or imported as libraries. They maintain separate credential stores for each API.
Key Features
For zpdata (ZwiftPower)
- Cyclist rankings - Individual and batch lookups by Zwift ID
- Race results - Detailed finish information and point scoring
- Signups - Event signup lists and participant info
- Primes/Sprints - Intermediate results and prime tracking
- Team data - Team rosters and member information
- Async support - Concurrent fetching with asyncio or trio backends
- Connection pooling - Efficient batch operations with shared HTTP client
For zrdata (Zwiftracing)
- Rider ratings - Current, max30, max90 ratings and categories
- Batch POST requests - Fetch up to 1000 riders in a single efficient API call
- Derived rating scores - Calculated DRS for all riders
- Power metrics - Zwiftracing compound score and power data
- Race results - Complete race result data with rating changes
- Team rosters - Full team member details and power metrics
- File-based batch input - Read rider IDs from files for batch processing
- Safe testing -
--noactionflag to preview operations without network calls
Common Features
- Flexible logging - Console and file logging with multiple levels (DEBUG, INFO, WARNING, ERROR)
- Secure credentials - System keyring integration for safe credential storage
- CLI and library APIs - Use as command-line tools or import as Python libraries
- JSON output - Raw JSON or formatted output for all data types
- Error handling - Comprehensive error messages and retry logic
Credentials Setup
For ZwiftPower (zpdata), you will need a zwiftpower account with credentials in your system keyring:
keyring set zpdatafetch username
keyring set zpdatafetch password
For Zwiftracing (zrdata), you will need your Zwiftracing API authorization header:
keyring set zrdatafetch authorization
# Or use the CLI: zrdata config
In principle, the library can use alternate backend keyrings, but I have not tested this so far. At the moment, only the system keyring is used. See the keyring docs for more details on how to use the keyring and keyring library for your system.
ZwiftPower Data (zpdata)
The zpdata command-line tool provides access to ZwiftPower data including cyclist rankings, race results, and event signups.
Command-line example
usage: zpdata [-h] [-v] [-vv] [--log-file PATH] [-r] [{config,cyclist,primes,result,signup,team}] [id ...]
Module for fetching zwiftpower data using the Zwifpower API
positional arguments:
{config,cyclist,primes,result,signup,team}
which command to run
id the id to search for, ignored for config
options:
-h, --help show this help message and exit
-v, --verbose enable INFO level logging to console
-vv, --debug enable DEBUG level logging to console
--log-file PATH path to log file (enables file logging)
-r, --raw print the raw results returned to screen
Basic usage:
# Fetch cyclist data (quiet mode - only errors shown)
zpdata cyclist 1234567
# Verbose mode - show INFO messages
zpdata -v cyclist 1234567
# Debug mode - show DEBUG messages
zpdata -vv cyclist 1234567
# Log to file only (quiet console)
zpdata --log-file zpdatafetch.log cyclist 1234567
# Both console and file logging
zpdata -v --log-file zpdatafetch.log cyclist 1234567
Library example (Synchronous API)
from zpdatafetch import Cyclist
c = Cyclist()
c.fetch(1234567) # fetch data for cyclist with zwift id 1234567
print(c.json())
The interface for each of the objects is effectively the same as the example above, with the individual class and id number changed as appropriate. The available classes are as follows:
- Cyclist: fetch one or more cyclists by zwift id
- Primes: fetch primes from one or more races using event id
- Result: fetch results from one or more races (finish, points) using event id
- Signup: fetch signups for a particular event by event id
- Team: fetch team data by team id
Zwiftracing Data (zrdata)
The zrdata command-line tool provides access to Zwiftracing API data including rider ratings, race results, and team rosters.
Command-line usage
usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--noaction] [--batch] [--batch-file FILE]
[{config,rider,result,team}] [id ...]
Module for fetching Zwiftracing data using the Zwiftracing API
positional arguments:
{config,rider,result,team}
which command to run
id the id to search for
options:
-h, --help show this help message and exit
-v, --verbose enable INFO level logging to console
-vv, --debug enable DEBUG level logging to console
--log-file PATH path to log file (enables file logging)
-r, --raw print the raw results returned to screen
--noaction report what would be done without actually fetching data
--batch use batch POST endpoint for multiple IDs (rider command only)
--batch-file FILE read IDs from file (one per line) for batch request (rider command only)
Basic Examples
# Fetch a single rider's rating data
zrdata rider 12345
# Fetch multiple riders individually (GET requests)
zrdata rider 12345 67890 11111
# Fetch multiple riders using batch POST endpoint (more efficient)
zrdata rider --batch 12345 67890 11111
# Fetch riders from a file
zrdata rider --batch-file riders.txt
# Fetch race results
zrdata result 3590800
# Fetch team roster
zrdata team 456
# View current configuration
zrdata config
# Set up authorization
zrdata config # Will prompt for authorization header
Advanced Options
# Verbose output with debug logging
zrdata -vv rider 12345
# Raw JSON output
zrdata -r rider 12345
# Test what would be fetched without making requests
zrdata --noaction --batch 12345 67890
# Log to file
zrdata --log-file zrdata.log rider 12345
# Combine options
zrdata -v --batch -r rider 12345 67890 11111
Batch Processing
For efficiency, zrdata supports batch operations that use the Zwiftracing API's POST endpoints:
Command-line batch:
# Batch with inline IDs (up to 1000 per request)
zrdata rider --batch 123 456 789
# Batch from file
cat > riders.txt << EOF
12345
67890
11111
EOF
zrdata rider --batch-file riders.txt
Programmatic batch (Python):
from zrdatafetch import ZRRider
# Batch fetch multiple riders in one API request
riders = ZRRider.fetch_batch(12345, 67890, 11111)
for zwift_id, rider in riders.items():
print(f"{rider.name}: {rider.current_rating}")
Library Usage
from zrdatafetch import ZRRider, ZRResult, ZRTeam
# Fetch single rider
rider = ZRRider(zwift_id=12345)
rider.fetch()
print(rider.json())
# Fetch batch of riders (more efficient)
riders = ZRRider.fetch_batch(12345, 67890, 11111)
for zwift_id, rider in riders.items():
print(f"{rider.name} - Rating: {rider.current_rating}")
# Fetch race results
result = ZRResult(race_id=3590800)
result.fetch()
print(f"Found {len(result.results)} riders")
# Fetch team roster
team = ZRTeam(team_id=456)
team.fetch()
print(f"Team: {team.team_name}")
for rider in team.riders:
print(f" {rider.name}: {rider.current_rating}")
Data Classes
ZRRider: Individual rider rating data
zwift_id: Rider's Zwift IDname: Rider's display namecurrent_rating: Current rating scorecurrent_rank: Current category rank (A, B, C, D, etc.)max30_rating: Best rating in last 30 daysmax30_rank: Category for max30max90_rating: Best rating in last 90 daysmax90_rank: Category for max90drs_rating: Derived rating score (max30 or max90)drs_rank: Category for DRSgender: Rider gender (M/F)zrcs: Zwiftracing compound score (power metric)
ZRResult: Race result data
race_id: The race ID (Zwift event ID)results: List of rider results with positions and rating changes
ZRTeam: Team roster data
team_id: Team/club IDteam_name: Team nameriders: List of team members with their ratings and power metrics
Configuration
To set up Zwiftracing API authorization:
# Interactive setup
zrdata config
# Or set directly in keyring
keyring set zrdatafetch authorization
# Then enter your Zwiftracing API authorization header
Rate Limiting
The Zwiftracing API enforces rate limits to prevent abuse. zrdatafetch automatically respects these limits and provides helpful messages when limits are exceeded.
Rate Limit Tiers
The API has two tier levels with different request quotas:
Standard Tier (Default):
- Riders (GET): 5 requests per 1 minute
- Riders (POST batch): 1 request per 15 minutes
- Results: 1 request per 1 minute
- Clubs (Teams): 1 request per 60 minutes
Premium Tier:
- Riders (GET): 10 requests per 1 minute (2x)
- Riders (POST batch): 10 requests per 15 minutes (10x)
- Results: 1 request per 1 minute (same as standard)
- Clubs (Teams): 10 requests per 60 minutes (10x)
You should know you're in the Premium tier from discussion with Tim Hanson. If you haven't spoken to him, you can contact him via the Zwiftracing Discord Server.
Using Premium Tier
To use premium tier rate limits, add the --premium flag to any CLI command:
# Fetch with premium rate limits
zrdata --premium rider 12345 67890
# Batch fetch with premium limits
zrdata --premium rider --batch 12345 67890 11111
# Batch from file with premium limits
zrdata --premium rider --batch-file riders.txt
# Check team with premium limits
zrdata --premium team 456
Rate Limit Errors
If you exceed the rate limit, you'll see a clear error message:
Rate limit exceeded (standard tier).
Status: 429 Too Many Requests.
Current rate limit status: {...}
Library Usage with Rate Limiting
from zrdatafetch import ZRRider, ZR_obj
# Use standard tier (default)
rider = ZRRider(zwift_id=12345)
rider.fetch()
# Use premium tier - set globally
ZR_obj.set_premium_mode(True)
rider = ZRRider(zwift_id=12345)
rider.fetch() # Now uses premium tier limits
# Or check current rate limit status
status = ZRRider().rate_limiter.get_status() if hasattr(ZRRider(), 'rate_limiter') else None
Async Rate Limiting
Async operations also respect rate limits with automatic throttling:
import anyio
from zrdatafetch import AsyncZRRider, AsyncZR_obj
async def main():
# Standard tier (default)
async with AsyncZR_obj() as zr:
rider = AsyncZRRider()
rider.set_session(zr)
await rider.fetch(12345) # Auto-throttled to standard limits
# Premium tier
async with AsyncZR_obj(premium=True) as zr:
rider = AsyncZRRider()
rider.set_session(zr)
await rider.fetch(12345) # Auto-throttled to premium limits
anyio.run(main)
Async Library API
The library provides a full async/await API for concurrent operations using anyio for backend-agnostic async support (asyncio or trio):
import anyio
from zrdatafetch import AsyncZRRider, AsyncZRResult, AsyncZRTeam, AsyncZR_obj
async def main():
# Use async context manager for automatic resource cleanup
async with AsyncZR_obj() as zr:
# Fetch multiple resources concurrently
rider = AsyncZRRider()
result = AsyncZRResult()
team = AsyncZRTeam()
# Set shared session for all operations
rider.set_session(zr)
result.set_session(zr)
team.set_session(zr)
# Fetch multiple items concurrently using task group
async with anyio.create_task_group() as tg:
tg.start_soon(rider.fetch, 12345)
tg.start_soon(result.fetch, 3590800)
tg.start_soon(team.fetch, 456)
print(rider.json())
print(result.json())
print(team.json())
# Run with asyncio (default)
anyio.run(main)
# Or use trio: pip install zpdatafetch[trio]
# anyio.run(main, backend='trio')
Async batch operations:
import anyio
from zrdatafetch import AsyncZRRider, AsyncZR_obj
async def main():
async with AsyncZR_obj() as zr:
# Batch fetch up to 1000 riders in one request
riders = await AsyncZRRider.fetch_batch(
12345, 67890, 11111, # Up to 1000 IDs
zr=zr # Use shared session
)
for zwift_id, rider in riders.items():
print(f"{rider.name}: {rider.current_rating}")
# Batch fetch with historical data
historical = await AsyncZRRider.fetch_batch(
12345, 67890,
epoch=1704067200, # Unix timestamp
zr=zr
)
anyio.run(main)
Connection pooling:
For maximum efficiency with multiple async operations, use a shared client:
import anyio
from zrdatafetch import AsyncZRRider, AsyncZR_obj
async def main():
# Create shared session for multiple operations
async with AsyncZR_obj(shared_client=True) as zr:
# All riders share the same HTTP connection pool
tasks = []
async with anyio.create_task_group() as tg:
for zwift_id in [12345, 67890, 11111]:
rider = AsyncZRRider()
rider.set_session(zr)
tg.start_soon(rider.fetch, zwift_id)
anyio.run(main)
Available async classes:
AsyncZR_obj: Async base class with httpx.AsyncClient supportAsyncZRRider: Async rider ratings fetching (with batch POST support)AsyncZRResult: Async race results fetchingAsyncZRTeam: Async team roster fetching
Async Library example
The library also provides a full async/await API for concurrent operations:
import anyio
from zpdatafetch import AsyncCyclist, AsyncResult, AsyncZP
async def main():
# Use async context manager
async with AsyncZP() as zp:
cyclist = AsyncCyclist()
result = AsyncResult()
cyclist.set_session(zp)
result.set_session(zp)
# Fetch multiple resources concurrently
async with anyio.create_task_group() as tg:
tg.start_soon(cyclist.fetch, 1234567, 7654321) # Multiple cyclists
tg.start_soon(result.fetch, 3590800, 3590801) # Multiple races
print(cyclist.json())
print(result.json())
anyio.run(main)
Available async classes:
- AsyncZP: Async authentication and HTTP client
- AsyncCyclist: Async cyclist data fetching
- AsyncResult: Async race results fetching
- AsyncSignup: Async signup list fetching
- AsyncTeam: Async team data fetching
- AsyncPrimes: Async prime/sprint data fetching
Async backend support:
The async API uses anyio to support both asyncio and trio backends:
- asyncio (default): Built into Python, widely used
- trio (optional): Install with
pip install zpdatafetch[trio]
You can use either backend transparently - the same code works with both.
See local/ASYNC_API_DOCUMENTATION.md and examples/async_*.py for detailed async usage examples.
The ZP class is the main driver for the library. It is used to fetch the data from zwiftpower. The other classes are used to parse the data into a more useful format.
Context Manager (Resource Management)
The library now supports context managers for automatic resource cleanup. This is especially useful when making multiple requests, as it ensures proper cleanup of the underlying HTTP session:
from zpdatafetch import Cyclist
# Using context manager for automatic cleanup
with Cyclist() as c:
c.fetch([1234567, 7654321]) # fetch multiple cyclists
print(c.json())
# HTTP session is automatically closed
Connection Pooling (Performance Optimization)
For batch operations, you can enable connection pooling to reuse a single HTTP client across multiple requests. This significantly improves performance when making multiple API calls:
from zpdatafetch import Cyclist, Result
# Multiple operations share a single connection pool
with Cyclist(shared_client=True) as cyclist:
cyclist.fetch([1234567, 7654321, 9876543])
cyclist_data = cyclist.json()
with Result(shared_client=True) as result:
result.fetch([111111, 222222, 333333])
result_data = result.json()
# Clean up shared session when done
from zpdatafetch.zp import ZP
ZP.close_shared_session()
The shared_client=True option (enabled by default) allows multiple instances to reuse the same HTTP connection pool, reducing overhead and improving throughput.
Automatic Retry with Exponential Backoff
The library includes built-in retry logic with exponential backoff for handling transient network failures. This is automatically applied to fetch_json() and fetch_page() methods:
from zpdatafetch import Cyclist
c = Cyclist()
# Retries are automatically handled internally
# Default: 3 retries with exponential backoff
c.fetch(1234567) # Automatically retries on transient errors
print(c.json())
For direct HTTP operations via the ZP class, you can configure retry behavior:
from zpdatafetch.zp import ZP
zp = ZP()
# Fetch with custom retry settings
data = zp.fetch_json(
'/some/endpoint',
max_retries=5, # number of retries
backoff_factor=1.5 # exponential backoff multiplier
)
The retry mechanism automatically handles:
- Connection errors
- Timeout errors
- Request errors
- HTTP 5xx server errors
This makes the library more resilient to temporary network issues and server hiccups.
Logging
zpdatafetch provides flexible logging support for both library and command-line usage.
Default Behavior (Quiet Mode)
By default, the library is completely quiet except for errors, which are sent to stderr. This ensures that library users get clean output unless something goes wrong.
Library Usage with Logging
To enable logging when using zpdatafetch as a library, use the setup_logging() function:
from zpdatafetch import setup_logging, Cyclist
# Enable console logging at INFO level
setup_logging(console_level='INFO')
c = Cyclist()
c.fetch(1234567)
Logging Configuration Options:
from zpdatafetch import setup_logging
# File logging only (no console output except errors)
setup_logging(log_file='zpdatafetch.log', force_console=False)
# Console logging at DEBUG level
setup_logging(console_level='DEBUG')
# Both console (INFO) and file (DEBUG) logging
setup_logging(
log_file='debug.log',
console_level='INFO', # Simple messages to console
file_level='DEBUG' # Detailed logs to file
)
# Force console logging even when not in a TTY
setup_logging(console_level='INFO', force_console=True)
Log Format:
- Console output: Simple, clean format showing only messages (e.g.,
"Logging in to Zwiftpower") - File output: Detailed format with timestamps, module names, log levels, function names, and line numbers
2025-10-24 15:17:39 - zpdatafetch.zp - INFO - login:90 - Logging in to Zwiftpower
Available Log Levels:
'DEBUG'- Detailed diagnostic information'INFO'- General informational messages'WARNING'- Warning messages'ERROR'- Error messages (default)
Object signature
Each object has a common set of methods available:
obj.fetch(id) or obj.fetch([id1, id2, id3]) # fetch the data from zwiftpower. As argument, fetch expects a single ID or a list (tuple or array) of IDs.
obj.json() # return the data as a json object
obj.asdict() # return the data as a dictionary
print(obj) # effectively the same as obj.asdict()
Development
I've switched over to using https://astral.sh/'s https://astral.sh/uv/ for the development toolchain. Directions below try to cover both options.
- Install this package
- Install the requirements
pip install -r requirements.txt
or
uv sync
- Set up your keyring. You may want to use a account that is separate from the one you use for riding and racing for this.
keyring set zpdatafetch username
keyring set zpdatafetch password
- Run the downloader
PYTHONPATH=`pwd`/src python src/zpdatafetch/zp.py
or
uv run src/zpdatafetch/zp.py
This should return a '200' message if you've set everything up correctly, proving that the program can log in correctly to Zwiftpower.
With a few exceptions, each object has a callable interface that can be used for simple direct access to experiment without additional code wrapped around it - yours or the provided command-line tool. They each respond to the -h flag to provide help. Basic examples follow.
Cyclist example
PYTHONPATH=`pwd`/src python src/zpdatafetch/cyclist.py -v -r <zwift_id>
Team example
PYTHONPATH=`pwd`/src python src/zpdatafetch/team.py -v -r <team_id>
Signup example
PYTHONPATH=`pwd`/src python src/zpdatafetch/signup.py -v -r <race_id>
Result example
PYTHONPATH=`pwd`/src python src/zpdatafetch/result.py -v -r <race_id>
Primes example
PYTHONPATH=`pwd`/src python src/zpdatafetch/primes.py -v -r <race_id>
- Build the project
build
or
uvx --from build pyproject-build --installer uv
To Do & Known Issues
While useful and usable, there's a bit that can be done to improve this package. Anyone interested to contribute is welcome to do so. These are the areas where I could use help:
- Check if there are any objects not handled
- Update the interface to allow alternate keyrings
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zpdatafetch-1.4.1.tar.gz.
File metadata
- Download URL: zpdatafetch-1.4.1.tar.gz
- Upload date:
- Size: 82.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f941d83e8ef971a82d7e7f8707c119b44fd3af80ca1f2e5d7891db988db719e9
|
|
| MD5 |
5279651c420252827232cee21f23415e
|
|
| BLAKE2b-256 |
0dfee5045fbadf878a144e60caebeabcfb5a2a01387833f9f70874e81eff462f
|
File details
Details for the file zpdatafetch-1.4.1-py3-none-any.whl.
File metadata
- Download URL: zpdatafetch-1.4.1-py3-none-any.whl
- Upload date:
- Size: 69.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9477a27655586ada2e51b49627b5d3c011e970ec42a5f45041112654282892e6
|
|
| MD5 |
fa2415bb2df6cbc783902942762f9633
|
|
| BLAKE2b-256 |
87cf297f70db753fa8a6e97a98b4c681f2120a077c0bf1cac13f53ab235aead5
|