Python client for the Jolpica F1 API (Ergast-compatible JSON)
Project description
BaseF1
Python package basef1 — a small, synchronous client for the Jolpica F1 API. The API serves Ergast-compatible JSON at https://api.jolpi.ca/ergast/f1/ so you can query historical and current Formula 1 data (seasons, races, results, drivers, constructors, standings, laps, pit stops, and more) without hand-crafting URLs for the common cases.
This repository is a third-party HTTP wrapper. It is not the Jolpica server itself; see the disclaimer below.
Disclaimer
- Not affiliated with the Jolpica F1 project, the former Ergast service, Formula 1, the FIA, or any team. This client is an independent convenience library.
- Data and availability come from
api.jolpi.ca. Accuracy, completeness, schema changes, downtime, and rate limits are controlled by the API operators, not by this package. Do not rely on this client for safety-critical or compliance-critical systems. - Trademarks such as “Formula 1” and related marks belong to their respective owners. This project uses publicly documented HTTP endpoints only.
- No warranty: the software is provided “as is” (see LICENSE). You are responsible for complying with the API’s terms of use and for respectful request volumes (caching, backoff, and spacing out bulk scripts).
Features
- Sync HTTP via httpx, with optional custom
httpx.Clientfor tests or proxies. - Chainable queries (
F1Query) that mirror Jolpica’s path-based filters: season, round, circuit, constructor, driver, grid slot, finishing status, then a terminal resource (races,results,driver_standings, etc.). - Pagination helpers:
limit(1–100) andoffsetvalidated before requests. ApiResponse: raw JSON,MRDataaccess, typed pagination strings, andget_data()to return the primary data table without hard-coding keys likeRaceTable/SeasonTable.- Typed domain models (
basef1.domain): dataclass hierarchy rooted atJolpicaModelwithto_dict()for JSON-shaped serialization;parse_api_response()/ApiResponse.parse()return aParsedApiResponsewith table-specific datas (RaceTable, nestedRace→Circuit→Location, results, standings rows, etc.). Unknown*Tablekeys fall back toRawTable. - In-memory cache (
TTLCache): enabled by default (5-minute TTL, 256 entries) so identical GETs reuse responses and stay under rate limits. Passcache=Falseto disable, orcache_ttl=/cache=to customize (see below). - Explicit errors via
JolpicaHTTPErrorfor non-2xx responses and malformed JSON. - Runnable examples under
examples/against the live API (use sparingly to avoid throttling).
Requirements
- Python 3.10+
- httpx (installed automatically with the package)
Installation
From a clone of this repository:
pip install -e .
For local development (tests and Ruff):
pip install -e ".[dev]"
Quick start
from basef1 import BaseF1Client
from basef1.domain.tables import SeasonTable
with BaseF1Client() as client:
seasons = client.get_seasons(limit=100)
table = seasons.get_data()
rows = table.get("Seasons") or []
print(seasons.pagination.total_int, len(rows))
parsed = seasons.parse()
assert isinstance(parsed.data, SeasonTable)
print(parsed.data.seasons[0].season)
Always prefer a context manager (with BaseF1Client() as client:) so the internal httpx.Client is closed when you are done. If you construct BaseF1Client() without with, call client.close() when finished (unless you passed your own httpx.Client and manage its lifecycle yourself).
Core concepts
Base URL and client
BaseF1Client defaults to https://api.jolpi.ca/ergast/f1. Override with base_url=... for mirrors or tests. timeout is forwarded to httpx when the library creates the client. HTTP responses are cached in memory by default (see Request caching); use cache=False when you need every request to hit the network (for example some tests).
query() and path filters
client.query() returns a fresh F1Query. Fluent methods append URL segments; order matters and must follow the Jolpica / Ergast MRD style paths (same as calling the REST API manually).
| Method | Path effect |
|---|---|
season(year) |
/{year}/ or current |
round(n) |
/{round}/ — must come immediately after season(...) (year or current) |
circuit(id) |
/circuits/{id}/ |
constructor(id) |
/constructors/{id}/ |
driver(id) |
/drivers/{id}/ |
grid(position) |
/grid/{position}/ |
with_status(id) |
/status/{id}/ (finishing status filter; avoids clashing with the get_statuses() resource) |
Terminal resources (one GET each)
Call these at the end of the chain (each returns an ApiResponse):
seasons, races, results, qualifying, sprint, drivers, constructors, circuits, driver_standings, constructor_standings, laps, pitstops, statuses.
Shorthand on the client: client.get_seasons() and client.get_statuses() are equivalent to client.query().get_seasons() and client.query().get_statuses().
Pagination
Every terminal accepts limit= and offset=. The API defaults to 30 rows per page; maximum limit is 100. Invalid values raise ValueError before any network I/O.
ApiResponse
| Member | Purpose |
|---|---|
.raw |
Full parsed JSON dict |
.mrdata |
The MRData object (meta + one *Table data) |
.pagination |
Pagination with limit, offset, total (strings from API) and total_int where parseable |
.get_data() |
Returns the primary data dict (e.g. race table body) using MRDATA_TABLE_PRIORITY and a *Table fallback; {} if none |
.parse() |
Builds a ParsedApiResponse: kind, pagination, and a typed data (or RawTable if the table is unknown) |
Exported constants for advanced use: MRDATA_METADATA_KEYS, MRDATA_TABLE_KEYS, MRDATA_TABLE_PRIORITY.
Typed models and serialization
Use parse_api_response(resp) or resp.parse() after any terminal query. Inspect envelope.kind, then narrow envelope.data (for example RaceTable, SeasonTable, StandingsTable). Nested objects (Driver, Circuit, RaceResult, …) all subclass JolpicaModel and implement to_dict() (omit None fields; JSON keys use Jolpica camelCase via dataclass field metadata where needed).
Typed models mirror documented fields; new API properties may require library updates.
Model taxonomy (MRData table to Python types)
MRData *Table key |
data class | Main row / nested types |
|---|---|---|
RaceTable |
RaceTable |
races: list[Race]; each Race has `circuit: Circuit |
RaceResult → Driver, Constructor, TimeElement, FastestLap → AverageSpeed |
||
QualifyingResult → Driver, Constructor; LapRow → LapTimingEntry; PitStopRow |
||
SeasonTable |
SeasonTable |
seasons: list[Season] |
DriverTable |
DriverTable |
drivers: list[Driver] |
ConstructorTable |
ConstructorTable |
constructors: list[Constructor] |
CircuitTable |
CircuitTable |
circuits: list[Circuit]; each Circuit has `location: Location |
StatusTable |
StatusTable |
statuses: list[StatusRow] |
StandingsTable |
StandingsTable |
standings_lists: list[StandingsList] → driver_standings / constructor_standings rows with nested Driver / Constructor |
Import concrete types from basef1.domain or the package root: from basef1 import Race, Circuit, Location.
Request caching
By default the client keeps a TTLCache (300 second TTL via DEFAULT_CACHE_TTL_SECONDS, 256 entries, LRU eviction). Duplicate same URL and query parameters within the TTL window only performs one HTTP GET.
Disable caching entirely:
with BaseF1Client(cache=False) as client:
...
Customize TTL or inject your own cache (do not pass both cache= and cache_ttl=):
from basef1 import BaseF1Client
with BaseF1Client(cache_ttl=600.0, cache_max_entries=512) as client:
client.get_seasons(limit=30, offset=0)
client.get_seasons(limit=30, offset=0) # cache hit if within TTL
from basef1 import TTLCache
cache = TTLCache(max_entries=64, ttl_seconds=120.0)
with BaseF1Client(cache=cache) as client:
...
Caching stores a deep copy of JSON datas; mutating response.raw does not corrupt the cache entry.
Errors
JolpicaHTTPError: HTTP status ≥ 400, or invalid JSON body, or transport failures (see.status_codeand.body).ValueError: invalidlimit/offset, orround()used without a valid precedingseason().
Standings and Jolpica-specific rules
Driver and constructor standings require a season in the path for Jolpica. Other nuances (e.g. differences vs legacy Ergast) are documented upstream: Ergast differences.
Use case examples
These snippets assume from basef1 import BaseF1Client and with BaseF1Client() as client:.
List every championship season (paginate with offset):
page = client.get_seasons(limit=100, offset=0)
seasons = page.get_data().get("Seasons") or []
All races in a calendar year:
races = client.query().season(2024).get_races(limit=100)
names = [r.get("raceName") for r in races.get_data().get("Races") or [] if isinstance(r, dict)]
Full result sheet for one round:
resp = client.query().season(2024).round(1).get_results(limit=100)
race = (resp.get_data().get("Races") or [None])[0]
results = race.get("Results") if isinstance(race, dict) else None
Races at a circuit, or for a constructor:
monza = client.query().circuit("monza").get_races(limit=50)
ferrari = client.query().constructor("ferrari").get_races(limit=50)
One driver’s results for a season (season before driver in the chain):
resp = client.query().season(2024).driver("hamilton").get_results(limit=50)
Qualifying and sprint entries for a season:
q = client.query().season(2024)
quali = q.get_qualifying(limit=100)
sprint = q.get_sprint(limit=100)
Championship tables (season required):
q = client.query().season(2024)
drivers = q.get_driver_standings(limit=100)
teams = q.get_constructor_standings(limit=100)
Lap timing and pit stops (season + round required):
q = client.query().season(2024).round(1)
laps = q.get_laps(limit=100)
stops = q.get_pitstops(limit=100)
Finishing status catalogue vs filtering races by status id:
catalogue = client.get_statuses(limit=100)
filtered = client.query().with_status(1).get_races(limit=50)
“Current” season and “next” / “last” round keywords:
next_race = client.query().season("current").round("next").get_races(limit=1)
last_results = client.query().season("current").round("last").get_results(limit=20)
Custom httpx.Client (testing, retries, proxies):
import httpx
from basef1 import BaseF1Client
with httpx.Client(timeout=60.0) as http:
client = BaseF1Client(client=http, cache=False) # optional: disable cache in tests
client.query().get_circuits(limit=5)
Runnable scripts
The examples/ directory contains small programs that hit the live API. Run them from the repository root after pip install -e .. Do not hammer all scripts in a tight loop or you may receive HTTP 429; add delays or run selectively.
| Script | Use case |
|---|---|
| examples/01_global_seasons.py | Global seasons (client.get_seasons()) |
| examples/02_global_lists.py | Global races, drivers, constructors, circuits |
| examples/03_season_races.py | Calendar for one season |
| examples/04_season_round_results.py | Results for season + round |
| examples/05_season_qualifying_and_sprint.py | Qualifying and sprint |
| examples/06_circuit_filter_races.py | Circuit filter + races |
| examples/07_constructor_filter_races.py | Constructor filter + races |
| examples/08_driver_season_results.py | Season + driver + results |
| examples/09_grid_filter_races.py | Grid slot filter + races |
| examples/10_status_filter_races.py | with_status + races |
| examples/11_status_catalogue.py | Status catalogue (get_statuses()) |
| examples/12_standings.py | Driver and constructor standings |
| examples/13_laps_and_pitstops.py | Laps and pit stops |
| examples/14_pagination_offset.py | limit / offset |
| examples/15_current_season_next_round.py | current, next, last |
python examples/01_global_seasons.py
Scripts that unpack rows use get_data() so they do not depend on hard-coded RaceTable / SeasonTable keys.
Development
pip install -e ".[dev]"
ruff check src tests examples
pytest
Further reading
License
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 basef1-0.1.0.tar.gz.
File metadata
- Download URL: basef1-0.1.0.tar.gz
- Upload date:
- Size: 20.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19da3963247e2cf7ddad27261a4ced6b71506213cc3c67e810dfe1020a1a3983
|
|
| MD5 |
aafca882047eeff0844c8b1912b942f6
|
|
| BLAKE2b-256 |
f232bec0e5a4d5e0b72d8a283d33ceb53a829014fb9a689982b31aa3cdd66657
|
Provenance
The following attestation bundles were made for basef1-0.1.0.tar.gz:
Publisher:
publish-pypi.yml on GoktugOcal/BaseF1
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
basef1-0.1.0.tar.gz -
Subject digest:
19da3963247e2cf7ddad27261a4ced6b71506213cc3c67e810dfe1020a1a3983 - Sigstore transparency entry: 1520920924
- Sigstore integration time:
-
Permalink:
GoktugOcal/BaseF1@8b74047c32445027a74f74ed0e46273dc04f7135 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GoktugOcal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@8b74047c32445027a74f74ed0e46273dc04f7135 -
Trigger Event:
release
-
Statement type:
File details
Details for the file basef1-0.1.0-py3-none-any.whl.
File metadata
- Download URL: basef1-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1495ec72e7a7a12f7e9c4ad2083c2ce4f78478cd83c0d7ff7582d6a5fea7d51
|
|
| MD5 |
6c1bf78d6dc81be86a04a7ec98a5e8e6
|
|
| BLAKE2b-256 |
b3acbd327a606c4b8a422dc785663b2cf96a374dda05954841f024c96ad8f4a1
|
Provenance
The following attestation bundles were made for basef1-0.1.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on GoktugOcal/BaseF1
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
basef1-0.1.0-py3-none-any.whl -
Subject digest:
b1495ec72e7a7a12f7e9c4ad2083c2ce4f78478cd83c0d7ff7582d6a5fea7d51 - Sigstore transparency entry: 1520921465
- Sigstore integration time:
-
Permalink:
GoktugOcal/BaseF1@8b74047c32445027a74f74ed0e46273dc04f7135 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GoktugOcal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@8b74047c32445027a74f74ed0e46273dc04f7135 -
Trigger Event:
release
-
Statement type: