gtfs-digester
Canonicalization, fingerprinting, and change detection for GTFS Schedule feeds.
What It Does
Takes a GTFS zip and produces:
- A content fingerprint (BLAKE3 merkle hash) — identical for semantically identical feeds regardless of zip metadata, file ordering, CSV whitespace, or time formatting
- Canonical Arrow tables for each file — normalized, sorted by primary key, all values as strings
- Per-file hashes for efficient hierarchical change detection
- Archive diffs — added/removed/modified files and rows by primary key
- Exploded parquet storage — write/read from local or cloud (GCS, S3) via fsspec
All files and columns are preserved, including non-standard ones.
Install
pip install gtfs-digester
# or
uv add gtfs-digester
CLI
# Fingerprint a feed
gtfs-digester digest google_transit.zip
# Fingerprint: v1:abc123...
# Files: 11
# agency.txt 1 rows
# stops.txt 9245 rows
# ...
# JSON output or just the hash
gtfs-digester digest --json google_transit.zip
gtfs-digester digest --quiet google_transit.zip
# Diff two feeds (exit code 1 if different)
gtfs-digester diff old.zip new.zip
# Write as exploded parquet
gtfs-digester write google_transit.zip ./output --schedule-url https://example.com/gtfs.zip
# Produce a normalized GTFS zip
gtfs-digester normalize messy.zip clean.zip
Python API
from gtfs_digester import GTFSArchive
# Load and fingerprint
archive = GTFSArchive.from_zip("google_transit.zip")
print(archive.fingerprint.hex()) # v1:abc123...
# Access canonical tables
stops = archive.arrow_table("stops.txt")
print(stops.num_rows)
# Compare two feed versions
old = GTFSArchive.from_zip("old.zip")
new = GTFSArchive.from_zip("new.zip")
diff = old.diff(new)
print(diff.is_identical)
for f in diff.modified_files:
fd = diff.file_diff(f)
print(f"{f}: {fd.summary()}")
# Write as exploded parquet
from gtfs_digester import write_exploded
write_exploded(archive, "gs://bucket/schedules/feed-1", schedule_url="https://...")
How Fingerprinting Works
- Each
.txtfile is parsed to a PyArrow table (all strings) - Columns reordered per GTFS spec (unknown columns preserved, sorted alphabetically after spec columns)
- Values normalized: whitespace stripped, times zero-padded (
9:05:00→09:05:00) - Rows sorted by primary key (numeric columns sorted numerically, not lexicographically)
- Canonical CSV serialized and BLAKE3 hashed per file
- Archive fingerprint = BLAKE3 of sorted
filename:hashpairs (merkle tree)
Unknown files (not in the GTFS spec) are preserved with lexicographic row sorting.
Storage Layout
write_exploded() produces a version-first directory:
base_path/
_feed_digest={v1:abc...}/
agency.parquet
stops.parquet
routes.parquet
trips.parquet
stop_times.parquet
...
metadata.json # provenance + digester info, written last (commit marker)
DuckDB reads across versions with hive partitioning:
SELECT * FROM read_parquet('base_path/_feed_digest=*/stops.parquet', hive_partitioning=true);
Development
uv sync --group dev
uv run pytest tests/ -k "not Real" # unit tests (~0.5s)
uv run pytest tests/ # includes real feed integration tests
License
MIT
Metadata
Release files for gtfs-digester 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| gtfs_digester-0.4.0.tar.gz | 18.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gtfs_digester-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.8 kB
Release files / gtfs_digester-0.4.0.tar.gz
| Download URL | gtfs_digester-0.4.0.tar.gz |
|---|---|
| Size | 18.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1d0a83cb53a885a713781555cd508d3e4ffd7937a52241501d23e61e41cfbfa6
|
|
BLAKE2b-256 checksum How to use checksums |
dea5a992708e8362a16ef7c81bb6e53bf3083103cc240dc735ce20f138dfa290
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / gtfs_digester-0.4.0-py3-none-any.whl
| Download URL | gtfs_digester-0.4.0-py3-none-any.whl |
|---|---|
| Size | 22.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5c38a48a399e97ca7eb90efba344baa27fa4bcaccf43eae02f75f62a43e0cb3d
|
|
BLAKE2b-256 checksum How to use checksums |
c137cdf7b27afd0a0cb614ee276683af20c902b5cacec2a87a878763dfe1509a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for gtfs-digester 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.