Skip to main content

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.Client for 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) and offset validated before requests.
  • ParsedApiResponse: every terminal method returns parsed MRData with pagination, kind, and typed data (RaceTable, SeasonTable, …, or RawTable for unknown tables). Use ApiResponse only when you construct JSON yourself and call parse_api_response().
  • Typed domain models (basef1.domain): dataclass hierarchy rooted at JolpicaModel with to_dict(); nested Race graph (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. Pass cache=False to disable, or cache_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_maxlen bounds the in-memory request log.
  • Explicit errors via JolpicaHTTPError for 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
RaceResultDriver, Constructor, TimeElement, FastestLapAverageSpeed
QualifyingResultDriver, Constructor; LapRowLapTimingEntry; 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 a request_log tuple (RequestLogEntry: method, URL, query params, cache vs network).
  • stats() — prints a human-readable summary (including approximate cache hit rate and up to 10 recent log lines). Pass file= to redirect output (for example io.StringIO()).
  • request_log_maxlen — caps how many requests are retained (default 512); use None for 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_code and .body).
  • ValueError: invalid limit / offset, or round() used without a valid preceding season().

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

MIT

Project details


Download files

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

Source Distribution

basef1-0.2.0.tar.gz (23.8 kB view details)

Uploaded Source

Built Distribution

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

basef1-0.2.0-py3-none-any.whl (23.8 kB view details)

Uploaded Python 3

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

Hashes for basef1-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e7a8708299e54be149f2bcea69bf460df929845babf6e82a39b14653560590cb
MD5 5f66b23cab5a604d06e1b2b4c00c4551
BLAKE2b-256 1d27a388ab5d7e422b14ffd1e6dbd8b6d8130015948085b256607727a23f45bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for basef1-0.2.0.tar.gz:

Publisher: publish-pypi.yml on GoktugOcal/BaseF1

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for basef1-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0434aefcd29a8ff18a43be3286e2001c8c5237f670fce037dd4e58513297aa78
MD5 01d677481f4d4933cef9dd1a4de28316
BLAKE2b-256 83cf51fea6b13f060ecddb2cb000b86495c01ffc57f58b3cc156ca62edbb19ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for basef1-0.2.0-py3-none-any.whl:

Publisher: publish-pypi.yml on GoktugOcal/BaseF1

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page