No description
  • Python 90.2%
  • HTML 6.6%
  • CSS 2.5%
  • Shell 0.4%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Max Katz-Christy ee10925159
All checks were successful
Vulnerability scan / vuln-scan (push) Successful in 8s
chore: add gitleaks config
2026-09-30 20:41:50 +02:00
.forgejo/workflows ci: check out from the public Forgejo URL in the vuln scan 2026-09-30 20:01:09 +02:00
.github/workflows ci: add osv-scanner vulnerability gate on GitHub and Forgejo Actions 2026-09-30 19:49:33 +02:00
scripts ci: add osv-scanner vulnerability gate on GitHub and Forgejo Actions 2026-09-30 19:49:33 +02:00
src/cape_flier feat(maps): split system maps into local and long-route maps by grid density 2026-09-30 02:11:07 +02:00
tests feat(maps): split system maps into local and long-route maps by grid density 2026-09-30 02:11:07 +02:00
.dockerignore feat: dagster pipeline publishing sites to the bucket 2026-09-28 18:29:36 +02:00
.gitignore feat: dev command, basemap styles, clickable system map and logo 2026-09-28 19:42:23 +02:00
.gitleaks.toml chore: add gitleaks config 2026-09-30 20:41:50 +02:00
.pre-commit-config.yaml feat: timetables, route and home pages, SVG maps and page size report 2026-09-28 18:23:27 +02:00
.python-version feat: scaffold package, config, build and serve CLI 2026-09-27 17:53:50 +02:00
CHANGELOG.md chore(release): bump version 0.10.1 -> 0.11.0 2026-09-30 02:21:12 +02:00
CLAUDE.md feat(maps): split system maps into local and long-route maps by grid density 2026-09-30 02:11:07 +02:00
Dockerfile feat: dagster pipeline publishing sites to the bucket 2026-09-28 18:29:36 +02:00
LICENSE.txt feat: scaffold package, config, build and serve CLI 2026-09-27 17:53:50 +02:00
package.json ci: add osv-scanner vulnerability gate on GitHub and Forgejo Actions 2026-09-30 19:49:33 +02:00
pnpm-lock.yaml chore(deps): update lockfile within range 2026-09-30 19:27:42 +02:00
pnpm-workspace.yaml feat: timetables, route and home pages, SVG maps and page size report 2026-09-28 18:23:27 +02:00
pyproject.toml chore(release): bump version 0.10.1 -> 0.11.0 2026-09-30 02:21:12 +02:00
README.md feat: build whole catalog feeds, group routes and maps by mode, show feed status 2026-09-29 02:27:02 +02:00
sites.yaml feat(root): list every file with its source, date range and status; keep expired routes 2026-09-29 11:49:59 +02:00
uv.lock chore(release): bump version 0.10.1 -> 0.11.0 2026-09-30 02:21:12 +02:00

Cape Flier

Static timetable websites generated from GTFS, served at sites.gtfs.zone. One site per agency, listed in sites.yaml or taken from the gtfs.zone feed catalog: an index of routes, printed-style timetables per route, direction and service day, and a simple route map. Plain HTML and CSS, no JavaScript needed to read a timetable.

Running Locally

uv sync
uv run cape-flier dev                                   # build the dev: feeds in sites.yaml and the home page, serve, rebuild on changes
uv run cape-flier dev --site columbia-county            # the same, one site only (faster rebuilds)
uv run cape-flier dev --refresh                         # download the feeds again instead of using .cache/feeds/
uv run cape-flier build                                 # clear dist/, build every site and the home page
uv run cape-flier build --listed                        # only the sites listed in sites.yaml, not the catalog's
uv run cape-flier build --dev                           # only the dev: feeds in sites.yaml
uv run cape-flier build --country US --limit 50 --workers 4 --cache .cache/feeds   # a sample of catalog sites
uv run cape-flier build --site columbia-county         # rebuild dist/columbia-county/ and the home page
uv run cape-flier build --site columbia-county --zip feed.zip   # use a local zip
uv run cape-flier serve                                 # serve dist/ on the LAN, port 8000
uv run cape-flier sizes                                 # largest built pages, gzip and raw

