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.
What’s new in 0.2.0: usage counters and logging (usage_snapshot(), stats(), request_log_maxlen), richer Sphinx docs (docs/examples.rst, docs/caching.rst), and package version 0.2.0.
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. ParsedApiResponse: every terminal method returns parsed MRData withpagination,kind, and typeddata(RaceTable,SeasonTable, …, orRawTablefor unknown tables). UseApiResponseonly when you construct JSON yourself and callparse_api_response().- Typed domain models (
basef1.domain): dataclass hierarchy rooted atJolpicaModelwithto_dict(); nestedRacegraph (results, qualifying rows, laps, pit stops, sprint results, sessions). - Response cache: in-memory TTL by default (
TTLCache, 5-minute TTL, 256 entries) or persistent SQLite (cache_backend="sqlite"). Identical GETs reuse cached JSON and stay under rate limits. Passcache=Falseto disable, orcache_ttl=/cache=to customize (see below). - Usage observability:
usage_snapshot()for logical vs HTTP request counts and cache hits;stats()for a printable summary;request_log_maxlenbounds the in-memory request log. - 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:
envelope = client.get_seasons(limit=100)
assert isinstance(envelope.data, SeasonTable)
print(envelope.pagination.total_int, len(envelope.data.seasons))
print(envelope.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 a ParsedApiResponse):
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.
ParsedApiResponse (terminal methods)
| Member | Purpose |
|---|---|
.kind |
MRData table key (e.g. RaceTable) or None if missing |
.pagination |
Pagination with limit, offset, total (strings from API) and total_int where parseable |
.data |
Typed table model (RaceTable, SeasonTable, …) or RawTable when unknown |
ApiResponse (raw JSON wrapper)
If you already have a Jolpica-shaped dict, wrap it with ApiResponse(raw=payload) and call parse() or parse_api_response(...) to obtain a ParsedApiResponse. For normal client usage you rarely construct ApiResponse yourself.
| Member | Purpose |
|---|---|
.raw |
Full parsed JSON dict |
.mrdata |
The MRData object (meta + one *Table data) |
.pagination |
Same as above |
.get_data() |
Primary data table dict under MRData (priority + *Table fallback); {} if none |
.parse() |
Builds ParsedApiResponse |
Exported constants for advanced use: MRDATA_METADATA_KEYS, MRDATA_TABLE_KEYS, MRDATA_TABLE_PRIORITY.
Typed models and serialization
Terminal methods already return ParsedApiResponse — inspect envelope.kind and narrow envelope.data (for example RaceTable, SeasonTable, StandingsTable). Nested objects (Driver, Circuit, RaceResult, …) 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 an in-memory 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:
...
Persistent local cache (SQLite, default file ~/.cache/basef1/http_cache.sqlite):
with BaseF1Client(cache_backend="sqlite") as client:
...
with BaseF1Client(
cache_backend="sqlite",
cache_path="/tmp/f1_cache.sqlite",
cache_ttl=600.0,
) as client:
...
Customize in-memory 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 SQLiteCache, TTLCache
with BaseF1Client(cache=SQLiteCache(path="/tmp/f1.sqlite", ttl_seconds=300.0)) as client:
...
cache = TTLCache(max_entries=64, ttl_seconds=120.0)
with BaseF1Client(cache=cache) as client:
...
Caching stores a deep copy of JSON payloads inside the cache backend; your ParsedApiResponse models are separate parsed views.
Usage statistics
usage_snapshot()— returns logical vs HTTP GET counts, cache hits, whether caching is enabled, and arequest_logtuple (RequestLogEntry: method, URL, query params,cachevsnetwork).stats()— prints a human-readable summary (including approximate cache hit rate and up to 10 recent log lines). Passfile=to redirect output (for exampleio.StringIO()).request_log_maxlen— caps how many requests are retained (default512); useNonefor unlimited.
with BaseF1Client() as client:
client.get_seasons(limit=30, offset=0)
client.get_seasons(limit=30, offset=0)
client.stats()
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):
from basef1.domain.tables import SeasonTable
page = client.get_seasons(limit=100, offset=0)
assert isinstance(page.data, SeasonTable)
season_rows = page.data.seasons
All races in a calendar year:
from basef1.domain.tables import RaceTable
env = client.query().season(2024).get_races(limit=100)
assert isinstance(env.data, RaceTable)
names = [r.race_name for r in env.data.races]
Full result sheet for one round:
from basef1.domain.tables import RaceTable
env = client.query().season(2024).round(1).get_results(limit=100)
assert isinstance(env.data, RaceTable)
race = env.data.races[0] if env.data.races else None
results = race.results if race else []
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
Runnable scripts unpack ParsedApiResponse.data as typed tables (SeasonTable, RaceTable, …). See also Sphinx docs/examples.rst for more patterns.
Documentation (Sphinx)
Build HTML docs locally:
pip install -e ".[docs]"
sphinx-build -b html docs docs/_build/html
Open docs/_build/html/index.html in a browser. Narrative topics live under docs/ (installation, quick start, examples, caching, changelog).
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.2.0.tar.gz.
File metadata
- Download URL: basef1-0.2.0.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7a8708299e54be149f2bcea69bf460df929845babf6e82a39b14653560590cb
|
|
| MD5 |
5f66b23cab5a604d06e1b2b4c00c4551
|
|
| BLAKE2b-256 |
1d27a388ab5d7e422b14ffd1e6dbd8b6d8130015948085b256607727a23f45bd
|
Provenance
The following attestation bundles were made for basef1-0.2.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.2.0.tar.gz -
Subject digest:
e7a8708299e54be149f2bcea69bf460df929845babf6e82a39b14653560590cb - Sigstore transparency entry: 1551235021
- Sigstore integration time:
-
Permalink:
GoktugOcal/BaseF1@edf069c53f56baf779686fe91281d1b76722a818 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/GoktugOcal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@edf069c53f56baf779686fe91281d1b76722a818 -
Trigger Event:
release
-
Statement type:
File details
Details for the file basef1-0.2.0-py3-none-any.whl.
File metadata
- Download URL: basef1-0.2.0-py3-none-any.whl
- Upload date:
- Size: 23.8 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 |
0434aefcd29a8ff18a43be3286e2001c8c5237f670fce037dd4e58513297aa78
|
|
| MD5 |
01d677481f4d4933cef9dd1a4de28316
|
|
| BLAKE2b-256 |
83cf51fea6b13f060ecddb2cb000b86495c01ffc57f58b3cc156ca62edbb19ab
|
Provenance
The following attestation bundles were made for basef1-0.2.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.2.0-py3-none-any.whl -
Subject digest:
0434aefcd29a8ff18a43be3286e2001c8c5237f670fce037dd4e58513297aa78 - Sigstore transparency entry: 1551235134
- Sigstore integration time:
-
Permalink:
GoktugOcal/BaseF1@edf069c53f56baf779686fe91281d1b76722a818 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/GoktugOcal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@edf069c53f56baf779686fe91281d1b76722a818 -
Trigger Event:
release
-
Statement type: