Metadata-Version: 2.4
Name: f1verse
Version: 0.9.0
Summary: The story layer for Formula 1 data — zero-dependency race narratives: lead changes, stints, race pace, live championship projection, team radio index.
Author: f1verse contributors
License: MIT
Project-URL: Homepage, https://github.com/jinsim/f1verse
Keywords: f1,formula1,formula-1,motorsport,racing,grand-prix,telemetry,live-timing,race-analysis,data-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# f1verse

**The story layer for Formula 1 data.** Data libraries fetch and tidy —
f1verse tells you *what happened*: lead changes, laps led, event timelines,
stint strategy, race pace, and a live championship projection.

**Zero dependencies.** Standard library only. Seasons 2023 onward;
historic records reach back to 1950.

```bash
pip install f1verse
```

```python
import f1verse

race = f1verse.load(2026, 12)        # year, round — no other library needed

race.laps_led()      # {'ANT': 32, 'NOR': 31, 'HAM': 9}
race.leader_runs()   # [{'abbr': 'NOR', 'from': 1, 'to': 4}, ...]
race.results()[7]    # {'abbr': 'HUL', 'gap': '+1 LAP', ...}
race.race_pace()     # median pace — pit/SC/VSC laps excluded by default
race.story()         # one call, whole story, plain JSON

race.championship_prediction()   # per-lap "if it ended now" title projection
race.team_radio()                # timestamped clip URLs (nothing downloaded)
```

### Beyond a single race

```python
f1verse.career("max_verstappen")
# {'starts': 245, 'wins': 71, 'podiums': 131, 'poles': 48, ...}  1950-present

f1verse.milestones("max_verstappen")
# [{'stat': 'poles', 'current': 48, 'target': 50, 'remaining': 2}]

f1verse.circuit_profile(2026, 13)
# corners, marshal sectors, track outline, and pit loss split by track state
# {'normal': 25.43, 'sc': 16.11, 'vsc': 18.4}  <- what an undercut costs here
# plus historic record: 75 races held, pole-to-win rate 0.30

f1verse.head_to_head(2026)
# teammate quali/race scores per constructor

f1verse.standings(2026)
```

### Running this on a schedule

```python
f1verse.status(2026)
# {'latest_race': {'round': 12, 'meeting': 'Dutch Grand Prix'},
#  'next': {'round': 13, 'session': 'Practice 1'}, 'next_in_hours': 125.7}

f1verse.due(2026, processed=[...session keys you already handled...])
# sessions finished, settled (45 min past the flag) and not yet processed —
# nothing published twice, nothing missed after downtime
```

Caching is policy-driven, not blanket: completed-session data is immutable
and cached forever, while schedules expire every few hours — a calendar
cached for a season would hide a cancelled round for the rest of the year.
`f1verse.cache_info()` and `f1verse.clear_cache(older_than=...)` are there
for operators.

### Telemetry, track position, conditions

```python
f1verse.lap_telemetry(race, "NOR", 40)
# per-sample speed, throttle, brake, gear, RPM and DRS state for one lap

f1verse.lap_trace(race, "NOR", 40)      # x/y/z coordinates of that lap
f1verse.top_speeds(race)                # fastest reading per driver

f1verse.weather_summary(race)
# {'track_c': {'min': 25.8, 'max': 38.2}, 'rain': True, 'samples': 191}
```

Telemetry is high-frequency, so these take a bounded window and filter
server-side rather than downloading a session and trimming it locally.

### Grounded narration

```python
facts = f1verse.race_facts(race)       # all numbers computed and formatted here
f1verse.brief(race)                    # deterministic text, no model required

result = f1verse.narrate(
    race,
    generate=lambda prompt: my_model(prompt),
    cache_dir=".cache/narration",
)
# {'text': '...', 'source': 'generated' | 'cache' | 'template', ...}
```

`narrate` accepts any text-generation callback; f1verse has no model SDK
dependency. Drafts are checked against the structured fact sheet. Unknown
numbers and driver codes are rejected, generation is retried at most twice,
and a deterministic summary is returned if verification still fails. The
optional cache is exact-match only and stores verified text.

### Predictions, pit-stop verdicts, official documents

```python
f1verse.win_probabilities({"NOR": 1, "ANT": 2, "RUS": 3},
                          year=2026, upto_round=12, circuit_id="monza")
# every probability ships with its own evidence:
#   grid base rate measured over 233 real races (pole wins 54.1%)
#   blended with that circuit's pole-to-win conversion (Monza: 0.30)
#   scaled by recent form (average finish over the last 5 rounds)

f1verse.pit_exchanges(race, pit_loss_s=22.74)
# [{'lap': 17, 'driver': 'RUS', 'rival': 'PIA', 'verdict': 'worked',
#   'gain_s': 3.54}, ...]
# neutralised laps (red flag / SC / VSC) and same-lap covering stops are
# excluded — calling those undercuts would be wrong

f1verse.fia_documents(2026)          # stewards' decisions, classified
f1verse.power_unit_documents(2026)   # "who changed which engine part"
```

## Why this exists

Raw timing data needs a lot of domain knowledge before it means anything:

- **Classified gaps are not comparable across lapped cars.** A car one lap
  down can show a smaller number than one that finished ahead on the lead
  lap. → `format_gap` applies the broadcast convention (`+1 LAP`).
- **"Who led the race" has to be derived.** Lead changes, laps led and the
  moments they happened are not published as such.
  → `leader_runs`, `laps_led`, `timeline`.
- **Race pace needs rules**, not just a threshold: in/out laps and laps run
  under SC/VSC have to go, or the number is meaningless.
  → `race_pace` applies them by default.
- **Web and video pipelines need plain JSON.** Every f1verse output is
  JSON-safe Python, ready to serialise.
- **Data quality should be checkable in code**, not read from logs.
  → `crosscheck` and `integrity_report` return structured verdicts.

## Additional live-timing feeds

The official live-timing archive publishes several feeds that are rarely
surfaced. f1verse parses three of them, with the same caching and
rate-limit etiquette as the rest of the library:

```python
f1verse.championship_prediction(session)
# per-lap "if the race ended now" projection of both championships,
# including the moments the projected champion changed

f1verse.team_radio(session)
# timestamped team-radio clips: [{'t', 'utc', 'driver_number', 'url'}]
# URLs only — nothing is downloaded or redistributed

f1verse.timing_stats(session)
# personal bests, best sectors, speed-trap figures
```

## Running continuously

See **[OPERATIONS.md](OPERATIONS.md)** for caching policy, rate limits and
scheduling.

## Tests

```bash
pip install -e ".[test]" && pytest -q
```

## Sources

| Layer | What it gives |
|---|---|
| Race data | laps, stints, pits, positions, results, overtakes |
| Live-timing archive | championship projection, team radio, timing stats |
| Historic records | careers, circuit records, standings — 1950 onward |
| Circuit geometry | track outline, corners, marshal sectors, pit loss |

All are public endpoints, read at runtime. See `src/f1verse/sources/` for
the exact hosts and `LICENSE` notes where attribution applies.

## Design rules

1. **Zero required dependencies.** The native loader reads public REST
   endpoints and the official live-timing archive directly, with its own
   on-disk cache and polite pacing.
2. **Everything returned is plain JSON-safe Python.**
3. **F1 domain rules are defaults, not options.**
4. **Cross-checked where possible** — lapped-car gaps, for instance, are
   computed by convention *and* confirmed against a second source.
5. **Code only.** No timing data, media, or images are bundled or
   redistributed; data is fetched by the end user.

## Roadmap

- Broader cross-validation coverage
- Additional session types (qualifying, sprint)
- Localisation packages


---

*Unofficial fan project. Not affiliated with, endorsed by, or associated
with Formula 1, FIA, FOM, or any F1 team. F1, FORMULA 1 and related marks
are trademarks of Formula One Licensing BV. This library contains code
only — no timing data, media, or images are included or redistributed;
data is fetched by the end user from publicly accessible endpoints,
subject to the respective providers' terms.*
