A package for fetching data from Zwiftpower and Zwiftracing.app
Project description
zdatafetch, zpdatafetch, zrdatafetch, & zsdatafetch
A python library and command-line tool for fetching data from ZwiftPower.com, Zwiftracing.app, and Zwift Status APIs.
Installation
uv add zpdatafetch
or
pip install 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 four command-line tools:
| Tool | API | Purpose | Data Types |
|---|---|---|---|
zpdata |
ZwiftPower | Race rankings, signups, results | Cyclist, Primes, Results, Signups, Sprints, Teams, League |
zrdata |
Zwiftracing | Rider ratings, race results, rosters | Rider Ratings, Race Results, Team Rosters |
zdata |
Zwift | Profiles, followers, activities, worlds | Profile, Followers, RideOns, Activity, Worlds, Riders |
zsdata |
Zwift Status | Service status, incidents, maintenance | Summary, Components, Incidents, Maintenance |
All tools support flexible logging and can be used as standalone CLI tools or
imported as Python libraries. zpdata, zrdata, and zdata require
credentials; zsdata uses the public Zwift Status API (no authentication
required).
Key Features
For zpdata (ZwiftPower)
- Cyclist rankings - Individual and batch lookups by Zwift ID
- Race results - Detailed finish information and point scoring per race
- Signups - Event signup lists and participant info
- Primes - Prime results for Fastest Through Segment (FTS) and First Across the Line (FAL)
- Sprints - Sprint data including sprint details and positions
- Team data - Team rosters and member information
- League data - League standings and Zwift Racing Score (ZRS) information
For zrdata (Zwiftracing)
- Rider ratings - Current, max30, max90 ratings and categories
- 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
For zdata (Zwift)
- Rider profiles - Fetch rider profile data by Zwift ID
- Followers - Follower and followee lists with filtering
- RideOns - Fetch RideOn data or give RideOns to activities
- Activity history - Fetch activity data with pagination
- Worlds - List currently active Zwift worlds
- Riders in world - See who is riding in a specific world
For zsdata (Zwift Status)
- Service status - Overall system status and component health
- Components - Individual service component status (Game, Web, Companion, etc.)
- Incidents - Current and historical incident reports with timelines
- Maintenance - Scheduled maintenance windows (all, upcoming, active)
Common Features
- Async support - Concurrent fetching with asyncio or trio backends
- Synchronous debug mode -
--syncflag for sequential, non-parallel requests - Connection pooling - Efficient batch operations with shared HTTP client
- 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
# or with zpdata
zpdata config
For Zwiftracing (zrdata), you will need your Zwiftracing API authorization header:
keyring set zrdatafetch authorization
# or with zrdata
zrdata config
For Zwift (zdata), you will need your Zwift account credentials:
zdata 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] [--v1fetch] [--noaction] [--sync]
[{config,cyclist,primes,result,signup,sprints,team}] [id ...]
Module for fetching zwiftpower data using the Zwifpower API
positional arguments:
{config,cyclist,primes,result,signup,sprints,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 response text from the server
--v1fetch output fetched data in v1.8 format (backward compatibility)
--noaction show what would be done without actually fetching data
--sync use synchronous (non-parallel) requests for debugging
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
# Synchronous mode for debugging (sequential, non-parallel requests)
zpdata --sync cyclist 1234567
# Debug mode with synchronous requests (ideal for troubleshooting)
zpdata -vv --sync cyclist 1234567
CLI Output Formats
The zpdata command supports multiple output formats:
Default output - Clean, parsed JSON data:
zpdata cyclist 123456
# Output: {"123456": {"name": "John Doe", "zwid": 123456, ...}}
--raw flag - True raw response text from the server:
# Single ID: outputs just the raw string
zpdata --raw cyclist 123456
# Output: {"name": "John Doe", "zwid": 123456, ...}
# Multiple IDs: outputs key: value format (one per line)
zpdata --raw cyclist 123456 789012
# Output:
# 123456: {"name": "John Doe", "zwid": 123456, ...}
# 789012: {"name": "Jane Smith", "zwid": 789012, ...}
--v1fetch flag - Parsed/fetched data (backward compatibility with v1.8):
zpdata --v1fetch cyclist 123456
# Output: {"123456": {"name": "John Doe", "zwid": 123456, ...}}
Library example (Synchronous API)
from zpdatafetch import Cyclist
c = Cyclist()
c.fetch(1234567) # fetch data for cyclist with zwift id 1234567
print(c.json())
Library Data Access Methods:
Each data class provides multiple ways to access the fetched data:
from zpdatafetch import Cyclist
c = Cyclist()
c.fetch(123456, 789012) # Fetch multiple cyclists
# Default: Get parsed/fetched data as JSON string
json_str = c.json()
# Returns: '{"123456": {"name": "John", ...}, "789012": {"name": "Jane", ...}}'
# Get fetched data as dictionary
data_dict = c.fetched() # or c.asdict()
# Returns: {123456: {"name": "John", ...}, 789012: {"name": "Jane", ...}}
# Get raw response text from server (as received)
raw_dict = c.raw()
# Returns: {123456: '{"name": "John", ...}', 789012: '{"name": "Jane", ...}'}
Single vs Multiple Object Results:
The data structure differs depending on whether you fetch one or multiple objects:
from zpdatafetch import Cyclist
# Single object
c = Cyclist()
c.fetch(123456)
c.fetched() # Returns: {123456: {"name": "John", ...}}
c.raw() # Returns: {123456: '{"name": "John", ...}'}
# Multiple objects
c.fetch(123456, 789012)
c.fetched() # Returns: {123456: {...}, 789012: {...}}
c.raw() # Returns: {123456: '...', 789012: '...'}
Synchronous mode (library usage):
You can enable synchronous mode to force sequential, non-parallel requests. This provides a clear, separate execution path:
from zpdatafetch import Cyclist
# Enable synchronous mode for debugging (class-level setting)
Cyclist.set_sync_mode(True)
# All fetch calls now use sequential requests
c = Cyclist()
c.fetch(1234567, 2345678) # Fetches sequentially, not in parallel
# Disable sync mode when done
Cyclist.set_sync_mode(False)
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
- Sprints: fetch sprints from one or more races using event id
- Team: fetch team data by team id
- League: fetch league standings by league id
Zwiftracing Data (zrdata)
The zrdata command-line tool provides access to Zwiftracing.app API data
including rider ratings, race results, and team rosters.
Command-line usage
usage: zrdata [-h] [-v] [-vv] [--log-file PATH] [-r] [--v1fetch] [--noaction] [--sync]
[--batch] [--batch-file FILE] [--premium] [--at DATETIME]
[{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 response text from the server
--v1fetch output fetched data in v1.8 format (backward compatibility)
--noaction report what would be done without actually fetching data
--sync use synchronous (non-parallel) requests for debugging
--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)
--premium use premium tier rate limits (higher request quotas)
--at DATETIME fetch historical ratings at a date/time in UTC (rider command only)
Note: All objects support both synchronous (fetch()) and asynchronous (afetch()) methods. See the Async API section below for details.
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
Historical Ratings
Use --at to fetch rider ratings at a specific point in time. Accepts ISO 8601
date/time strings, interpreted as UTC:
# Fetch ratings as of a specific date
zrdata rider --at 2024-06-15 12345
# Fetch ratings at a specific date and time
zrdata rider --at "2024-06-15 14:30" 12345
# ISO 8601 with T separator
zrdata rider --at 2024-06-15T14:30:00 12345
# Combine with batch
zrdata rider --batch --at 2024-06-15 12345 67890 11111
# Preview what would be fetched
zrdata rider --noaction --at 2024-06-15 12345
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
# Synchronous mode for debugging
zrdata --sync rider 12345
# Debug mode with synchronous requests (ideal for troubleshooting)
zrdata -vv --sync rider 12345
# Log to file
zrdata --log-file zrdata.log rider 12345
# Combine options
zrdata -v --batch -r rider 12345 67890 11111
CLI Output Formats
The zrdata command supports multiple output formats:
Default output - Clean JSON with rider attributes:
zrdata rider 12345
# Output: {"zwift_id": 12345, "name": "John Doe", "current_rating": 650, ...}
--raw flag - True raw response text from the server:
# Single ID: outputs just the raw string
zrdata --raw rider 12345
# Output: {"id": 12345, "name": "John Doe", "rating": 650, ...}
# Multiple IDs: outputs key: value format (one per line)
zrdata --raw rider 12345 67890
# Output:
# 12345: {"id": 12345, "name": "John Doe", "rating": 650, ...}
# 67890: {"id": 67890, "name": "Jane Smith", "rating": 720, ...}
--v1fetch flag - Parsed/fetched data as JSON dict:
zrdata --v1fetch rider 12345
# Output: {"id": 12345, "name": "John Doe", "rating": 650, ...}
Batch Processing
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 (Synchronous API)
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}")
Library Data Access Methods (ZRRider, ZRResult, ZRTeam):
Each data class provides multiple ways to access the fetched data:
from zrdatafetch import ZRRider
rider = ZRRider(zwift_id=12345)
rider.fetch()
# Default: Get rider data as JSON with public attributes
json_str = rider.json()
# Returns: '{"zwift_id": 12345, "name": "John", "current_rating": 650, ...}'
# Get fetched/parsed data as dictionary (internal format from API)
fetched_dict = rider.fetched()
# Returns: {"id": 12345, "name": "John", "rating": 650, ...}
# Get raw response text from server (as received)
raw_str = rider.raw()
# Returns: '{"id": 12345, "name": "John", "rating": 650, ...}'
Single vs Multiple Object Results:
For ZRRider, ZRResult, and ZRTeam, each instance represents a single object:
from zrdatafetch import ZRRider
# Single rider instance
rider = ZRRider(zwift_id=12345)
rider.fetch()
rider.json() # Returns: '{"zwift_id": 12345, ...}'
rider.fetched() # Returns: {"id": 12345, ...} (dict)
rider.raw() # Returns: '{"id": 12345, ...}' (string)
# Multiple riders via batch
riders = ZRRider.fetch_batch(12345, 67890)
# Returns: {12345: ZRRider(...), 67890: ZRRider(...)}
for zwift_id, rider in riders.items():
print(rider.raw()) # Each rider's raw string
print(rider.fetched()) # Each rider's fetched dict
print(rider.json()) # Each rider's JSON representation
Synchronous mode (library usage):
You can enable synchronous mode to force sequential, non-parallel requests:
from zrdatafetch import ZRRider, ZRResult, ZRTeam
# Enable synchronous mode for debugging (class-level setting)
ZRRider.set_sync_mode(True)
ZRResult.set_sync_mode(True)
ZRTeam.set_sync_mode(True)
# All fetch calls now use sequential requests
rider = ZRRider(zwift_id=12345)
rider.fetch() # Uses synchronous fetch path
# Disable sync mode when done
ZRRider.set_sync_mode(False)
ZRResult.set_sync_mode(False)
ZRTeam.set_sync_mode(False)
This provides a clear, separate execution path.
Note: All data classes support both synchronous (fetch()) and asynchronous (afetch()) methods. See the Async API section below for details.
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 (Zwiftracing)
The library provides a full async/await API for concurrent operations using anyio for backend-agnostic async support (asyncio or trio).
Unified API Design: All data classes (ZRRider, ZRResult, ZRTeam) support both synchronous and asynchronous operations:
- Use
fetch()for synchronous operations - Use
afetch()for asynchronous operations type(obj)returns the same class regardless of sync/async usage
import anyio
from zrdatafetch import ZRRider, ZRResult, ZRTeam, AsyncZR_obj
async def main():
# Use async context manager for automatic resource cleanup
async with AsyncZR_obj() as zr:
# Create instances of unified data classes
rider = ZRRider()
result = ZRResult()
team = ZRTeam()
# 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.afetch, 12345) # Use afetch() for async
tg.start_soon(result.afetch, 3590800) # Use afetch() for async
tg.start_soon(team.afetch, 456) # Use afetch() for async
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 ZRRider, AsyncZR_obj
async def main():
async with AsyncZR_obj() as zr:
# Batch fetch up to 1000 riders in one request
riders = await ZRRider.afetch_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 (epoch is a Unix timestamp)
from shared.validation import parse_datetime_to_epoch
historical = await ZRRider.afetch_batch(
12345, 67890,
epoch=parse_datetime_to_epoch('2024-01-01'),
zr=zr
)
anyio.run(main)
Connection pooling:
For maximum efficiency with multiple async operations, use a shared client:
import anyio
from zrdatafetch import ZRRider, 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 = ZRRider()
rider.set_session(zr)
tg.start_soon(rider.afetch, zwift_id) # Use afetch()
anyio.run(main)
Backwards compatibility:
For backwards compatibility, the old AsyncZRRider, AsyncZRResult, and AsyncZRTeam names are still available as aliases:
from zrdatafetch import AsyncZRRider, ZRRider
# These are the same class
assert AsyncZRRider is ZRRider # True!
Async Library API (ZwiftPower)
The library provides a full async/await API for concurrent operations using anyio for backend-agnostic async support (asyncio or trio).
Unified API Design: All data classes (Cyclist, Result, Signup, Team, Primes) support both synchronous and asynchronous operations:
- Use
fetch()for synchronous operations - Use
afetch()for asynchronous operations type(obj)returns the same class regardless of sync/async usage
import anyio
from zpdatafetch import Cyclist, Result, AsyncZP
async def main():
# Use async context manager
async with AsyncZP() as zp:
cyclist = Cyclist()
result = Result()
cyclist.set_session(zp)
result.set_session(zp)
# Fetch multiple resources concurrently
async with anyio.create_task_group() as tg:
tg.start_soon(cyclist.afetch, 1234567, 7654321) # Use afetch() for async
tg.start_soon(result.afetch, 3590800, 3590801) # Use afetch() for async
print(cyclist.json())
print(result.json())
anyio.run(main)
Backwards compatibility:
For backwards compatibility, the old AsyncCyclist, AsyncResult, AsyncSignup, AsyncTeam, and AsyncPrimes names are still available as aliases:
from zpdatafetch import AsyncCyclist, Cyclist
# These are the same class
assert AsyncCyclist is Cyclist # True!
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)
Session Sharing (Performance Optimization)
When working with multiple data objects or making multiple API requests, you can share HTTP sessions to improve performance and reduce overhead. This prevents unnecessary login attempts (for ZwiftPower) and reuses HTTP connection pools.
ZwiftPower Session Sharing
Synchronous API:
from zpdatafetch import ZP, Cyclist, Result, Team, Primes, Sprints
# Create a single ZP session
zp = ZP()
# Share it across multiple objects
cyclist = Cyclist()
cyclist.set_zp_session(zp)
cyclist.fetch(1234567)
result = Result()
result.set_zp_session(zp)
result.fetch(3590800)
team = Team()
team.set_zp_session(zp)
team.fetch(456)
# Only logs in once to ZwiftPower.
Asynchronous API:
import anyio
from zpdatafetch import AsyncZP, Cyclist, Result, Team
async def main():
# Create a single async session
async with AsyncZP() as zp:
cyclist = Cyclist()
cyclist.set_session(zp)
result = Result()
result.set_session(zp)
team = Team()
team.set_session(zp)
# Fetch concurrently, sharing the same session
async with anyio.create_task_group() as tg:
tg.start_soon(cyclist.afetch, 1234567)
tg.start_soon(result.afetch, 3590800)
tg.start_soon(team.afetch, 456)
print(cyclist.json())
print(result.json())
print(team.json())
anyio.run(main)
Zwiftracing Session Sharing
Synchronous API:
from zrdatafetch import ZR_obj, ZRRider, ZRResult, ZRTeam
# Create a single ZR_obj instance
zr = ZR_obj()
# Share it across multiple objects
rider = ZRRider()
rider.set_zr_session(zr)
rider.fetch(zwift_id=12345)
result = ZRResult()
result.set_zr_session(zr)
result.fetch(race_id=3590800)
team = ZRTeam()
team.set_zr_session(zr)
team.fetch(team_id=456)
# All use the same HTTP connection pool
Batch Operations with Session Sharing:
from zrdatafetch import ZR_obj, ZRRider
# Share session for batch operation
zr = ZR_obj()
riders = ZRRider.fetch_batch(12345, 67890, 11111, zr=zr)
for zwift_id, rider in riders.items():
print(f"{rider.name}: {rider.current_rating}")
Asynchronous API:
import anyio
from zrdatafetch import AsyncZR_obj, ZRRider, ZRResult, ZRTeam
async def main():
# Create a single async session
async with AsyncZR_obj() as zr:
rider = ZRRider()
rider.set_session(zr)
result = ZRResult()
result.set_session(zr)
team = ZRTeam()
team.set_session(zr)
# Fetch concurrently, sharing the same session
async with anyio.create_task_group() as tg:
tg.start_soon(rider.afetch, 12345)
tg.start_soon(result.afetch, 3590800)
tg.start_soon(team.afetch, 456)
print(rider.json())
print(result.json())
print(team.json())
anyio.run(main)
Async Batch with Session Sharing:
import anyio
from zrdatafetch import AsyncZR_obj, ZRRider
async def main():
async with AsyncZR_obj() as zr:
# Share session for batch operation
riders = await ZRRider.afetch_batch(12345, 67890, 11111, zr=zr)
for zwift_id, rider in riders.items():
print(f"{rider.name}: {rider.current_rating}")
anyio.run(main)
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()
Zwift Data (zdata)
The zdata command-line tool provides access to Zwift's unofficial API for
rider profiles, social data, activities, and world information. Requires Zwift
account credentials.
Command-line usage
zdata config # Set up Zwift credentials
zdata profile 550564 # Fetch rider profile
zdata profile 550564 123456 # Fetch multiple profiles
zdata followers 550564 # Fetch followers and followees
zdata followers --followers-only 550564 # Followers only
zdata followers --followees-only 550564 # Followees only
zdata rideons 550564 12345678 # Fetch RideOns (rider_id activity_id)
zdata rideons --give 550564 12345678 # Give a RideOn
zdata activity 550564 # Fetch activity history
zdata activity --limit 50 550564 # Fetch 50 activities
zdata worlds # List active worlds
zdata ridersinworld 1 # Riders in world by ID
zdata ridersinworld watopia # Riders in world by name
zdata profile --raw 550564 # Raw JSON output
zdata -v profile 550564 # Verbose logging
Library usage
from zdatafetch import (
ZwiftProfile,
ZwiftFollowers,
ZwiftRideOns,
ZwiftActivity,
ZwiftWorlds,
ZwiftRidersInWorld,
)
# Fetch rider profile
profile = ZwiftProfile()
profile.fetch(550564)
print(profile.json())
# Fetch followers
followers = ZwiftFollowers()
followers.fetch(550564)
print(f"Followers: {followers.follower_count()}")
# Fetch activity history
activity = ZwiftActivity()
activity.fetch(550564, start=0, limit=20)
# List active worlds
worlds = ZwiftWorlds()
worlds.fetch()
# Give a RideOn
ZwiftRideOns.give_rideon(550564, 12345678)
Zwift Status Data (zsdata)
The zsdata command-line tool provides access to the public Zwift Status API
(powered by Statuspage.io). No authentication required.
Command-line usage
zsdata status # Overall status summary
zsdata status --components # Component list only
zsdata incidents # All incidents
zsdata incidents --unresolved # Unresolved incidents only
zsdata maintenance # All scheduled maintenance
zsdata maintenance --upcoming # Upcoming maintenance only
zsdata maintenance --active # Active maintenance only
zsdata status --raw # Raw JSON output
zsdata status --json # Parsed JSON output
zsdata -v status # Verbose logging
Library usage
from zsdatafetch import ZSSummaryFetch, ZSIncidentFetch, ZSMaintenanceFetch
# Get overall status
summary = ZSSummaryFetch().fetch()
print(summary.status.description) # "All Systems Operational"
for comp in summary.components:
print(f" {comp.name}: {comp.status}")
# Get unresolved incidents
incidents = ZSIncidentFetch().fetch(unresolved_only=True)
for incident in incidents:
print(f"{incident.name} ({incident.impact})")
# Get upcoming maintenance
maintenance = ZSMaintenanceFetch().fetch(upcoming=True)
for m in maintenance:
print(f"{m.name}: {m.scheduled_for} - {m.scheduled_until}")
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
zpdata cyclist <zwift_id>
# Team
zpdata team <team_id>
# Signup
zpdata signup <race_id>
# Race Result
zpdata result <race_id>
# Primes
zpdata primes <race_id>
# Sprints
zpdata sprints <race_id>
# Zwiftrace Result
zrdata result <race_id>
# Zwiftrace Rider Stats
zrdata rider <zwift_id>
# Zwiftrace Team Data
zrdata team <team_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 - Zwiftracing has a clean, documented API. Zwiftpower, not so much...
- Update the interface to allow alternate keyrings
- Open to suggestions...
Contributors
- Emilio Garcia - @byrd92: League endpoint for Zwiftpower
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-2.2.5.tar.gz.
File metadata
- Download URL: zpdatafetch-2.2.5.tar.gz
- Upload date:
- Size: 150.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be3964f57bae3a36ef077417dd41699feba9e55d9b7fa43a72b067494fed84fe
|
|
| MD5 |
b6a74675b0a192712446adcee8b08ac6
|
|
| BLAKE2b-256 |
93c952a716ab6a4c298d7ef16aeb4a874b5e09d455b2a4c4c913f4048a404651
|
Provenance
The following attestation bundles were made for zpdatafetch-2.2.5.tar.gz:
Publisher:
build_publish.yml on puckdoug/zpdatafetch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zpdatafetch-2.2.5.tar.gz -
Subject digest:
be3964f57bae3a36ef077417dd41699feba9e55d9b7fa43a72b067494fed84fe - Sigstore transparency entry: 1519754830
- Sigstore integration time:
-
Permalink:
puckdoug/zpdatafetch@be4ed8ea99450fe43348d3ee4ce60ba64e134ab0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/puckdoug
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build_publish.yml@be4ed8ea99450fe43348d3ee4ce60ba64e134ab0 -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file zpdatafetch-2.2.5-py3-none-any.whl.
File metadata
- Download URL: zpdatafetch-2.2.5-py3-none-any.whl
- Upload date:
- Size: 188.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07ef4498f82fb2a2bef98f9ca2ab7e038a2ebc9e4b4076e9590811c86355c01f
|
|
| MD5 |
148d6cf0569cefcbbbc225e52c2aa740
|
|
| BLAKE2b-256 |
649332ba3fe0bd1dba580f08a818e42ece8d6aae9e8b5f3ab61557d59bd6cb9a
|
Provenance
The following attestation bundles were made for zpdatafetch-2.2.5-py3-none-any.whl:
Publisher:
build_publish.yml on puckdoug/zpdatafetch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zpdatafetch-2.2.5-py3-none-any.whl -
Subject digest:
07ef4498f82fb2a2bef98f9ca2ab7e038a2ebc9e4b4076e9590811c86355c01f - Sigstore transparency entry: 1519754867
- Sigstore integration time:
-
Permalink:
puckdoug/zpdatafetch@be4ed8ea99450fe43348d3ee4ce60ba64e134ab0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/puckdoug
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build_publish.yml@be4ed8ea99450fe43348d3ee4ce60ba64e134ab0 -
Trigger Event:
workflow_run
-
Statement type: