Skip to main content

Python API wrapper for the Hevy API (sync + async, typed models, retries, pagination)

Project description

Hevy API Wrapper

Python Version License: MIT

A fully-typed, comprehensive Python client for the Hevy API with both synchronous and asynchronous support.

Built with modern Python best practices using httpx for HTTP transport and pydantic v2 for data validation and type safety.

โš ๏ธ Important: This is an unofficial community project. We are not affiliated with, endorsed by, or employed by Hevy. API access requires a Hevy Pro membership.


๐Ÿ“‹ Table of Contents


โœจ Features

  • ๐Ÿ”„ Sync & Async Support โ€“ Use Client for synchronous or AsyncClient for async/await patterns
  • ๐ŸŽฏ Fully Typed โ€“ Complete type hints and Pydantic models for all API resources
  • ๐Ÿ“ฆ All Endpoints Covered โ€“ Workouts, routines, exercise templates, routine folders, exercise history
  • ๐Ÿ” Smart Retries โ€“ Automatic exponential backoff for rate limits (429) and server errors (5xx)
  • ๐Ÿ“„ Pagination Helpers โ€“ Simple page/pageSize parameters with validation
  • ๐Ÿ›ก๏ธ Custom Exception Hierarchy โ€“ Structured error handling with request IDs
  • ๐Ÿ” Environment Variable Support โ€“ Use .env files via python-dotenv
  • โœ… 100% Test Coverage โ€“ Comprehensive test suite with mocked API responses

๐Ÿ“ฆ Installation

Using pip (No Poetry Required)

pip install hevy-api-wrapper

That's it! No need to install Poetry or any other build tools.

Using Poetry

If you're already using Poetry in your project:

poetry add hevy-api-wrapper

For Development

If you want to contribute or modify the code, you'll need Poetry:

# Install Poetry first (if not already installed)
pip install poetry

# Clone and setup
git clone https://github.com/dkuncik/hevy-api-wrapper.git
cd hevy-api-wrapper
poetry install

๐Ÿš€ Quick Start

Get Your API Key

Note: API access requires a Hevy Pro membership and is only available through the web application.

  1. Visit https://hevy.com/settings?developer (requires Hevy Pro)
  2. Generate your API key
  3. Store it securely in a .env file:
HEVY_API_TOKEN=your_api_key_here

Disclaimer: This is an unofficial, community-built wrapper. We are not affiliated with, endorsed by, or employed by the Hevy team.

Basic Usage (Sync)

from hevy_api_wrapper import Client

# Load from environment variable HEVY_API_TOKEN
client = Client.from_env()

# List your workouts
workouts = client.workouts.get_workouts(page=1, page_size=10)
for workout in workouts.workouts:
    print(f"{workout.title} - {workout.start_time}")

# Get total workout count
total = client.workouts.get_count()
print(f"Total workouts: {total}")

# Don't forget to close
client.close()

Using Context Manager (Recommended)

from hevy_api_wrapper import Client

with Client.from_env() as client:
    workouts = client.workouts.get_workouts(page=1, page_size=5)
    print(f"Found {len(workouts.workouts)} workouts")

Async Usage

import asyncio
from hevy_api_wrapper import AsyncClient


async def main():
    async with AsyncClient.from_env() as client:
        # List exercise templates
        templates = await client.exercise_templates.get_exercise_templates(
            page=1,
            page_size=25
        )

        for template in templates.exercise_templates:
            print(f"{template.title} ({template.type})")


asyncio.run(main())

๐Ÿ“š API Endpoints

Workouts

client = Client.from_env()

# List workouts with pagination
workouts = client.workouts.get_workouts(page=1, page_size=10)

# Get a single workout by ID
workout = client.workouts.get_workout("workout-id")

# Create a new workout
from hevy_api_wrapper.models import (
    PostWorkoutsRequestBody,
    PostWorkoutsRequestBodyWorkout,
    PostWorkoutsRequestExercise,
    PostWorkoutsRequestSet,
)

body = PostWorkoutsRequestBody(
    workout=PostWorkoutsRequestBodyWorkout(
        title="Morning Workout",
        description="Chest and triceps",
        start_time="2024-12-02T08:00:00Z",
        end_time="2024-12-02T09:30:00Z",
        routine_id=None,  # Optional: set to None or omit to create workout without a routine
        exercises=[
            PostWorkoutsRequestExercise(
                exercise_template_id="05293BCA",
                sets=[
                    PostWorkoutsRequestSet(
                        type="normal",
                        weight_kg=80,
                        reps=10,
                        rpe=8.5
                    )
                ]
            )
        ]
    )
)
created = client.workouts.create_workout(body)

# Update an existing workout
updated = client.workouts.update_workout("workout-id", body)

# Get workout events (changes since a timestamp)
events = client.workouts.get_events(
    page=1,
    page_size=10,
    since="2024-01-01T00:00:00Z"  # Defaults to epoch time if not provided
)

# Get total workout count
count = client.workouts.get_count()

Routines

# List routines
routines = client.routines.get_routines(page=1, page_size=10)

# Get a single routine
routine = client.routines.get_routine("routine-id")

# Create a routine
from hevy_api_wrapper.models import (
    PostRoutinesRequestBody,
    PostRoutinesRequestBodyRoutine,
    PostRoutinesRequestExercise,
    PostRoutinesRequestSet,
)

body = PostRoutinesRequestBody(
    routine=PostRoutinesRequestBodyRoutine(
        title="Push Day",
        folder_id=None,
        exercises=[
            PostRoutinesRequestExercise(
                exercise_template_id="05293BCA",
                rest_seconds=90,
                sets=[
                    PostRoutinesRequestSet(
                        type="normal",
                        weight_kg=80,
                        reps=10
                    )
                ]
            )
        ]
    )
)
created = client.routines.create_routine(body)

# Update a routine
from hevy_api_wrapper.models import (
    PutRoutinesRequestBody,
    PutRoutinesRequestBodyRoutine,
)

update_body = PutRoutinesRequestBody(
    routine=PutRoutinesRequestBodyRoutine(
        title="Push Day (Updated)",
        exercises=[...]
    )
)
updated = client.routines.update_routine("routine-id", update_body)

Exercise Templates

# List all exercise templates (includes Hevy's library + your custom exercises)
templates = client.exercise_templates.get_exercise_templates(
    page=1,
    page_size=100  # Max 100 per page
)

# Get a single exercise template
template = client.exercise_templates.get_exercise_template("template-id")

# Create a custom exercise
from hevy_api_wrapper.models import (
    CreateCustomExerciseRequestBody,
    CreateCustomExercise,
    CustomExerciseType,
    MuscleGroup,
    EquipmentCategory,
)

body = CreateCustomExerciseRequestBody(
    exercise=CreateCustomExercise(
        title="My Custom Exercise",
        exercise_type=CustomExerciseType.weight_reps,
        equipment_category=EquipmentCategory.barbell,
        muscle_group=MuscleGroup.chest,
        other_muscles=[MuscleGroup.triceps]
    )
)
created = client.exercise_templates.create_custom_exercise(body)

Routine Folders

# List routine folders
folders = client.routine_folders.get_routine_folders(page=1, page_size=10)

# Get a single folder
folder = client.routine_folders.get_routine_folder(42)

# Create a new folder
from hevy_api_wrapper.models import (
    PostRoutineFolderRequestBody,
    PostRoutineFolder,
)

body = PostRoutineFolderRequestBody(
    routine_folder=PostRoutineFolder(title="My Programs")
)
created = client.routine_folders.create_routine_folder(body)

Exercise History

# Get exercise history for a specific exercise template
history = client.exercise_history.get_exercise_history(
    "exercise-template-id",
    start_date="2024-01-01T00:00:00Z",  # Optional
    end_date="2024-12-31T23:59:59Z"  # Optional
)

for entry in history.exercise_history:
    print(f"{entry.workout_title}: {entry.weight_kg}kg x {entry.reps}")

๐ŸŽฏ Configuration

Client Options

from hevy_api_wrapper import Client

client = Client(
    api_key="your-api-key",  # Or use from_env()
    base_url="https://api.hevyapp.com/",  # API base URL
    api_key_header="api-key",  # Header name for API key
    timeout=30.0,  # Request timeout in seconds
    max_retries=3,  # Max retry attempts for 429/5xx
    backoff_factor=0.5  # Exponential backoff multiplier
)

Environment Variables

Create a .env file in your project root:

HEVY_API_TOKEN=your_api_key_here

Then use Client.from_env() or AsyncClient.from_env() to automatically load it:

from hevy_api_wrapper import Client

# Looks for HEVY_API_TOKEN by default
client = Client.from_env()

# Or specify a custom variable name
client = Client.from_env(env_var="MY_HEVY_KEY")

๐Ÿ”ง Error Handling

The wrapper provides structured exception classes for different error scenarios:

from hevy_api_wrapper import Client
from hevy_api_wrapper.errors import (
    HevyApiError,  # Base exception
    AuthError,  # 401, 403
    NotFoundError,  # 404
    ValidationError,  # 400
    RateLimitError,  # 429
    ServerError,  # 5xx
)

try:
    workout = client.workouts.get_workout("invalid-id")
except NotFoundError as e:
    print(f"Workout not found: {e}")
    print(f"Status code: {e.status_code}")
    print(f"Request ID: {e.request_id}")
except AuthError as e:
    print(f"Authentication failed: {e}")
except HevyApiError as e:
    print(f"API error: {e}")

๐Ÿ“– Examples

Check out the examples/ directory for complete working examples:

Running Examples

# Set your API key
export HEVY_API_TOKEN=your_api_key_here  # Linux/macOS
$env:HEVY_API_TOKEN = "your_api_key_here"  # PowerShell

# Run an example
python examples/workouts_example.py

๐Ÿงช Testing

The project includes a comprehensive test suite with 100% endpoint coverage:

# Run all tests
poetry run pytest

# Run with coverage report
poetry run pytest --cov=hevy_api_wrapper --cov-report=html

# Run specific test file
poetry run pytest tests/test_endpoints_sync.py -v

Tests use respx to mock HTTP responses, ensuring fast and reliable testing without hitting the actual API.


๐Ÿ—๏ธ Project Structure

hevy-api-wrapper/
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ hevy_api_wrapper/
โ”‚       โ”œโ”€โ”€ __init__.py           # Main exports
โ”‚       โ”œโ”€โ”€ client.py             # Client & AsyncClient
โ”‚       โ”œโ”€โ”€ errors.py             # Exception hierarchy
โ”‚       โ”œโ”€โ”€ version.py            # Version info
โ”‚       โ”œโ”€โ”€ endpoints/            # API endpoint groups
โ”‚       โ”‚   โ”œโ”€โ”€ workouts.py
โ”‚       โ”‚   โ”œโ”€โ”€ routines.py
โ”‚       โ”‚   โ”œโ”€โ”€ exercise_templates.py
โ”‚       โ”‚   โ”œโ”€โ”€ routine_folders.py
โ”‚       โ”‚   โ””โ”€โ”€ exercise_history.py
โ”‚       โ””โ”€โ”€ models/               # Pydantic models (one per file)
โ”‚           โ”œโ”€โ”€ workout.py
โ”‚           โ”œโ”€โ”€ routine.py
โ”‚           โ”œโ”€โ”€ exercise_template.py
โ”‚           โ”œโ”€โ”€ paginated_workouts.py
โ”‚           โ””โ”€โ”€ ... (50+ model files)
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ test_endpoints_sync.py   # Sync endpoint tests
โ”‚   โ””โ”€โ”€ test_endpoints_async.py  # Async endpoint tests
โ”œโ”€โ”€ examples/                     # Working example scripts
โ”œโ”€โ”€ pyproject.toml               # Poetry configuration
โ””โ”€โ”€ README.md                    # This file

๐Ÿค Contributing

Contributions are welcome! Here's how to get started:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make your changes and add tests
  4. Run tests: poetry run pytest
  5. Commit your changes: git commit -m 'Add amazing feature'
  6. Push to the branch: git push origin feature/amazing-feature
  7. Open a Pull Request

Development Setup

# Clone your fork
git clone https://github.com/dkuncik/hevy-api-wrapper.git
cd hevy-api-wrapper

# Install dependencies
poetry install

# Run tests
poetry run pytest -v

# Run tests with coverage
poetry run pytest --cov=hevy_api_wrapper

๐Ÿ“ License

This project is licensed under the MIT License - see the LICENSE file for details.


๐Ÿ”— Links


๐Ÿงพ Changelog

v1.0.0 โ€” First public stable release (2025-12-03)

This is the first public stable release of hevy-api-wrapper.

