Metadata-Version: 2.4
Name: basef1
Version: 0.1.0
Summary: Python client for the Jolpica F1 API (Ergast-compatible JSON)
Project-URL: Homepage, https://github.com/jolpica/jolpica-f1
Project-URL: Documentation, https://github.com/jolpica/jolpica-f1/tree/main/docs
Author: Göktuğ Öcal
License: MIT
License-File: LICENSE
Keywords: api,basef1,ergast,f1,formula1,jolpica
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: shibuya; extra == 'docs'
Requires-Dist: sphinx>=7; extra == 'docs'
Description-Content-Type: text/markdown

# BaseF1

Python package **`basef1`** — a small, synchronous client for the **[Jolpica F1 API](https://github.com/jolpica/jolpica-f1)**. 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](https://github.com/jolpica/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](https://github.com/jolpica/jolpica-f1/blob/main/docs/rate_limits.md) 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](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](https://www.python-httpx.org/), 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.
- **`ApiResponse`**: raw JSON, `MRData` access, typed pagination strings, and **`get_data()`** to return the primary data table without hard-coding keys like `RaceTable` / `SeasonTable`.
- **Typed domain models** (`basef1.domain`): dataclass hierarchy rooted at **`JolpicaModel`** with **`to_dict()`** for JSON-shaped serialization; **`parse_api_response()`** / **`ApiResponse.parse()`** return a **`ParsedApiResponse`** with table-specific datas (`RaceTable`, nested `Race` → `Circuit` → `Location`, results, standings rows, etc.). Unknown `*Table` keys fall back to **`RawTable`**.
- **In-memory cache** (`TTLCache`): **enabled by default** (5-minute TTL, 256 entries) so identical GETs reuse responses and stay under rate limits. Pass **`cache=False`** to disable, or **`cache_ttl=`** / **`cache=`** to customize (see below).
- **Explicit errors** via `JolpicaHTTPError` for non-2xx responses and malformed JSON.
- **Runnable examples** under [`examples/`](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:

```bash
pip install -e .
```

For local development (tests and Ruff):

```bash
pip install -e ".[dev]"
```

---

## Quick start

```python
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](http://ergast.com/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 | None`, session slots (`Session`), `results: list[RaceResult]`, `qualifying_results`, `laps`, `pit_stops` |
| | | `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 | None` |
| `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:

```python
with BaseF1Client(cache=False) as client:
    ...
```

Customize TTL or inject your own cache (do not pass both `cache=` and `cache_ttl=`):

```python
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_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](https://github.com/jolpica/jolpica-f1/blob/main/docs/ergast_differences.md).

---

## Use case examples

These snippets assume `from basef1 import BaseF1Client` and `with BaseF1Client() as client:`.

**List every championship season (paginate with offset):**

```python
page = client.get_seasons(limit=100, offset=0)
seasons = page.get_data().get("Seasons") or []
```

**All races in a calendar year:**

```python
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:**

```python
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:**

```python
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):**

```python
resp = client.query().season(2024).driver("hamilton").get_results(limit=50)
```

**Qualifying and sprint entries for a season:**

```python
q = client.query().season(2024)
quali = q.get_qualifying(limit=100)
sprint = q.get_sprint(limit=100)
```

**Championship tables (season required):**

```python
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):**

```python
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:**

```python
catalogue = client.get_statuses(limit=100)
filtered = client.query().with_status(1).get_races(limit=50)
```

**“Current” season and “next” / “last” round keywords:**

```python
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):**

```python
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/`](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](examples/01_global_seasons.py) | Global seasons (`client.get_seasons()`) |
| [examples/02_global_lists.py](examples/02_global_lists.py) | Global races, drivers, constructors, circuits |
| [examples/03_season_races.py](examples/03_season_races.py) | Calendar for one season |
| [examples/04_season_round_results.py](examples/04_season_round_results.py) | Results for season + round |
| [examples/05_season_qualifying_and_sprint.py](examples/05_season_qualifying_and_sprint.py) | Qualifying and sprint |
| [examples/06_circuit_filter_races.py](examples/06_circuit_filter_races.py) | Circuit filter + races |
| [examples/07_constructor_filter_races.py](examples/07_constructor_filter_races.py) | Constructor filter + races |
| [examples/08_driver_season_results.py](examples/08_driver_season_results.py) | Season + driver + results |
| [examples/09_grid_filter_races.py](examples/09_grid_filter_races.py) | Grid slot filter + races |
| [examples/10_status_filter_races.py](examples/10_status_filter_races.py) | `with_status` + races |
| [examples/11_status_catalogue.py](examples/11_status_catalogue.py) | Status catalogue (`get_statuses()`) |
| [examples/12_standings.py](examples/12_standings.py) | Driver and constructor standings |
| [examples/13_laps_and_pitstops.py](examples/13_laps_and_pitstops.py) | Laps and pit stops |
| [examples/14_pagination_offset.py](examples/14_pagination_offset.py) | `limit` / `offset` |
| [examples/15_current_season_next_round.py](examples/15_current_season_next_round.py) | `current`, `next`, `last` |

```bash
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

```bash
pip install -e ".[dev]"
ruff check src tests examples
pytest
```

---

## Further reading

- [Jolpica F1 documentation](https://github.com/jolpica/jolpica-f1/tree/main/docs)
- [Rate limits](https://github.com/jolpica/jolpica-f1/blob/main/docs/rate_limits.md)
- [Differences vs Ergast](https://github.com/jolpica/jolpica-f1/blob/main/docs/ergast_differences.md)

---

## License

[MIT](LICENSE)
