django-stravakit
Reusable Django app for Strava API integration. Provides models for Activities and Gear, a management command for importing data, and a rich admin interface powered by django-unfold.
Requirements
- Python 3.10+
- Django 5.1+
- PostgreSQL (uses
jsonb_extract_path_text,unaccent)
Python dependencies (installed automatically):
- stravalib — Strava API client (also provides the rate limiter)
- django-unfold — admin UI framework
- django-htmx — htmx integration for the frontend pages
Installation
pip install django-stravakit
The distribution is django-stravakit; the app it installs is imported as strava.
Add strava, django.contrib.humanize and django_htmx to INSTALLED_APPS, and the htmx middleware to MIDDLEWARE:
INSTALLED_APPS = [
# ...
"unfold",
"django.contrib.humanize",
"django_htmx",
"strava",
]
MIDDLEWARE = [
# ...
"django_htmx.middleware.HtmxMiddleware",
]
The activities page uses htmx (via
django-htmx) for server-side filtering, sorting and
stat calculation. The htmx runtime is served by {% htmx_script %} — no CDN required.
Wire up the URLs in your project's urls.py to expose the frontend pages:
from django.urls import include, path
urlpatterns = [
# ...
path("strava/", include("strava.urls", namespace="strava")),
]
Run migrations:
python manage.py migrate
Configuration
Add your Strava API credentials to your Django settings (the app reads them via
getattr(settings, ...)). Source them however you like — e.g. from environment variables:
# settings.py
STRAVA_CLIENT_ID = "..."
STRAVA_CLIENT_SECRET = "..."
STRAVA_ACCESS_TOKEN = "..."
STRAVA_REFRESH_TOKEN = "..."
STRAVA_TOKEN_EXPIRES = "..." # optional, format: 2024-01-01T00:00:00Z
Rate limiting
API calls respect Strava's rate limits.
Requests are proactively spaced out to stay within the limits, and any 429
(rate limit exceeded) response is retried after sleeping until the offending
limit window resets. Two optional settings tune this behaviour:
# settings.py
STRAVA_RATE_LIMIT_PRIORITY = "medium" # optional, one of: high, medium, low (default: medium)
STRAVA_RATE_LIMIT_MAX_RETRIES = 3 # optional, retries after a 429 (default: 3)
high— no proactive throttling (burst until a limit is hit)medium— spread requests so the short-term (15 min) limit is not exceededlow— spread requests so the daily limit is not exceeded
Usage
Import activities
Import activities from the Strava API:
python manage.py import_strava
The command fetches all activities newer than the latest one in the database. On first run, it imports all available activities.
Backfilling activities the incremental import missed
Because the cursor is "newer than the latest one stored", an activity that appears behind it — uploaded a day late, backdated by hand, or made public after the importer had already passed — is never picked up. Rescan a window to find those:
python manage.py import_strava --days 90 --missing # last 90 days, gaps only
python manage.py import_strava --days 90 --missing --dry-run # report, change nothing
python manage.py import_strava --after 2025-01-01 --before 2025-04-01
| Option | Meaning |
|---|---|
--days N |
Window of the last N days (counted back from --before, else now). |
--after DATE |
Explicit window start, YYYY-MM-DD. Not usable with --days. |
--before DATE |
Explicit window end. |
--missing |
Import only activities with no local row; skip the rest without spending a detail API call on them. |
--dry-run |
List what would be imported; fetch no details and write nothing. |
--missing is what makes a routine rescan cheap: a 90-day window is one list request plus
one detail request per genuinely missing activity, so a week where nothing was missed costs
2–3 requests. Without it the same window re-fetches every activity in range, which for 90
days sits right at Strava's 100-requests-per-15-minutes limit — use that only when you
actually want to refresh stored rows (names, kudos, gear links).
A windowed run deliberately leaves Athlete.synced_at alone: rescanning history is not the
same as being up to date, and that field is what the dashboard shows as "Last updated".
For a long historical backfill, walk it in chunks (--days 90 --before <date>) rather than
opening one enormous window, so a single run can't exhaust the daily quota.
Pages
The app ships a set of htmx-powered pages (registered under the strava URL namespace).
All filtering, sorting and stat recalculation happens server-side and is swapped in
without a full page reload.
- Dashboard (
strava:dashboard) — headline stats, "By the Numbers" totals, personal records (including "Furthest from Home"), run-performance breakdown, gear summary, the latest activity, and an activity map. The map controls (search + sport/gear/year filters) recompute every section live. - Activities (
strava:activities) — searchable, sortable list of activities with filtering by sport, gear and month, a summary band (distance, elevation, time, this week) and grid/table views. - Gear (
strava:gear) — gear cards showing usage, wear level and replacement alerts. - Gallery (
strava:gallery) — photo gallery of activities that have images.
Admin interface
The app registers Activity and Gear models in the Django admin with:
- Filtering by sport type, gear, distance range, and sync status
- Display of pace, speed, heartrate, elevation, and time
- Actions to import, fetch, and sync activities with the Strava API
- Full-text search with PostgreSQL
unaccentsupport
Models
Activity - Stores Strava activities with extracted fields (name, sport type, distance, start date, gear) and the raw API JSON response.
Gear - Stores gear details (brand, model, description). Automatically fetched from the API when first referenced by an activity.
Athlete - Stores the authenticated athlete's profile (name, avatar, city/country, follower and following counts). Populated by import_strava (and the dashboard refresh button) so the site chrome shows the real athlete instead of a hardcoded name. The frontend reads it via Athlete.current(); the app is single-athlete.
Activity and Gear carry a nullable athlete foreign key (on_delete=CASCADE) identifying their owner. It's set during import; rows imported before athlete linking existed are backfilled to the athlete on the next import.
Customising the site chrome
The nav name, avatar and follower/following counts are driven by the imported Athlete — nothing is hardcoded. The two branding elements in strava/pages/base.html are exposed as template blocks, so a consuming project can override them by extending the base template:
{% block brand %}— the name shown in the page<title>(defaults todjango-stravakit){% block logo %}— the header logo SVG
License
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 django_stravakit-0.3.0.tar.gz.
File metadata
- Download URL: django_stravakit-0.3.0.tar.gz
- Upload date:
- Size: 166.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47c9723a95e66270c24c7699fb5b47c2cb28bf83a2f387764d1ebad30116e256
|
|
| MD5 |
a5c165f7130b61020d717adf9bf852b7
|
|
| BLAKE2b-256 |
f41dd0598f748354d0616a47184ac1516d997ae063e50e0561b048965a8637df
|
File details
Details for the file django_stravakit-0.3.0-py3-none-any.whl.
File metadata
- Download URL: django_stravakit-0.3.0-py3-none-any.whl
- Upload date:
- Size: 176.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a717e251a15aeba9da62ca4ea2e95cfc8ee06c8edc7cece6c7010ab52f652233
|
|
| MD5 |
0c0f58e5bd43ab8f3b6872d291a6319b
|
|
| BLAKE2b-256 |
5b3a7ef02bee5ba88799997b1cc664111665eff1242f54277b640958a8f301c9
|