A calendar of trail, road and ultra races worldwide, aggregated from public race calendars and served as a static site at races.sanch.pet. Races the site owner plans to run are highlighted.
races fetchruns one parser per source (src/races/sources/), normalises every race into one schema, geocodes towns offline against GeoNames, merges races listed by several sources and writesdata/events.json.races buildcopiessite/intodist/with one JSON file; the page filters it in the browser.- A daily GitHub Actions run fetches, commits the refreshed
data/events.jsontomainand force-pushes the built site as a single commit to thepublicbranch, which the web server pulls.
Which sources are parsed, which are only linked, and why: docs/sources.md.
Requires uv (mise install provides it).
uv sync
uv run races fetch # about a minute: DUV asks for 20 s between requests
uv run races build
python3 -m http.server -d dist 8000 # open http://localhost:8000races fetch --only utmb refreshes one source and keeps the others' last data. Raw responses are cached in .cache/raw/ for 20 hours, so re-running is cheap and polite.
Checks:
uv run ruff check . && uv run ruff format --check .
uv run pytest # recorded fixtures, no networkconfig/regions.yaml— the region groups in the filter (a label and a list of ISO country codes each). Edit and rebuild; no code changes needed.picks.yaml— the owner's picks: an eventid(shown under "Details" on each race) or any of its source URLs, with an optionalstatus(planned,registered,done) andnote. A race no source lists can be defined in the pick itself (name, date, country, url; see the file's header); it merges with a source's record once a parser finds the same race. A pick that matches no race failsraces build. Picks apply at build time, so an edit shows on the next push without a fetch. Friends' picks will go topicks/<name>.yamlin the same format.
Add src/races/sources/<name>.py with a class that has name, homepage, priority, fetch(client) -> list[str] and parse(pages, fetched_at) -> list[Event], register it in sources/__init__.py, record a trimmed response in tests/fixtures/ and test the parser on it. Fetch only through PoliteClient: it sends an identifying User-Agent, obeys robots.txt (including Crawl-delay), rate-limits per host and caches. Check the source's terms before writing the parser and record the verdict in docs/sources.md.
Race facts belong to their sources and organisers; every race links back to them. Code is MIT-licensed. Place coordinates derive from GeoNames (CC BY 4.0); map tiles © OpenStreetMap contributors.
