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.
Files changed (47) hide show
  1. gtfslint_cli-0.1.0/CHANGELOG.md +30 -0
  2. gtfslint_cli-0.1.0/CITATION.cff +19 -0
  3. gtfslint_cli-0.1.0/LICENSE +21 -0
  4. gtfslint_cli-0.1.0/MANIFEST.in +9 -0
  5. gtfslint_cli-0.1.0/PKG-INFO +327 -0
  6. gtfslint_cli-0.1.0/README.md +289 -0
  7. gtfslint_cli-0.1.0/benchmarks/synthetic_feed.py +127 -0
  8. gtfslint_cli-0.1.0/pyproject.toml +107 -0
  9. gtfslint_cli-0.1.0/setup.cfg +4 -0
  10. gtfslint_cli-0.1.0/src/gtfslint/__init__.py +32 -0
  11. gtfslint_cli-0.1.0/src/gtfslint/__main__.py +6 -0
  12. gtfslint_cli-0.1.0/src/gtfslint/api.py +71 -0
  13. gtfslint_cli-0.1.0/src/gtfslint/cli.py +177 -0
  14. gtfslint_cli-0.1.0/src/gtfslint/config.py +112 -0
  15. gtfslint_cli-0.1.0/src/gtfslint/fieldtypes.py +165 -0
  16. gtfslint_cli-0.1.0/src/gtfslint/notices.py +682 -0
  17. gtfslint_cli-0.1.0/src/gtfslint/py.typed +0 -0
  18. gtfslint_cli-0.1.0/src/gtfslint/reader.py +83 -0
  19. gtfslint_cli-0.1.0/src/gtfslint/report.py +451 -0
  20. gtfslint_cli-0.1.0/src/gtfslint/spec.py +418 -0
  21. gtfslint_cli-0.1.0/src/gtfslint/stats.py +207 -0
  22. gtfslint_cli-0.1.0/src/gtfslint/validator.py +1560 -0
  23. gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/PKG-INFO +327 -0
  24. gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/SOURCES.txt +45 -0
  25. gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/dependency_links.txt +1 -0
  26. gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/entry_points.txt +2 -0
  27. gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/requires.txt +12 -0
  28. gtfslint_cli-0.1.0/src/gtfslint_cli.egg-info/top_level.txt +1 -0
  29. gtfslint_cli-0.1.0/tests/__init__.py +0 -0
  30. gtfslint_cli-0.1.0/tests/conftest.py +121 -0
  31. gtfslint_cli-0.1.0/tests/data/valid_feed/agency.txt +2 -0
  32. gtfslint_cli-0.1.0/tests/data/valid_feed/calendar.txt +3 -0
  33. gtfslint_cli-0.1.0/tests/data/valid_feed/calendar_dates.txt +3 -0
  34. gtfslint_cli-0.1.0/tests/data/valid_feed/fare_attributes.txt +3 -0
  35. gtfslint_cli-0.1.0/tests/data/valid_feed/fare_rules.txt +4 -0
  36. gtfslint_cli-0.1.0/tests/data/valid_feed/feed_info.txt +2 -0
  37. gtfslint_cli-0.1.0/tests/data/valid_feed/frequencies.txt +2 -0
  38. gtfslint_cli-0.1.0/tests/data/valid_feed/routes.txt +3 -0
  39. gtfslint_cli-0.1.0/tests/data/valid_feed/shapes.txt +12 -0
  40. gtfslint_cli-0.1.0/tests/data/valid_feed/stop_times.txt +27 -0
  41. gtfslint_cli-0.1.0/tests/data/valid_feed/stops.txt +10 -0
  42. gtfslint_cli-0.1.0/tests/data/valid_feed/transfers.txt +2 -0
  43. gtfslint_cli-0.1.0/tests/data/valid_feed/trips.txt +8 -0
  44. gtfslint_cli-0.1.0/tests/test_reading.py +218 -0
  45. gtfslint_cli-0.1.0/tests/test_report_cli.py +249 -0
  46. gtfslint_cli-0.1.0/tests/test_rules.py +271 -0
  47. 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,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/gtfslint/py.typed
7
+ recursive-include src/gtfslint *.py
8
+ recursive-include tests *.py *.txt
9
+ recursive-include benchmarks *.py
@@ -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).