build resolves a site's feed: id to its download URL through data.gtfs.zone/feeds.json at build time, or uses the site's url: directly. With --cache, feeds.json and the slugs assigned to catalog feeds are kept there too.

Configuration

sites.yaml holds defaults, an optional catalog filter and a list of sites.

catalog makes a site of every feed in data.gtfs.zone/feeds.json whose schedule is up and that passes countries (ISO codes, empty for all), max_bytes (zip size) and exclude (feed ids). Its slug comes from the feed name and is pinned in the bucket's slugs.json, so it never changes or goes to another feed.

Each listed site needs exactly one of feed or url, a slug when it has a url (optional for a feed), and may override any default. A listed feed is built whatever the catalog filter says:

Option Values Default
map svg, png, none svg
horizon_days days used to derive day types 28
time_format 12h, 24h 12h
timepoints auto, all, timepoint-flag auto
basemap none, a Stadia style, or {light: <style>, dark: <style>} to follow the color scheme (tiles under the svg map) none

Stadia styles: stadia-alidade-smooth, stadia-alidade-smooth-dark, stadia-alidade-bright, stadia-alidade-satellite, stadia-outdoors, stadia-osm-bright, stadia-toner, stadia-toner-lite, stadia-toner-dark, stadia-toner-blacklite, stadia-toner-background, stadia-terrain, stadia-terrain-background, stadia-watercolor.

title names the site, and routes filters routes by route_types, route_ids, exclude_route_types and exclude_route_ids. Unknown keys are an error.

Every site's footer credits the publisher (from feed_info.txt, else the first agency), links the GTFS download and the license, and links the feed's Transitland and Mobility Database pages. The license comes from feeds.json's licenses; license_url on a site sets or overrides it, and is the only source for a url: site.

Routes without a valid route_color get a color hashed from their route_id, the same one interlocking and coloring-book use.

Library

cape_flier.build.build_site(zip_bytes, site) returns {path: bytes} for one site and does no I/O, so it can run anywhere Python does.

Pipeline

cape_flier.pipeline.definitions is a Dagster code location with 16 partitions, each a shard of the sites by slug. A daily schedule at 11:00 UTC runs every shard. A run resolves the sites from sites.yaml and feeds.json, then for each of its shard's sites, in worker processes: download the feed, build it, upload the files whose hash changed to the sites.gtfs.zone bucket, delete ones no longer built and write <slug>/manifest.json. A site that fails keeps its previous pages. The run then writes _shards/<nn>.json and _content/<nn>.json, and rewrites the bucket root from every shard file (country index, a page per country, a sitemap index over one sitemap per shard, robots.txt, error.html), deleting sites no longer resolved unless the site count fell by more than 20%. A Gatus heartbeat is pushed once 95% of sites have been built that day, not counting feeds known to be unusable.

The content report _content/<nn>.json, listed in _content/index.json, holds per site the outcome of its last download (ok, not_zip, missing_files, parse_error, http_error, timeout, memory or error) and the day that outcome began. For an ok feed it also holds the zip's size and hash, feed_info, service range, agencies, counts and route types. geometry-car merges it into feeds.json. A feed that was not_zip, missing_files or parse_error is not downloaded again until its catalog size or Last-Modified, its URL or the cape-flier version changes, or a week passes.

uv sync --extra pipeline
S3_ENDPOINT=... S3_ACCESS_KEY=... S3_SECRET_KEY=... uv run dagster dev -m cape_flier.pipeline.definitions

Environment: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET (default sites.gtfs.zone), S3_REGION (default garage), GATUS_URL, GATUS_TOKEN, CAPE_FLIER_CONFIG (default sites.yaml), CAPE_FLIER_WORKERS (default 4), CAPE_FLIER_WORKER_MEMORY (address space per worker in bytes, default 2500000000).

License

AGPL-3.0-or-later. See LICENSE.txt.