Highlights:

  • Sync and async clients (Client, AsyncClient)
  • Typed models for workouts, routines, exercise templates, routine folders, and exercise history
  • Robust pagination, retries, and structured error handling
  • Examples for common operations and test suite for endpoints
  • GitHub Actions for tests and code quality; pre-commit hooks for formatting

Install:

pip install hevy-api-wrapper==1.0.0

๐Ÿ’ก Tips & Best Practices

Use Context Managers

Always use context managers (with or async with) to ensure proper cleanup:

# โœ… Good
with Client.from_env() as client:
    workouts = client.workouts.get_workouts()

# โŒ Bad
client = Client.from_env()
workouts = client.workouts.get_workouts()
# Forgot to call client.close()!

Handle Rate Limits Gracefully

The client automatically retries on 429 (rate limit) errors, but you can catch them:

from hevy_api_wrapper.errors import RateLimitError

try:
    workouts = client.workouts.get_workouts()
except RateLimitError as e:
    print(f"Rate limited. Try again later. Request ID: {e.request_id}")

Pagination Best Practices

# Fetch all workouts across multiple pages
all_workouts = []
page = 1

while True:
    response = client.workouts.get_workouts(page=page, page_size=10)
    all_workouts.extend(response.workouts)

    if page >= response.page_count:
        break
    page += 1

print(f"Total workouts fetched: {len(all_workouts)}")

Type Hints for Better IDE Support

from hevy_api_wrapper import Client
from hevy_api_wrapper.models import Workout, PaginatedWorkouts

client: Client = Client.from_env()
workouts: PaginatedWorkouts = client.workouts.get_workouts()
workout: Workout = workouts.workouts[0]

Working with Workout Events

When using get_events(), the response contains lists of updated and deleted workouts:

from datetime import datetime, timedelta

# Get events from the last 7 days
since = (datetime.now() - timedelta(days=7)).isoformat() + "Z"
events = client.workouts.get_events(page=1, page_size=100, since=since)

# Process updated workouts
for workout in events.updated:
    print(f"Updated: {workout.title} at {workout.updated_at}")

# Process deleted workouts
for deleted in events.deleted:
    print(f"Deleted workout ID: {deleted.id} at {deleted.deleted_at}")

Understanding API Response Structures

Some API endpoints wrap responses in extra layers. The client automatically unwraps these:

# When you call create_routine(), the API returns:
# { "routine": { "id": "...", "title": "...", ... } }
#
# The client automatically extracts and returns just the routine object
routine = client.routines.create_routine(body)
print(routine.title)  # Direct access to routine properties

# Same for workouts in the update endpoint:
# API returns: { "workout": [{ "id": "...", ... }] }
# Client returns: Workout object
workout = client.workouts.update_workout(workout_id, body)

Made with โค๏ธ for the Hevy community

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

hevy_api_wrapper-1.0.0.tar.gz (23.6 kB view details)

Uploaded Source

Built Distribution

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

hevy_api_wrapper-1.0.0-py3-none-any.whl (39.9 kB view details)

Uploaded Python 3

File details

Details for the file hevy_api_wrapper-1.0.0.tar.gz.

File metadata

  • Download URL: hevy_api_wrapper-1.0.0.tar.gz
  • Upload date:
  • Size: 23.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.12.12 Linux/6.11.0-1018-azure

File hashes

Hashes for hevy_api_wrapper-1.0.0.tar.gz
Algorithm Hash digest
SHA256 ffdb25a1acc4cc56885550a63c2ec05e28d9e27267f2171ce9150525598c66f1
MD5 0eb913bcd4ce9230e6f72a5507ec95fa
BLAKE2b-256 238af3b5cba2cf5803a5c1377a19fc7920d7349e7eeaa1659dfe6d25f44bb3c1

See more details on using hashes here.

File details

Details for the file hevy_api_wrapper-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: hevy_api_wrapper-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 39.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.12.12 Linux/6.11.0-1018-azure

File hashes

Hashes for hevy_api_wrapper-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2aa96a80e00bc206e2090114eb58b3f303521d0376854fdfa4b1ccaa3a46b06a
MD5 371601061b9cecd67c4709d4cb21b978
BLAKE2b-256 79355147992df7c2378e0bd0b7ac08a95fd9c5889eaea92e5e9e4e2c5e944b85

See more details on using hashes here.

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