Python API wrapper for the Hevy API (sync + async, typed models, retries, pagination)
Project description
Hevy API Wrapper
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
- Installation
- Quick Start
- API Endpoints
- Configuration
- Error Handling
- Examples
- Testing
- Project Structure
- Contributing
- License
- Links
- Changelog
- Tips & Best Practices
โจ Features
- ๐ Sync & Async Support โ Use
Clientfor synchronous orAsyncClientfor 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
.envfiles 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.
- Visit https://hevy.com/settings?developer (requires Hevy Pro)
- Generate your API key
- Store it securely in a
.envfile:
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:
- workouts_example.py โ Comprehensive workout examples including list, create, update, get by ID, get events, and get count
- routines_example.py โ Routine and routine folder examples including list, create, update, and get by ID
- exercise_templates_example.py โ Exercise template examples including list, create custom exercise, and get by ID
- exercise_history_example.py โ Get exercise history for a specific template with optional date filtering
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:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests
- Run tests:
poetry run pytest - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - 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
- Hevy App: https://www.hevyapp.com/
- Hevy API Documentation: https://api.hevyapp.com/docs/
- Issues: GitHub Issues
๐งพ 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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ffdb25a1acc4cc56885550a63c2ec05e28d9e27267f2171ce9150525598c66f1
|
|
| MD5 |
0eb913bcd4ce9230e6f72a5507ec95fa
|
|
| BLAKE2b-256 |
238af3b5cba2cf5803a5c1377a19fc7920d7349e7eeaa1659dfe6d25f44bb3c1
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2aa96a80e00bc206e2090114eb58b3f303521d0376854fdfa4b1ccaa3a46b06a
|
|
| MD5 |
371601061b9cecd67c4709d4cb21b978
|
|
| BLAKE2b-256 |
79355147992df7c2378e0bd0b7ac08a95fd9c5889eaea92e5e9e4e2c5e944b85
|