gtfslint-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gtfslint_cli-0.1.0/CHANGELOG.md +30 -0
- gtfslint_cli-0.1.0/CITATION.cff +19 -0
- gtfslint_cli-0.1.0/LICENSE +21 -0
- gtfslint_cli-0.1.0/MANIFEST.in +9 -0
- gtfslint_cli-0.1.0/PKG-INFO +327 -0
- gtfslint_cli-0.1.0/README.md +289 -0
- gtfslint_cli-0.1.0/benchmarks/synthetic_feed.py +127 -0
- gtfslint_cli-0.1.0/pyproject.toml +107 -0
- gtfslint_cli-0.1.0/setup.cfg +4 -0
- gtfslint_cli-0.1.0/src/gtfslint/__init__.py +32 -0
- gtfslint_cli-0.1.0/src/gtfslint/__main__.py +6 -0
- gtfslint_cli-0.1.0/src/gtfslint/api.py +71 -0
- gtfslint_cli-0.1.0/src/gtfslint/cli.py +177 -0
- gtfslint_cli-0.1.0/src/gtfslint/config.py +112 -0
- gtfslint_cli-0.1.0/src/gtfslint/fieldtypes.py +165 -0
- gtfslint_cli-0.1.0/src/gtfslint/notices.py +682 -0
- gtfslint_cli-0.1.0/src/gtfslint/py.typed +0 -0
- gtfslint_cli-0.1.0/src/gtfslint/reader.py +83 -0
- gtfslint_cli-0.1.0/src/gtfslint/report.py +451 -0
- gtfslint_cli-0.1.0/src/gtfslint/spec.py +418 -0
- gtfslint_cli-0.1.0/src/gtfslint/stats.py +207 -0
- gtfslint_cli-0.1.0/src/gtfslint/validator.py +1560 -0
- gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/PKG-INFO +327 -0
- gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/SOURCES.txt +45 -0
- gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/dependency_links.txt +1 -0
- gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/entry_points.txt +2 -0
- gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/requires.txt +12 -0
- gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/top_level.txt +1 -0
- gtfslint_cli-0.1.0/tests/__init__.py +0 -0
- gtfslint_cli-0.1.0/tests/conftest.py +121 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/agency.txt +2 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/calendar.txt +3 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/calendar_dates.txt +3 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/fare_attributes.txt +3 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/fare_rules.txt +4 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/feed_info.txt +2 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/frequencies.txt +2 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/routes.txt +3 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/shapes.txt +12 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/stop_times.txt +27 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/stops.txt +10 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/transfers.txt +2 -0
- gtfslint_cli-0.1.0/tests/data/valid_feed/trips.txt +8 -0
- gtfslint_cli-0.1.0/tests/test_reading.py +218 -0
- gtfslint_cli-0.1.0/tests/test_report_cli.py +249 -0
- gtfslint_cli-0.1.0/tests/test_rules.py +271 -0
- gtfslint_cli-0.1.0/tests/test_stats_and_config.py +143 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-08
|
|
8
|
+
- Published on PyPI as `gtfslint-cli`; the import name and command remain `gtfslint`.
|
|
9
|
+
### Added
|
|
10
|
+
- First release.
|
|
11
|
+
- `gtfslint.validate(path) -> Report` and `gtfslint.stats(path) -> FeedStats` for GTFS
|
|
12
|
+
Schedule feeds stored as a `.zip` archive or a folder. Files are read with the standard
|
|
13
|
+
library `csv` module and may have a UTF-8 BOM, CRLF line endings, quoted fields and extra
|
|
14
|
+
columns.
|
|
15
|
+
- 63 rules with stable codes (GL001-GL704), severities, file/line/field locations and links
|
|
16
|
+
to the GTFS reference. Rules that mirror a MobilityData validator notice give its name.
|
|
17
|
+
They cover files and columns, field types, foreign keys, parent stations,
|
|
18
|
+
calendar/calendar_dates coherence, feed_info dates, duplicate keys, stop_times ordering
|
|
19
|
+
and timing, travel speed, stop spacing, unused entities, shape distances and block
|
|
20
|
+
overlaps.
|
|
21
|
+
- `stop_times.txt` and `shapes.txt` are streamed, with a second pass for trips or shapes
|
|
22
|
+
whose rows are not contiguous. 1,000,000 stop_times rows validate in about 7 s.
|
|
23
|
+
- Quality metrics: counts, route types, service span, trips per day, busiest date, headways
|
|
24
|
+
per route and direction, vehicle-hours, bounding box, share of trips with shapes, and
|
|
25
|
+
completeness of the wheelchair and bike fields.
|
|
26
|
+
- Output formats: text, JSON, Markdown, self-contained escaped HTML, and GitHub Actions
|
|
27
|
+
annotations.
|
|
28
|
+
- CLI commands `gtfslint check`, `gtfslint stats` and `gtfslint rules`, with
|
|
29
|
+
`--select/--ignore`, `--fail-on` and CI exit codes (0 / 1 / 2). Configuration is read from
|
|
30
|
+
`[tool.gtfslint]` in `pyproject.toml`. A pre-commit hook is included.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "gtfslint"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-08"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/gtfslint_sdk"
|
|
13
|
+
url: "https://pypi.org/project/gtfslint-cli/"
|
|
14
|
+
abstract: "Fast, pure-Python GTFS Schedule validator and quality report for continuous integration."
|
|
15
|
+
keywords:
|
|
16
|
+
- gtfs
|
|
17
|
+
- public transport
|
|
18
|
+
- data quality
|
|
19
|
+
- validator
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gtfslint-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast, pure-Python GTFS Schedule validator and quality report for CI: coded rules, feed metrics, JSON/Markdown/HTML/GitHub output.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/gtfslint_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/gtfslint_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/gtfslint_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/gtfslint_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: gtfs,gtfs-schedule,transit,public-transport,validator,linter,data-quality,timetable,mobility,ci,github-actions,gis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Classifier: Intended Audience :: Science/Research
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
|
|
31
|
+
Requires-Dist: tzdata; sys_platform == "win32"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# gtfslint
|
|
40
|
+
|
|
41
|
+
**Fast, pure-Python GTFS Schedule validator and quality report that runs in CI.**
|
|
42
|
+
|
|
43
|
+
`gtfslint` checks a [GTFS Schedule](https://gtfs.org/documentation/schedule/reference/) feed
|
|
44
|
+
(a `.zip` archive or a folder) against the specification, reports every problem with a stable
|
|
45
|
+
rule code, a severity and the exact file/line/field, and computes quality metrics such as
|
|
46
|
+
service span, trips per day and headways. It is a `pip install` away, has no compiled
|
|
47
|
+
dependencies, and validates a feed with one million `stop_times.txt` rows in about seven
|
|
48
|
+
seconds.
|
|
49
|
+
|
|
50
|
+
The canonical [MobilityData GTFS Validator](https://github.com/MobilityData/gtfs-validator)
|
|
51
|
+
is a Java application. `gtfslint` does not replace it. It covers the checks that catch most
|
|
52
|
+
broken feeds and fits where a Python tool works better: pre-commit hooks, GitHub Actions,
|
|
53
|
+
data pipelines and notebooks. When a rule matches a MobilityData notice, the notice name is
|
|
54
|
+
listed next to it, so you can compare the two reports.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install gtfslint-cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The distribution on PyPI is called `gtfslint-cli` (the name `gtfslint` is too close to an
|
|
63
|
+
existing project); the import name and the command are both `gtfslint`.
|
|
64
|
+
|
|
65
|
+
Python 3.9 or newer. The only dependencies are `tomli` on Python < 3.11 and `tzdata` on
|
|
66
|
+
Windows.
|
|
67
|
+
|
|
68
|
+
## Command line
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gtfslint check feed.zip # text report, exit code 1 on errors
|
|
72
|
+
gtfslint check feed.zip --html report.html # also write a self-contained HTML report
|
|
73
|
+
gtfslint check feed/ --format json > report.json
|
|
74
|
+
gtfslint check feed.zip --format github # GitHub Actions annotations
|
|
75
|
+
gtfslint check feed.zip --select GL2,GL4 --ignore GL408 --fail-on warning
|
|
76
|
+
gtfslint stats feed.zip --html stats.html # quality metrics
|
|
77
|
+
gtfslint rules # list every rule
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`check` options:
|
|
81
|
+
|
|
82
|
+
| Option | Meaning |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `-f, --format` | `text` (default), `json`, `markdown`, `html`, `github` |
|
|
85
|
+
| `-o, --output FILE` | write the `--format` output to a file instead of stdout |
|
|
86
|
+
| `--json / --markdown / --html FILE` | write extra reports in those formats |
|
|
87
|
+
| `--select`, `--ignore` | comma-separated rule codes, code prefixes (`GL4`) or names (`invalid_url`) |
|
|
88
|
+
| `--fail-on` | lowest severity that fails: `error` (default), `warning`, `info`, `never` |
|
|
89
|
+
| `--reference-date` | date used by the expiration rules (default: today) |
|
|
90
|
+
| `--max-notices-per-rule` | notices kept per rule (default 1000; counts are always complete) |
|
|
91
|
+
| `--stats` | add feed statistics to the report |
|
|
92
|
+
| `-q, --quiet` | print only the summary line |
|
|
93
|
+
| `--config FILE`, `--no-config` | choose or skip the `pyproject.toml` configuration |
|
|
94
|
+
|
|
95
|
+
Exit codes: **0** when no notice reaches `--fail-on`, **1** when at least one does, **2** for
|
|
96
|
+
usage errors, invalid configuration or an unreadable feed.
|
|
97
|
+
|
|
98
|
+
### GitHub Actions
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- run: pip install gtfslint-cli
|
|
102
|
+
- run: gtfslint check gtfs/ --format github --html gtfs-report.html
|
|
103
|
+
- uses: actions/upload-artifact@v4
|
|
104
|
+
if: always()
|
|
105
|
+
with:
|
|
106
|
+
name: gtfs-report
|
|
107
|
+
path: gtfs-report.html
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
When the feed is a folder, annotations point to the exact file and line. When it is a zip
|
|
111
|
+
archive, they are attached to the archive and the message gives the location inside it.
|
|
112
|
+
|
|
113
|
+
### pre-commit
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
repos:
|
|
117
|
+
- repo: https://github.com/YusufEminoglu/gtfslint_sdk
|
|
118
|
+
rev: v0.1.0
|
|
119
|
+
hooks:
|
|
120
|
+
- id: gtfslint
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The hook runs `gtfslint check` on every committed `.zip` feed.
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
Settings are read from the nearest `pyproject.toml`. Command-line options override them.
|
|
128
|
+
|
|
129
|
+
```toml
|
|
130
|
+
[tool.gtfslint]
|
|
131
|
+
select = ["ALL"] # codes, prefixes or names
|
|
132
|
+
ignore = ["GL401", "GL606"]
|
|
133
|
+
fail-on = "warning" # error | warning | info | never
|
|
134
|
+
max-notices-per-rule = 1000
|
|
135
|
+
reference-date = "2026-11-08" # default: today
|
|
136
|
+
min-stop-distance-m = 2.0 # GL502 threshold
|
|
137
|
+
max-parent-distance-m = 1000.0 # GL504 threshold
|
|
138
|
+
max-stop-distance-km = 50 # GL503 threshold for every route type (default: per type)
|
|
139
|
+
speed-limits-kmh = { 3 = 120, 0 = 80 } # GL408 limits per basic route_type
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Python API
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import gtfslint
|
|
146
|
+
|
|
147
|
+
report = gtfslint.validate("feed.zip") # -> gtfslint.Report
|
|
148
|
+
print(report.errors, report.warnings, report.infos, report.ok)
|
|
149
|
+
for notice in report.notices:
|
|
150
|
+
print(notice.code, notice.severity.label, notice.location, notice.message)
|
|
151
|
+
|
|
152
|
+
report.write("report.html") # format inferred from the suffix
|
|
153
|
+
data = report.to_dict() # also to_json/to_text/to_markdown/to_github
|
|
154
|
+
report.exit_code(fail_on="warning")
|
|
155
|
+
|
|
156
|
+
report = gtfslint.validate(
|
|
157
|
+
"feed/",
|
|
158
|
+
select=["GL2", "GL4"],
|
|
159
|
+
ignore=["fast_travel_between_consecutive_stops"],
|
|
160
|
+
reference_date="2026-11-08",
|
|
161
|
+
with_stats=True,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
st = gtfslint.stats("feed.zip") # -> gtfslint.FeedStats
|
|
165
|
+
st.trips_per_day["2026-11-09"], st.busiest_date, st.headways[0].median_headway_min
|
|
166
|
+
cfg = gtfslint.load_config("pyproject.toml") # -> gtfslint.Config
|
|
167
|
+
gtfslint.validate("feed.zip", config=cfg)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`gtfslint.RULES` maps every code to a `Rule`, which has `code`, `name`, `severity`,
|
|
171
|
+
`description`, `reference` and `mobilitydata`. `gtfslint.FeedError` is raised when the
|
|
172
|
+
path does not exist or is not a readable zip archive.
|
|
173
|
+
|
|
174
|
+
## Quality metrics
|
|
175
|
+
|
|
176
|
+
`gtfslint stats` and `gtfslint.stats()` report:
|
|
177
|
+
|
|
178
|
+
- **Counts:** agencies, stops, stations, entrances, generic nodes, boarding areas, routes,
|
|
179
|
+
trips, stop_times, shapes, services, frequency-based trips.
|
|
180
|
+
- **Route types:** number of routes per type. Extended types are listed separately.
|
|
181
|
+
- **Service span:** first and last service date, the number of active dates, and the first
|
|
182
|
+
departure and last arrival times. Times past `24:00:00` are kept as they are.
|
|
183
|
+
- **Trips per day:** trip count for each service date, after combining `calendar.txt` and
|
|
184
|
+
`calendar_dates.txt`. Trips in `frequencies.txt` are expanded into their departures. The
|
|
185
|
+
busiest date is reported separately.
|
|
186
|
+
- **Headways per route and direction:** minimum, median, mean and maximum headway in
|
|
187
|
+
minutes, and the number of departures, on the busiest date or on `--date`. Vehicle-hours
|
|
188
|
+
for the same date.
|
|
189
|
+
- **Coverage:** bounding box of stops and shape points (lon/lat).
|
|
190
|
+
- **Shapes:** percentage of trips whose `shape_id` resolves to a shape.
|
|
191
|
+
- **Completeness:** percentage of stops with `wheelchair_boarding`, trips with
|
|
192
|
+
`wheelchair_accessible`, trips with `bikes_allowed` (value 1 or 2), and stops with
|
|
193
|
+
coordinates.
|
|
194
|
+
|
|
195
|
+
## Rules
|
|
196
|
+
|
|
197
|
+
Every rule has a code (`GL` + three digits), a snake_case name, a default severity and a
|
|
198
|
+
link to the part of the GTFS Schedule reference it enforces. The last column gives the
|
|
199
|
+
matching [MobilityData validator notice](https://gtfs-validator.mobilitydata.org/rules.html),
|
|
200
|
+
when there is one. Row numbers are physical line numbers, with the header on line 1.
|
|
201
|
+
|
|
202
|
+
| Code | Name | Severity | Description | MobilityData notice |
|
|
203
|
+
|---|---|---|---|---|
|
|
204
|
+
| [GL001](https://gtfs.org/documentation/schedule/reference/#dataset-files) | `missing_required_file` | error | A file required by the specification (agency, routes, trips, stop_times, stops) is missing. | `missing_required_file` |
|
|
205
|
+
| [GL002](https://gtfs.org/documentation/schedule/reference/#dataset-files) | `missing_calendar_and_calendar_date_files` | error | Neither calendar.txt nor calendar_dates.txt is present; at least one is required. | `missing_calendar_and_calendar_date_files` |
|
|
206
|
+
| [GL003](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `empty_file` | error | A required file is empty or has a header but no records. | `empty_file` |
|
|
207
|
+
| [GL004](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `missing_required_column` | error | A required column is absent from the header of a file. | `missing_required_column` |
|
|
208
|
+
| [GL005](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `duplicated_column` | error | The same column name appears more than once in a header. | `duplicated_column` |
|
|
209
|
+
| [GL006](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `empty_column_name` | error | A header contains an empty column name. | `empty_column_name` |
|
|
210
|
+
| [GL007](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `invalid_row_length` | error | A row has a different number of fields than the header. | `invalid_row_length` |
|
|
211
|
+
| [GL008](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `csv_parsing_failed` | error | The file could not be parsed as UTF-8 CSV; the rest of the file was skipped. | `csv_parsing_failed` |
|
|
212
|
+
| [GL009](https://gtfs.org/documentation/schedule/reference/#dataset-files) | `unknown_file` | info | The feed contains a file that is not part of the GTFS Schedule specification. | `unknown_file` |
|
|
213
|
+
| [GL010](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `unknown_column` | info | A file has a column that is not defined by the specification. | `unknown_column` |
|
|
214
|
+
| [GL011](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `leading_or_trailing_whitespaces` | warning | A value or column name has leading or trailing whitespace (it is trimmed before the other checks). | `leading_or_trailing_whitespaces` |
|
|
215
|
+
| [GL012](https://gtfs.org/documentation/schedule/reference/#dataset-attributes) | `duplicate_key` | error | Two records share the same primary key (e.g. stop_id, or trip_id + stop_sequence). | `duplicate_key` |
|
|
216
|
+
| [GL013](https://gtfs.org/documentation/schedule/reference/#field-definitions) | `missing_required_field` | error | A required or conditionally required field is empty. | `missing_required_field` |
|
|
217
|
+
| [GL014](https://gtfs.org/documentation/schedule/reference/#routestxt) | `route_both_short_and_long_name_missing` | error | A route has neither route_short_name nor route_long_name. | `route_both_short_and_long_name_missing` |
|
|
218
|
+
| [GL015](https://gtfs.org/documentation/schedule/reference/#file-requirements) | `empty_row` | warning | A row contains only empty fields. | `empty_row` |
|
|
219
|
+
| [GL016](https://gtfs.org/documentation/schedule/reference/#feed_infotxt) | `missing_recommended_file` | warning | feed_info.txt is missing; it is recommended so consumers know the publisher and validity window. | `missing_recommended_file` |
|
|
220
|
+
| [GL101](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_color` | error | A color is not a six-digit hexadecimal value such as FFFFFF. | `invalid_color` |
|
|
221
|
+
| [GL102](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_url` | error | A URL is not a fully qualified http:// or https:// URL. | `invalid_url` |
|
|
222
|
+
| [GL103](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_timezone` | error | A timezone is not a valid IANA TZ database name such as Europe/Istanbul. | `invalid_timezone` |
|
|
223
|
+
| [GL104](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_date` | error | A date is not a valid calendar date in YYYYMMDD format. | `invalid_date` |
|
|
224
|
+
| [GL105](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_time` | error | A time is not in H:MM:SS or HH:MM:SS format (hours above 24 are allowed). | `invalid_time` |
|
|
225
|
+
| [GL106](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_integer` | error | A value that must be an integer (or enumeration) could not be parsed. | `invalid_integer` |
|
|
226
|
+
| [GL107](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_float` | error | A value that must be a number could not be parsed. | `invalid_float` |
|
|
227
|
+
| [GL108](https://gtfs.org/documentation/schedule/reference/#field-types) | `unexpected_enum_value` | warning | An enumeration field has a value not listed by the specification. | `unexpected_enum_value` |
|
|
228
|
+
| [GL109](https://gtfs.org/documentation/schedule/reference/#field-types) | `number_out_of_range` | error | A number is outside its allowed range (latitude, longitude, non-negative or positive fields). | `number_out_of_range` |
|
|
229
|
+
| [GL110](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_email` | error | An e-mail address is malformed. | `invalid_email` |
|
|
230
|
+
| [GL111](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_language_code` | error | A language code is not a well-formed IETF BCP 47 tag. | `invalid_language_code` |
|
|
231
|
+
| [GL112](https://gtfs.org/documentation/schedule/reference/#field-types) | `invalid_currency` | error | A currency code is not an ISO 4217 alphabetical code. | `invalid_currency` |
|
|
232
|
+
| [GL113](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `point_near_origin` | error | A stop or shape point lies at (0, 0) ('null island'), usually a missing coordinate. | `point_near_origin` |
|
|
233
|
+
| [GL114](https://gtfs.org/documentation/schedule/reference/#agencytxt) | `inconsistent_agency_timezone` | error | Agencies in agency.txt use different agency_timezone values. | `inconsistent_agency_timezone` |
|
|
234
|
+
| [GL115](https://gtfs.org/documentation/schedule/reference/#routestxt) | `route_color_contrast` | warning | route_color and route_text_color have a WCAG contrast ratio below 3:1. | `route_color_contrast` |
|
|
235
|
+
| [GL116](https://gtfs.org/documentation/schedule/reference/#field-types) | `non_ascii_or_non_printable_char` | warning | An ID contains non-ASCII or non-printable characters. | `non_ascii_or_non_printable_char` |
|
|
236
|
+
| [GL201](https://gtfs.org/documentation/schedule/reference/#field-definitions) | `foreign_key_violation` | error | A reference points to a record that does not exist (e.g. trips.route_id, stop_times.stop_id, stops.parent_station, transfers, fare tables). | `foreign_key_violation` |
|
|
237
|
+
| [GL202](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `wrong_parent_location_type` | error | parent_station references a location of the wrong type (a stop's parent must be a station, a boarding area's parent must be a stop). | `wrong_parent_location_type` |
|
|
238
|
+
| [GL203](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `station_with_parent_station` | error | A station (location_type 1) has a parent_station. | `station_with_parent_station` |
|
|
239
|
+
| [GL204](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `location_with_unexpected_stop_time` | error | stop_times references a station, entrance or generic node instead of a stop or boarding area. | `location_with_unexpected_stop_time` |
|
|
240
|
+
| [GL301](https://gtfs.org/documentation/schedule/reference/#calendartxt) | `start_and_end_range_out_of_order` | error | A start date/time is after its end (calendar, feed_info, frequencies). | `start_and_end_range_out_of_order` |
|
|
241
|
+
| [GL302](https://gtfs.org/documentation/schedule/reference/#calendartxt) | `service_never_active` | warning | A service_id is never active once calendar and calendar_dates are combined. | - |
|
|
242
|
+
| [GL303](https://gtfs.org/documentation/schedule/reference/#calendar_datestxt) | `redundant_calendar_date` | info | A calendar_dates exception adds a date that is already active or removes one that is not. | - |
|
|
243
|
+
| [GL304](https://gtfs.org/documentation/schedule/reference/#calendartxt) | `expired_calendar` | warning | A service has no active dates on or after the reference date. | `expired_calendar` |
|
|
244
|
+
| [GL401](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `stop_times_not_sorted` | info | The rows of a trip are not listed in increasing stop_sequence order (allowed, but slows down consumers). | - |
|
|
245
|
+
| [GL402](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `stop_time_with_arrival_before_previous_departure_time` | error | A stop's arrival_time is earlier than the departure_time at the previous stop. | `stop_time_with_arrival_before_previous_departure_time` |
|
|
246
|
+
| [GL403](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `stop_time_arrival_after_departure` | error | A stop's arrival_time is later than its own departure_time. | - |
|
|
247
|
+
| [GL404](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `stop_time_with_only_arrival_or_departure_time` | error | Only one of arrival_time and departure_time is set. | `stop_time_with_only_arrival_or_departure_time` |
|
|
248
|
+
| [GL405](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `missing_trip_edge` | error | The first or last stop of a trip has no arrival_time/departure_time. | `missing_trip_edge` |
|
|
249
|
+
| [GL406](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `decreasing_or_equal_stop_time_distance` | error | shape_dist_traveled does not increase along a trip. | `decreasing_or_equal_stop_time_distance` |
|
|
250
|
+
| [GL407](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `stop_time_timepoint_without_times` | error | timepoint=1 but the stop has no arrival/departure time. | `stop_time_timepoint_without_times` |
|
|
251
|
+
| [GL408](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `fast_travel_between_consecutive_stops` | warning | The speed between two timed stops exceeds the plausible maximum for the route type. | `fast_travel_between_consecutive_stops` |
|
|
252
|
+
| [GL409](https://gtfs.org/documentation/schedule/reference/#stop_timestxt) | `unusable_trip` | warning | A trip visits fewer than two stops. | `unusable_trip` |
|
|
253
|
+
| [GL501](https://gtfs.org/documentation/schedule/reference/#shapestxt) | `decreasing_shape_distance` | error | shape_dist_traveled decreases along a shape. | `decreasing_shape_distance` |
|
|
254
|
+
| [GL502](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `stops_too_close` | warning | Two different stops (not in the same station) are closer than min-stop-distance-m; often an accidental duplicate. | - |
|
|
255
|
+
| [GL503](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `stops_too_far` | warning | Two consecutive stops of a trip are farther apart than plausible for the route type; often a wrong coordinate. | - |
|
|
256
|
+
| [GL504](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `stop_too_far_from_parent_station` | warning | A stop, entrance or node is farther than max-parent-distance-m from its parent station. | - |
|
|
257
|
+
| [GL601](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `stop_without_stop_time` | warning | A stop (location_type 0) is never served by stop_times. | `stop_without_stop_time` |
|
|
258
|
+
| [GL602](https://gtfs.org/documentation/schedule/reference/#stopstxt) | `unused_station` | info | A station has no child stops. | `unused_station` |
|
|
259
|
+
| [GL603](https://gtfs.org/documentation/schedule/reference/#routestxt) | `route_without_trips` | warning | A route has no trips. | - |
|
|
260
|
+
| [GL604](https://gtfs.org/documentation/schedule/reference/#shapestxt) | `unused_shape` | warning | A shape is not referenced by any trip. | `unused_shape` |
|
|
261
|
+
| [GL605](https://gtfs.org/documentation/schedule/reference/#tripstxt) | `unused_trip` | warning | A trip has no stop_times. | `unused_trip` |
|
|
262
|
+
| [GL606](https://gtfs.org/documentation/schedule/reference/#calendartxt) | `unused_service` | info | A service_id from calendar/calendar_dates is not used by any trip. | - |
|
|
263
|
+
| [GL701](https://gtfs.org/documentation/schedule/reference/#tripstxt) | `block_trips_with_overlapping_stop_times` | error | Two trips with the same block_id run at overlapping times on a shared service date. | `block_trips_with_overlapping_stop_times` |
|
|
264
|
+
| [GL702](https://gtfs.org/documentation/schedule/reference/#feed_infotxt) | `missing_feed_info_date` | warning | feed_info.txt has no feed_start_date or no feed_end_date. | `missing_feed_info_date` |
|
|
265
|
+
| [GL703](https://gtfs.org/documentation/schedule/reference/#feed_infotxt) | `feed_expiration_date7_days` | warning | feed_end_date is less than 7 days after the reference date (or already passed). | `feed_expiration_date7_days` |
|
|
266
|
+
| [GL704](https://gtfs.org/documentation/schedule/reference/#feed_infotxt) | `feed_expiration_date30_days` | warning | feed_end_date is between 7 and 30 days after the reference date. | `feed_expiration_date30_days` |
|
|
267
|
+
|
|
268
|
+
### Notes on rule behaviour
|
|
269
|
+
|
|
270
|
+
- **Field checks (GL101-GL116)** apply to every field that the reference defines for
|
|
271
|
+
agency, stops, routes, trips, stop_times, calendar, calendar_dates, fare_attributes,
|
|
272
|
+
fare_rules, shapes, frequencies, transfers, pathways, levels, feed_info, translations and
|
|
273
|
+
attributions. Newer files such as Fares v2, flex and networks are recognised, so they do
|
|
274
|
+
not raise `unknown_file`, but their fields are not checked yet.
|
|
275
|
+
- **Required fields that depend on other fields (GL013):** `stop_name`, `stop_lat` and
|
|
276
|
+
`stop_lon` for `location_type` 0-2; `parent_station` for `location_type` 2-4;
|
|
277
|
+
`agency_id` when there are several agencies; `stop_id` in stop_times unless
|
|
278
|
+
`location_group_id` or `location_id` is set; the transfer stops for `transfer_type`
|
|
279
|
+
1-3 and the transfer trips for 4-5.
|
|
280
|
+
- **Foreign keys (GL201):** trips → routes, calendars and shapes; stop_times → trips and
|
|
281
|
+
stops; stops → parent station and levels; routes and fare_attributes → agency;
|
|
282
|
+
frequencies → trips; transfers → stops, routes and trips; fare_rules → fare_attributes,
|
|
283
|
+
routes and `zone_id`; pathways → stops; attributions → agency, routes and trips.
|
|
284
|
+
- **Travel speed (GL408)** is checked between consecutive *timed* stops. Untimed stops in
|
|
285
|
+
between add to the distance. A time difference under 60 s counts as 60 s, because most
|
|
286
|
+
feeds round times to the minute. The default limits in km/h are: tram 100, subway 150,
|
|
287
|
+
rail 500, bus 150, ferry 80, cable tram 30, aerial lift 50, funicular 50, trolleybus 150,
|
|
288
|
+
monorail 150, air 1200.
|
|
289
|
+
- **Stop spacing (GL503)** default maximum gap in km: 20 for urban rail and cable modes,
|
|
290
|
+
100 for bus, 500 for rail and ferry.
|
|
291
|
+
- **Block overlaps (GL701)** are reported when two trips with the same `block_id` overlap
|
|
292
|
+
in time *and* their services share at least one date. Frequency-based trips are skipped.
|
|
293
|
+
- **Expiration (GL304, GL703, GL704)** is measured from `--reference-date`, which defaults
|
|
294
|
+
to today. Set a fixed date in CI if builds must be reproducible.
|
|
295
|
+
|
|
296
|
+
## Performance
|
|
297
|
+
|
|
298
|
+
`stop_times.txt` and `shapes.txt` are streamed one trip or one shape at a time. Small tables
|
|
299
|
+
are kept in compact indexes, and parsed values are cached. The file is not loaded into
|
|
300
|
+
memory. Producers almost always write the rows of a trip together. If a trip's rows are
|
|
301
|
+
scattered through the file, gtfslint finds those trips and reads them again in a second pass,
|
|
302
|
+
so the results stay correct.
|
|
303
|
+
|
|
304
|
+
Benchmark (`benchmarks/synthetic_feed.py`, a synthetic zipped feed with 100 routes, 50,000
|
|
305
|
+
trips, 2,000 stops and **1,000,000 stop_times rows**, Python 3.12 on a Windows laptop):
|
|
306
|
+
|
|
307
|
+
| Task | Time |
|
|
308
|
+
|---|---|
|
|
309
|
+
| `gtfslint.validate()` (all rules) | 6.8 s (about 150,000 rows/s) |
|
|
310
|
+
| `gtfslint.stats()` | 6.5 s |
|
|
311
|
+
|
|
312
|
+
## Development
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
pip install -e ".[dev]"
|
|
316
|
+
python -m pytest --cov=gtfslint
|
|
317
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
318
|
+
python -m mypy src
|
|
319
|
+
python benchmarks/synthetic_feed.py /tmp/gtfs-bench
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The tests use a small synthetic feed, `tests/data/valid_feed`, which passes every rule.
|
|
323
|
+
`tests/test_rules.py` has one broken variant of it per rule.
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT © Yusuf Eminoğlu. See [LICENSE](LICENSE).
|