- Python 90.2%
- HTML 6.6%
- CSS 2.5%
- Shell 0.4%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Vulnerability scan / vuln-scan (push) Successful in 8s
|
||
| .forgejo/workflows | ||
| .github/workflows | ||
| scripts | ||
| src/cape_flier | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .pre-commit-config.yaml | ||
| .python-version | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| LICENSE.txt | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| pyproject.toml | ||
| README.md | ||
| sites.yaml | ||
| uv.lock | ||
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.