Skip to main content

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):

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 exceeded
  • low — 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 unaccent support

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 to django-stravakit)
  • {% block logo %} — the header logo SVG

License

GNU General Public License v3

Download files

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

Source Distribution

django_stravakit-0.3.0.tar.gz (166.5 kB view details)

Uploaded Source

Built Distribution

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

django_stravakit-0.3.0-py3-none-any.whl (176.3 kB view details)

Uploaded Python 3

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

Hashes for django_stravakit-0.3.0.tar.gz
Algorithm Hash digest
SHA256 47c9723a95e66270c24c7699fb5b47c2cb28bf83a2f387764d1ebad30116e256
MD5 a5c165f7130b61020d717adf9bf852b7
BLAKE2b-256 f41dd0598f748354d0616a47184ac1516d997ae063e50e0561b048965a8637df

See more details on using hashes here.

File details

Details for the file django_stravakit-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_stravakit-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a717e251a15aeba9da62ca4ea2e95cfc8ee06c8edc7cece6c7010ab52f652233
MD5 0c0f58e5bd43ab8f3b6872d291a6319b
BLAKE2b-256 5b3a7ef02bee5ba88799997b1cc664111665eff1242f54277b640958a8f301c9

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 Sentry Error logging StatusPage Status page