Skip to main content

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 - --sync flag 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 ID
  • name: Rider's display name
  • current_rating: Current rating score
  • current_rank: Current category rank (A, B, C, D, etc.)
  • max30_rating: Best rating in last 30 days
  • max30_rank: Category for max30
  • max90_rating: Best rating in last 90 days
  • max90_rank: Category for max90
  • drs_rating: Derived rating score (max30 or max90)
  • drs_rank: Category for DRS
  • gender: 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 ID
  • team_name: Team name
  • riders: 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.

  1. Install this package
  2. Install the requirements
pip install -r requirements.txt

or

uv sync
  1. 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
  1. 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>
  1. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zpdatafetch-2.2.5.tar.gz (150.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zpdatafetch-2.2.5-py3-none-any.whl (188.0 kB view details)

Uploaded Python 3

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

Hashes for zpdatafetch-2.2.5.tar.gz
Algorithm Hash digest
SHA256 be3964f57bae3a36ef077417dd41699feba9e55d9b7fa43a72b067494fed84fe
MD5 b6a74675b0a192712446adcee8b08ac6
BLAKE2b-256 93c952a716ab6a4c298d7ef16aeb4a874b5e09d455b2a4c4c913f4048a404651

See more details on using hashes here.

Provenance

The following attestation bundles were made for zpdatafetch-2.2.5.tar.gz:

Publisher: build_publish.yml on puckdoug/zpdatafetch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for zpdatafetch-2.2.5-py3-none-any.whl
Algorithm Hash digest
SHA256 07ef4498f82fb2a2bef98f9ca2ab7e038a2ebc9e4b4076e9590811c86355c01f
MD5 148d6cf0569cefcbbbc225e52c2aa740
BLAKE2b-256 649332ba3fe0bd1dba580f08a818e42ece8d6aae9e8b5f3ab61557d59bd6cb9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for zpdatafetch-2.2.5-py3-none-any.whl:

Publisher: build_publish.yml on puckdoug/zpdatafetch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page