Generate motorsport race calendars in ICS format
Project description
motorsport-calendar
Generate motorsport race calendars in ICS format — subscribe once, stay updated automatically.
Overview
motorsport-calendar fetches race calendars from public APIs and exports them as .ics files
you can subscribe to in Google Calendar, Apple Calendar, Outlook, or any iCalendar-compatible app.
It ships as a CLI and as a native desktop app (Alpha — Beta packaging and positioning
already validated, see docs/AI_CONTEXT.md).
# Formula 1 — 2026 season
motocal generate-f1 2026 f1-2026.ics
# WEC — FIA World Endurance Championship
motocal generate-wec 2026 wec-2026.ics
# All enabled championships in one file
motocal generate 2026 motorsport-2026.ics
# Desktop app
motocal-gui
Import the .ics file into your calendar app — every race, qualifying session, and
practice appears automatically with the correct local time.
Features
- ICS export — one VEVENT per session, compatible with every major calendar app
- 16 working championships across single-seater, endurance, GT and motorcycle racing — see the table below
- Desktop app (Alpha) — Dashboard, upcoming weekend, personal calendar builder, search, favorites, and preferences, all built on Flet
- Extensible architecture — add a new series in a few files, zero changes elsewhere
- HTTP cache — disk-based JSON cache with configurable TTL; skip with
--refresh - YAML configuration — sources, cache path, alarm reminders, opt-out per championship
- CLI —
motocalpowered by Typer + Rich, with coloured output and progress feedback
Installation
CLI only (recommended)
pip install motorsport-calendar
CLI + Desktop app
pip install "motorsport-calendar[gui]"
With uv
uv tool install motorsport-calendar
# With the desktop app:
uv tool install "motorsport-calendar[gui]"
From source
git clone https://github.com/naviss29/motorsport-calendar.git
cd motorsport-calendar
uv sync --all-extras
Building a standalone desktop binary (no Python required on the target machine) is documented in
docs/PACKAGING.mdanddocs/RELEASE.md.
Running the CLI
After installation, the motocal command is available in your terminal:
motocal --help
Windows note: If
motocalis not found afterpip install, your PythonScriptsdirectory may not be inPATH. As a fallback, use:python -m motorsport_calendar --helpTo add the Scripts directory to PATH permanently, see Python on Windows FAQ.
Usage
One championship at a time
Every working championship has its own generate-<id> command:
motocal generate-f1 2026 f1-2026.ics
motocal generate-f2 2026 f2-2026.ics
motocal generate-f3 2026 f3-2026.ics
motocal generate-f1-academy 2025 f1a-2025.ics
motocal generate-formula-e 2026 formula-e-2026.ics
motocal generate-wec 2026 wec-2026.ics
motocal generate-elms 2026 elms-2026.ics
motocal generate-mlmc 2026 mlmc-2026.ics
motocal generate-gtwc-europe 2026 gtwc-europe-2026.ics
motocal generate-gtwc-america 2026 gtwc-america-2026.ics
motocal generate-gtwc-asia 2026 gtwc-asia-2026.ics
motocal generate-igtc 2026 igtc-2026.ics
motocal generate-motogp 2026 motogp-2026.ics
motocal generate-moto2 2026 moto2-2026.ics
motocal generate-moto3 2026 moto3-2026.ics
# Force re-download from the source instead of using the cache
motocal generate-f1 2026 f1-2026.ics --refresh
generate-imsaandgenerate-worldsbkalso exist (full provider architecture, wired into the CLI and config) but currently exit with "source non implémentée" — no public, structured schedule source was found for either championship. Seedocs/DATA_SOURCES.mdfor the investigation. They are hidden from the desktop app's selectors until a real source is found.
All enabled championships
# Fetches every enabled provider and merges them into one ICS file
motocal generate 2026 motorsport-2026.ics
If one provider fails (network error, unimplemented source…), the others continue. A per-provider summary is displayed:
Génération calendrier 2026 — 16 providers activés…
✓ formula1 : 24 événements
✓ formula2 : 14 événements
✓ wec : 8 événements
✗ imsa : source non implémentée
Export terminé : motorsport-2026.ics (…)
Other commands
# List all registered providers
motocal providers
# Show version
motocal version
Desktop app (Alpha)
A native desktop window is available as an optional extra, built with Flet:
pip install "motorsport-calendar[gui]"
motocal-gui
# or
python -m motorsport_calendar.gui
The app is organized around 8 pages:
- Tableau de bord — quick overview: next race, weekend at a glance, what's new
- Ce week-end — every session happening this weekend, across all championships
- Mon calendrier — pick a season and championships, generate an
.icsfile with a native save dialog - Recherche — find a championship, event, or circuit; results open the matching page directly
- Favoris — star championships to prioritize them across the app
- Préférences — startup update check, default season, export reminder delay; notifications and theme/language are prepared but not wired to a real backend yet
- À propos — project presentation, tech stack, links
- Soutenir le projet — how to support the project (donations, roadmap voting, feedback)
The desktop app uses exactly the same pipeline as the CLI — same providers, same cache, same
config.yaml, same ICS exporter. No business logic is duplicated.
Tableau de bord, Ce week-end, and Mon calendrier each have a refresh button (top-right of
the page) to bypass the HTTP cache and re-fetch immediately — the same effect as the CLI's
--refresh flag, without editing config.yaml or deleting the cache by hand.
A notification-computation engine (NotificationService) exists internally and is fully
tested, but isn't exposed in the app yet — Flet ships no OS notification service on any
platform, so the Préférences page doesn't offer a notifications toggle until a real native
backend exists; see docs/DECISIONS.md for the investigation.
To build a standalone desktop binary, see docs/PACKAGING.md.
Configuration
Create config.yaml in your working directory (or ~/.config/motorsport-calendar/config.yaml).
All keys are optional — sensible defaults apply when absent.
# Timezone for display (IANA format)
timezone: Europe/Paris
cache:
enabled: true
path: ~/.cache/motorsport-calendar
ttl_hours: 24 # re-download after 24 h
ics:
alarm_minutes: 30 # VALARM reminder before each session (0 = disabled)
providers:
formula1:
enabled: true
source: openf1 # openf1.org API — covers 2023 onwards
# source: jolpica # api.jolpi.ca (Ergast successor) — covers 1950 onwards
wec:
enabled: true
source: official # fiawec.com
# Disable a championship:
# imsa:
# enabled: false
Every other championship (f2, f3, f1-academy, formula-e, elms, mlmc,
gtwc-europe, gtwc-america, gtwc-asia, igtc, motogp, moto2, moto3…) can be
configured the same way — see motocal providers for the full, currently registered list.
Architecture
config.yaml
│
▼ registry.enabled()
┌──────────┬──────────┬──────────┐
│Formula 1 │ WEC │ MotoGP │ …more (auto-discovered)
│ Provider │ Provider │ Provider │
└────┬─────┴────┬─────┴────┬─────┘
│ │ │
OpenF1 fiawec.com Pulselive
Source Source Source
│ │ │
▼ ▼ ▼
Events Events Events
└──────┬──────┘
▼
IcsExporter
▼
calendar.ics
Each provider package auto-registers itself at import time via ProviderRegistry.
Each source auto-registers via SourceRegistry. The CLI calls registry.discover() once —
no hardcoded lists anywhere. The desktop app's controller (gui/controller.py) calls into
the exact same registry — no separate data path.
Key concepts
| Concept | Role |
|---|---|
Provider |
Fetches data from one source and maps it to list[Event] |
Source |
Encapsulates the HTTP/scraping logic for one data endpoint |
F1CalendarBaseSource |
Shared base for F2, F3, Academy, Formula E — one class to subclass |
AcoSportsEventSource |
Shared base for WEC, ELMS, MLMC (same ACO CMS, JSON-LD) |
PulseliveBase |
Shared base for MotoGP, Moto2, Moto3 (Dorna's official API) |
SroTimetableBase |
Shared base for GTWC Europe/America/Asia and IGTC (SRO's HTML tables) |
ProviderRegistry |
Auto-discovers and holds all registered provider factories |
SourceRegistry |
Auto-discovers and holds all registered source factories |
IcsExporter |
Serialises a list[Event] to an RFC 5545 .ics file |
HttpCache |
Disk-based JSON cache keyed by SHA-256(url + params) |
ConfigService |
Reads config.yaml and merges with Pydantic defaults |
Championships
| Championship | Status | Source | Availability |
|---|---|---|---|
| Formula 1 (recent) | ✅ Available | openf1.org | 2023 → present |
| Formula 1 (historical) | ✅ Available | jolpi.ca (Ergast successor) | 1950 → present |
| Formula 2 | ✅ Available | f1calendar dataset (MIT) | Recent seasons |
| Formula 3 | ✅ Available | f1calendar dataset (MIT) | 2022 → present |
| F1 Academy | ✅ Available | f1calendar dataset (MIT) | 2023 → present |
| Formula E | ✅ Available | f1calendar dataset (MIT) | Recent seasons |
| WEC — FIA World Endurance Championship | ✅ Available | fiawec.com (JSON-LD) | Recent seasons |
| ELMS — European Le Mans Series | ✅ Available | europeanlemansseries.com (JSON-LD) | Recent seasons |
| Michelin Le Mans Cup | ✅ Available | lemanscup.com (JSON-LD) | Recent seasons |
| GT World Challenge Europe | ✅ Available | gt-world-challenge-europe.com (HTML) | Recent seasons |
| GT World Challenge America | ✅ Available | gt-world-challenge-america.com (HTML) | Recent seasons |
| GT World Challenge Asia | ✅ Available | gt-world-challenge-asia.com (HTML) | Recent seasons |
| Intercontinental GT Challenge (IGTC) | ✅ Available | intercontinentalgtchallenge.com (HTML) | Recent seasons |
| MotoGP | ✅ Available | api.pulselive.motogp.com (official Dorna API) | Recent seasons |
| Moto2 | ✅ Available | api.pulselive.motogp.com (official Dorna API) | Recent seasons |
| Moto3 | ✅ Available | api.pulselive.motogp.com (official Dorna API) | Recent seasons |
| IMSA WeatherTech SportsCar Championship | 🔴 Architecture ready, no source found | — | Hidden from the desktop app |
| World Superbike (WorldSBK) | 🔴 Architecture ready, no source found | — | Hidden from the desktop app |
See docs/DATA_SOURCES.md for the full per-championship
investigation, including why IMSA and WorldSBK don't have a working source yet.
Roadmap
The project is currently Alpha, with Beta preparation work done — the desktop app,
all 16 working providers, and standalone Linux + Windows packaging are in place and
validated; see docs/ROADMAP.md for the detailed, per-sprint
breakdown, and docs/AI_CONTEXT.md for a running log of every
sprint.
| Milestone | Status |
|---|---|
| Multi-provider CLI, cache, config, ICS export | ✅ |
| 16 working championships across single-seater, endurance, GT and motorcycle racing | ✅ |
| Desktop app — Dashboard, weekend, calendar builder, search, favorites, preferences | ✅ Alpha |
| Standalone desktop packaging (no Python required) | ✅ Linux, ✅ Windows |
| PyPI release, stable public API | 🚧 |
Contribution
Run the tests
# All tests with coverage
uv run pytest
# A specific file
uv run pytest tests/test_cli_generate.py -v
# Packaging integrity
uv run pytest tests/test_packaging.py -v
# Lint + type check
uv run ruff check motorsport_calendar tests
uv run mypy motorsport_calendar
Add a support series sharing the f1calendar dataset (F2, F3, Academy, Formula E…)
Subclass F1CalendarBaseSource — only four overrides needed:
# providers/formula3/sources/f1calendar.py ← already implemented
from motorsport_calendar.providers.support_series.f1calendar_base import F1CalendarBaseSource
from motorsport_calendar.providers.formula3.source import Formula3Source
from motorsport_calendar.models import Championship, ChampionshipCategory, SessionType
_SESSION_MAP = {
"practice": (SessionType.FP1, 45, "Free Practice"),
"qualifying": (SessionType.QUALIFYING, 30, "Qualifying"),
"sprint": (SessionType.SPRINT, 30, "Sprint Race"),
"feature": (SessionType.RACE, 40, "Feature Race"),
}
_CIRCUIT_DATA = { ... } # slug → (country, IANA timezone)
class F1CalendarSource(F1CalendarBaseSource, Formula3Source):
@property
def _series_key(self) -> str: return "f3"
@property
def _session_map(self): return _SESSION_MAP
@property
def _circuit_data(self): return _CIRCUIT_DATA
def _make_championship(self, year: int) -> Championship:
return Championship(id=f"formula3-{year}", name="FIA Formula 3 Championship",
category=ChampionshipCategory.SINGLE_SEATER)
Then register in sources/__init__.py — one line. That's all.
The same pattern applies to the other shared bases: AcoSportsEventSource (WEC, ELMS,
MLMC), PulseliveBase (MotoGP, Moto2, Moto3), SroTimetableBase (GTWC *, IGTC).
Add any other provider
- Create
motorsport_calendar/providers/mychampionship/ - Add
source.py(abstractSourceclass) andprovider.py(concreteProvider) - Add
sources/mysource.pywith the HTTP/scraping implementation - Register in
__init__.pyandsources/__init__.py— two lines total
motocal generate picks it up automatically.
License
MIT © 2026 Alan Yvenou
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 motorsport_calendar-0.3.0.tar.gz.
File metadata
- Download URL: motorsport_calendar-0.3.0.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be99fa6204225a95afa3dc11b8d7cb6770636ceb4bc56dc6ffdad1688bdd6f92
|
|
| MD5 |
5da90ae70ae697729f7718731b8bbc16
|
|
| BLAKE2b-256 |
216a85be4c15fb56f67b991484d1dc879380bc4fde591d599c43b92dd79b3919
|
Provenance
The following attestation bundles were made for motorsport_calendar-0.3.0.tar.gz:
Publisher:
publish.yml on naviss29/motorsport-calendar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
motorsport_calendar-0.3.0.tar.gz -
Subject digest:
be99fa6204225a95afa3dc11b8d7cb6770636ceb4bc56dc6ffdad1688bdd6f92 - Sigstore transparency entry: 2277558071
- Sigstore integration time:
-
Permalink:
naviss29/motorsport-calendar@ac8bb61061a9ed6cf114ca3c4eb53b24e879eaa9 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/naviss29
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac8bb61061a9ed6cf114ca3c4eb53b24e879eaa9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file motorsport_calendar-0.3.0-py3-none-any.whl.
File metadata
- Download URL: motorsport_calendar-0.3.0-py3-none-any.whl
- Upload date:
- Size: 289.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b0c023b2eb97cb3fd01f51008469395940c0dc449e17ffafefe555374a1c825
|
|
| MD5 |
756a674490a919ebf0e003c14be1b1f5
|
|
| BLAKE2b-256 |
d00520a7ca0c1ed1d254b70b9c5a441dc5abbcebd41b9120ee87debfa8a77bee
|
Provenance
The following attestation bundles were made for motorsport_calendar-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on naviss29/motorsport-calendar
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
motorsport_calendar-0.3.0-py3-none-any.whl -
Subject digest:
6b0c023b2eb97cb3fd01f51008469395940c0dc449e17ffafefe555374a1c825 - Sigstore transparency entry: 2277558236
- Sigstore integration time:
-
Permalink:
naviss29/motorsport-calendar@ac8bb61061a9ed6cf114ca3c4eb53b24e879eaa9 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/naviss29
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac8bb61061a9ed6cf114ca3c4eb53b24e879eaa9 -
Trigger Event:
push
-
Statement type: