Skip to content

Add standalone car-usage tool for iRacing series/class - #9

Open
klaus993 wants to merge 15 commits into
mainfrom
claude/awesome-curie-glc6bu
Open

Add standalone car-usage tool for iRacing series/class#9
klaus993 wants to merge 15 commits into
mainfrom
claude/awesome-curie-glc6bu

Conversation

@klaus993

Copy link
Copy Markdown
Owner

car_usage.py is a self-contained script (no package imports) that finds the
most-used car in an iRacing series and car class for a week, counting race
entries across all splits. Reuses the repo's OAuth password_limited login,
disk-caches subsession results, and throttles API calls.

Defaults target the IMSA series / IMSA23 (GT3) class. Includes verbose
logging (-v INFO, -vv DEBUG) for debugging, and a unit-tested pure tally
function (tests/test_car_usage.py) that runs without network or pytest.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01NBgnb2HVeEvCyATrYPU3od

claude and others added 15 commits June 17, 2026 03:51
car_usage.py is a self-contained script (no package imports) that finds the
most-used car in an iRacing series and car class for a week, counting race
entries across all splits. Reuses the repo's OAuth password_limited login,
disk-caches subsession results, and throttles API calls.

Defaults target the IMSA series / IMSA23 (GT3) class. Includes verbose
logging (-v INFO, -vv DEBUG) for debugging, and a unit-tested pure tally
function (tests/test_car_usage.py) that runs without network or pytest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBgnb2HVeEvCyATrYPU3od
…, API error logging

Fixes found via a live smoke run (auth + discovery confirmed working):

- Series selection: add --series-id and prefer an exact name match over a
  substring match, so "IMSA iRacing Series" (447) is no longer shadowed by
  "IMSA iRacing Series - Fixed" (539).
- Race-session selection: identify the race simsession by name instead of a
  numeric code (the old code reused the event_type 5, risking qualifying rows
  being counted).
- Car-class resolution: map car_class_id -> short name from the result's
  top-level car_classes array, falling back to per-row fields, so the IMSA23
  filter and the "classes seen" hint are reliable.
- API error/rate-limit logging: route all API calls through a helper that logs
  failures, detects HTTP 429, and retries with exponential backoff.

Tests cover all paths and run without pytest/network (10 passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBgnb2HVeEvCyATrYPU3od
Setting the root logger to DEBUG at -vv also enabled urllib3's per-request
logging, flooding output with members-ng/S3 URLs. Pin urllib3/iracingdataapi/
requests/botocore loggers to WARNING so verbose mode shows our logs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBgnb2HVeEvCyATrYPU3od
Omitting --class now produces a per-class breakdown (most-used car within each
car class, classes ordered by total entries) instead of defaulting to IMSA23.
Passing --class keeps the single-class ranking unchanged.

Adds count_cars_by_class() and format_all_classes(), reusing the existing
class-map / race-row / class-resolution helpers. Unknown classes bucket under
"(unknown)". Tests cover grouping, within-class ranking, unknown bucketing, and
class ordering (13 passing, no network/pytest needed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBgnb2HVeEvCyATrYPU3od
- Pass use_pydantic=True to irDataClient (with a TypeError fallback for older
  library versions) so it stops emitting the raw-dict DeprecationWarning;
  _to_dict() already normalizes pydantic models back to plain dicts.
- Resolve the track raced that week from the season schedule and include it in
  the stderr status line and the output headers (single-class and per-class).

Adds _track_for_week() with tests (16 passing, no network/pytest needed).
model_dump() returns real datetime objects (e.g. best_qual_lap_at), which broke
json caching with "Object of type datetime is not JSON serializable" and left
corrupt half-written cache files.

- _to_dict now uses model_dump(mode="json") so values are JSON-safe.
- fetch_result serializes to a string before opening the file (no partial writes
  on error) and uses default=str plus catches TypeError as a safety net.

Adds a _to_dict test asserting the json-mode path is used. 17 tests passing.
- New mutually-exclusive --csv / --json flags emit one row per car
  (series, season, week, track, class, rank, car, entries, share_pct) to
  stdout while logs/progress stay on stderr, so output pipes cleanly.
- Unify single-class and per-class paths in main() around one by_class map;
  add pure build_rows()/format_csv()/format_json() helpers.
- Document car_usage.py in the README (usage, flags, output columns, caching).

Adds export tests (row shape, class ordering, --top, CSV header, JSON
round-trip). 22 tests passing.
Derive the displayed/exported track from the actual race results, which is
ground truth for any season or week. Past-season queries (--year/--quarter)
previously showed the active season's scheduled track because series_seasons()
only returns current seasons; the schedule track is now only trusted when the
resolved season is the active one, and the real track is recovered from results.
Series names from the data API can differ from their UI titles in
punctuation/spacing, so a literal substring query (e.g. the non-Fixed
'Formula C - Dallara F3 Series') failed to match and fell through to the
'- Fixed' variant. resolve_series now compares normalized names (punctuation/
spacing/case-insensitive): a normalized exact match wins, otherwise the
shortest matching name is preferred so a base series beats its '- Fixed'
variant. Also adds --list-series [QUERY] to print id + name for all series
(optionally filtered) for easy discovery.
Where car_usage.py answers "which car is most used", car_meta answers
"which car is actually good" — combining usage share with win rate,
average finish, and raw pace (fastest qualifying lap, a robust p5 qual
lap, and gap to the fastest car). It reuses car_usage.py for auth,
caching, and series/season resolution.

Four scopes via --scope: all (whole field), top (split 1, the aliens),
mine (the split your --irating lands in), and tiers (the meta across
every iRating tier). Passing mine without --irating falls back to tiers.
--csv/--json export one row per car per scope with lap times in seconds.

Adds tests/test_car_meta.py (9 tests over the pure metric/selection
logic) and a README section. Full suite: 153 passed.
Env-var credentials frequently arrive wrapped in quotes or carrying
invisible edge characters (BOM, zero-width spaces, NBSP) from copy-paste
or .env loaders. These survive a print() unchanged so the value looks
correct, but iRacing's OAuth endpoint rejects the request with
invalid_request "invalid character at index 0". Add _clean_credential()
(strip whitespace + invisible chars, then one layer of matching quotes)
in both config.py and the self-contained car_usage.py, and apply it to
all four IRACING_* values.

Also surface the OAuth server's JSON error body on an HTTPError instead
of collapsing every failure into an identical "400 Bad Request".

Adds credential-normalization tests (tests/test_config.py and new cases
in tests/test_car_usage.py).
Standalone tool that pulls the full per-category driver list from
iRacing's driver_stats_by_category endpoint, caches it in a local SQLite
store (the dataset is large — hundreds of thousands of drivers per road
category — and the useful queries are indexed percentile/count lookups),
and answers "what top %% is N iRating?" offline after the first fetch.

Reuses the package OAuth client, so it shares the same IRACING_* env
vars. Supports --target, --update (--all / --categories), and --status.

Adds tests/test_irating_stats.py.
Dissects a single race the way car_meta dissects a week: stint structure,
pit stops and what they cost, inferred tyre calls, fuel bounds, the weather
timeline, penalties and position changes across the whole field.

Built for wet/mixed races where the result turns on when people stopped
rather than raw pace; degrades to "no transition detected" on a dry race.

Reuses car_usage for auth/retry/caching and car_meta for lap-time helpers,
and adds RefreshingClient because OAuth tokens expire after ~10 minutes and
a long fetch that reuses one client silently loses data partway through.

Explicit about what the API does and does not expose. Stints and pit timing
are measured. Fuel is reported as an upper bound (capacity x max fill /
opening-stint laps) since nobody can start above the cap. Tyre compound is
inferred from post-stop pace against the class median and carries a
confidence. Litres per stop is not derivable and is not claimed.

Three subtleties the tests pin down: iRacing flags both the in-lap and the
out-lap of a stop, so consecutive flagged laps are one stop; a pitted flag
on lap 0 is a pit-lane start, not a strategy stop; and the class median
must exclude pit laps, or a dry race pit window reads as rain arriving.
CLAUDE.md records the repo layout and conventions, plus the iRacing Data API
behaviours that produce silently wrong numbers rather than errors: 0-indexed
positions, -1 ratings on team entries, sampled member_chart_data, both in-lap
and out-lap carrying the pitted flag, ten-thousandths lap times, and the fact
that fuel capacity, tyre compound and litres per stop are not exposed at all.

Also captures the analysis pitfalls found while building race_report: pit laps
must be excluded from a class-median pace curve, fuel is a bound rather than an
estimate, and pit-phase time is not pit-service time.

Gitignore now covers the --csv/--json redirect targets used in the README
examples, which were showing up as untracked noise.
…cript

race_report could only bound fuel for six GT3s, so GTP and LMP2 entries showed
no bound at all. The table now covers every car_id in the IMSA week-5
car_restrictions payload: eleven GT3s, the Dallara P217, and the five LMDh
cars, which share one tank because GTP is a spec chassis.

fetch_fuel_capacities.py is how the table gets refreshed. Capacity is the one
input the Data API does not expose, so it comes from the iRacing wiki, read
through its MediaWiki api.php endpoint because the rendered pages return 402
to automated clients. Page titles are resolved by search first, since the
slugs do not match the in-sim names ("BMW M4 EVO GT3" vs "BMW M4 GT3 EVO",
"Mercedes-Benz AMG GT3 - 2020" vs "Mercedes-AMG GT3 2020"). Car ids come from
the API so the output is keyed the way race_report needs.

The Audi R8 LMS EVO II is deliberately left out: it appears in GT3 Regional
fields but its car_id was not in a car_restrictions payload I could verify,
and a wrong id silently attaches a capacity to the wrong car while the bound
still looks plausible. Two new tests guard the field coverage and the LMDh
tanks agreeing with each other.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants