Add standalone car-usage tool for iRacing series/class - #9
Open
klaus993 wants to merge 15 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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