swiss-netex
CLI for Swiss national NeTEx timetable packages (opentransportdata.swiss): download, inspect, and filter by operator/line.
This is not a full NeTEx model library. It prepares smaller operator-scoped packages from the national SKI NeTEx feed.
Requirements
- Ruby ≥ 4.0
Installation
gem install swiss-netexOr from source:
git clone https://github.com/gatemedia/swiss-netex.git
cd swiss-netex
mise run bootstrap # or: bundle install
bundle exec exe/swiss-netex versionQuick start
Download (or reuse cache) and filter one operator in a single step:
swiss-netex extract -y 2026 -O 151 -o tl.zipStdout prints the output path. Progress goes to stderr.
swiss-netex help
swiss-netex help extract # full flags for any command
swiss-netex versionCommands
download
Resolve and stream the latest national NeTEx zip. Resource downloads are public (no API token on the happy path). Prints the file path on stdout.
swiss-netex download
swiss-netex download --year 2026
swiss-netex download -y 2026 -o ./cache
swiss-netex download --dataset timetablenetex_2026 --force
swiss-netex download --cache-dir ./cache- Default dataset: highest published
timetablenetex_YYYYon the public catalog - Override with
--year YYYYor--dataset(slug or full page URL) - Default cache:
~/.cache/swiss-netex - Cache key is the remote filename (export timestamp embedded). A positive-size
local file with that name that also opens as a zip is a cache hit — OTD signed
URLs reject HEAD, so remote size is not re-checked. Use
--forceto refresh.
operators
List operators from the RESOURCE frame. Scans RESOURCE only — not TIMETABLE
shards — so listing stays cheap even on a full national package. Omit --from
to download (or reuse cache) first — same flags as download / extract.
swiss-netex operators -y 2026
swiss-netex operators --from ./cache/PROD_NETEX_….zip
swiss-netex operators -y 2026 | cut -f1,4Columns: go, operator_id, sboid, short_name, name.
lines
List lines from the SERVICE frame (SERVICE only — not TIMETABLE). Optional
--operator / --line narrow the list (same alias rules as filter). Omit
--from to download (or reuse cache) first.
swiss-netex lines -y 2026
swiss-netex lines -y 2026 -O 151
swiss-netex lines --from ./cache/PROD_NETEX_….zip -O 151 -l m1Columns: line_id, public_code, slnid, operator_ref, name.
filter
Filter a local package (zip or extracted directory) by operator and optional
line. Writes a smaller zip with transitive frame contents and a README.txt.
swiss-netex filter --from ./cache/PROD_NETEX_….zip -O 151 -o tl.zip
swiss-netex filter --from ./cache/PROD_NETEX_….zip \
-O 151 -O 55 -l 1 -o filtered.zip --verbose
swiss-netex filter --from ./package-dir/ -O 151 -o ./out/-
--operator/-Ois repeatable (union). Aliases: GO number,ch:1:Operator:*, SBOID, ShortName - Optional
--line/-l(repeatable): PublicCode, id, SLNID, ShortName, Name - Keeps matching SERVICE lines and TIMETABLE
ServiceJourneys, then the transitive closure in RESOURCE / SERVICE / SITE / SERVICECALENDAR / COMMON - Empty timetable shards are dropped (original national shard indices kept)
- Without
--allow-empty, exits non-zero when no lines or journeys match -
--verboseprints per-shard and per-frame keep/drop counters on stderr - Output zip includes
README.txt(filters, counts, Swiss-profile notes)
extract
Download (when --from is omitted) then filter. The common one-shot workflow.
swiss-netex extract -O 151 -o tl.zip
swiss-netex extract -y 2026 -O 151 -o tl.zip
swiss-netex extract --from ./cache/PROD_NETEX_….zip -O 151 -o tl.zipSame filter flags as filter, plus download flags when no --from is given.
operators and lines share the same package-source rules: omit --from to
download/cache; with --from, --dataset / --year / --force /
--cache-dir are rejected.
Output and conventions
| Stream | Content |
|---|---|
| stdout | Machine-usable result (file path, or TSV/table for discovery) |
| stderr | Progress, status, verbose counters, errors |
Discovery commands (operators, lines): raw TSV when piped / non-TTY;
aligned columns on an interactive terminal (same cells, two-space padding).
-o path rules (filter / extract):
-o value |
Result |
|---|---|
tl.zip |
as-is |
tl (no extension) |
tl.zip |
tl.tar |
as-is (explicit non-zip extension left alone) |
existing directory or trailing /
|
dir/<source>_op-….zip |
Exit codes: 0 ok, 1 runtime/domain error, 64 usage/parse error.
Memory and speed
National packages are large (~hundreds of MB compressed, tens of GB
uncompressed). filter / extract stream frames and never load the full
national set into a DOM:
- short-lived worker processes on platforms with
fork - parent byte pre-scan skips empty TIMETABLE shards
- process pool for matching shards and support frames
- non-DOM COMMON scanner
| Env | Effect |
|---|---|
SWISS_NETEX_TT_WORKERS=N |
Pool size (default: CPU count; 1 = serial) |
SWISS_NETEX_TT_BATCH |
Shards per child (default: 1) |
SWISS_NETEX_NO_FORK=1 |
Force in-process mode |
Prefer a machine with ≥4 GB free RAM. Peak system RSS scales roughly with worker count × largest kept shard.
Swiss profile notes (filtered packages)
These are normal for SKI national exports and operator-scoped subsets — not
filter bugs. The same points are written into each output README.txt.
Calendar / DayType
Journeys typically reference AvailabilityConditions that carry
FromDate / ToDate / ValidDayBits. Many selections collect zero
DayType / DayTypeRef entities. Importers should read validity from the
kept ACs in SERVICECALENDAR, not assume a DayType model.
COMMON connections
JourneyMeeting and InterchangeRule are kept only when every tracked
journey, stop, and line ref still resolves inside the package (no dangling
partner LineRef). Cross-operator interchanges drop unless you include both
operators (e.g. -O 151 -O 55). An empty connection section is valid.
TIMETABLE shard names
Only shards with kept journeys are written. Filenames keep the national
TIMETABLE_{i}_{n}_… indices, so numbering can have gaps (e.g. 45–69 plus
113). Consumers should glob timetable members, not assume 1…N contiguous.
Data license
The tool is MIT. Timetable data remains under OpenTransportData terms. Do not commit national dumps into this repository.
Development
This project uses mise for the local toolchain.
mise trust # once, if prompted
mise install # Ruby from mise.toml / .ruby-version
mise run bootstrap # bundle install
mise run test # bundle exec rake test
mise run lint # bundle exec rake rubocop
mise run ci # tests + rubocop
mise run swiss-netex help
mise run swiss-netex version
mise run build # gem package into pkg/Without mise:
bundle install
bundle exec rake # test + rubocop
bundle exec rake test
bundle exec rake rubocopRelease
-
Bump
SwissNetex::VERSIONand add aCHANGELOG.mdsection for it -
Publish from a clean tree:
VERSION=X.Y.Z bundle exec rake publishThat dates the CHANGELOG section, runs tests + RuboCop + gem build, commits the dated changelog if needed, tags
vX.Y.Z, and pushes the branch and tag. Do not use Bundler'srake release(that gem-pushes with an API key). -
GitHub Actions then tests the tag, creates a GitHub Release from that CHANGELOG section, and publishes to RubyGems via trusted publishing.
License
MIT © GateMedia — see LICENSE.