gbfs-validator 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.
- gbfs_validator-0.1.0/LICENSE +21 -0
- gbfs_validator-0.1.0/NOTICE +31 -0
- gbfs_validator-0.1.0/PKG-INFO +344 -0
- gbfs_validator-0.1.0/README.md +315 -0
- gbfs_validator-0.1.0/pyproject.toml +57 -0
- gbfs_validator-0.1.0/setup.cfg +4 -0
- gbfs_validator-0.1.0/src/gbfs_validator/__init__.py +12 -0
- gbfs_validator-0.1.0/src/gbfs_validator/cli.py +168 -0
- gbfs_validator-0.1.0/src/gbfs_validator/conditionals.py +162 -0
- gbfs_validator-0.1.0/src/gbfs_validator/constants/__init__.py +0 -0
- gbfs_validator-0.1.0/src/gbfs_validator/constants/error_ids.py +56 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/PIN +2 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/free_bike_status.json +60 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/gbfs.json +56 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/station_information.json +98 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/station_status.json +84 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_alerts.json +101 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_calendar.json +69 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_hours.json +62 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_information.json +77 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_pricing_plans.json +73 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_regions.json +45 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/free_bike_status.json +99 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/gbfs.json +82 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/gbfs_versions.json +67 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/station_information.json +137 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/station_status.json +105 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_alerts.json +116 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_calendar.json +78 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_hours.json +77 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_information.json +148 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_pricing_plans.json +89 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_regions.json +53 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/free_bike_status.json +95 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/gbfs.json +82 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/gbfs_versions.json +67 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/station_information.json +137 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/station_status.json +98 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_alerts.json +116 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_calendar.json +78 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_hours.json +77 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_information.json +747 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_pricing_plans.json +87 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_regions.json +53 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/free_bike_status.json +135 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/gbfs.json +84 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/gbfs_versions.json +67 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/geofencing_zones.json +151 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/station_information.json +192 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/station_status.json +145 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_alerts.json +116 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_calendar.json +78 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_hours.json +77 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_information.json +747 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_pricing_plans.json +87 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_regions.json +53 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/vehicle_types.json +82 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/free_bike_status.json +140 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/gbfs.json +84 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/gbfs_versions.json +67 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/geofencing_zones.json +151 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/station_information.json +192 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/station_status.json +145 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_alerts.json +116 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_calendar.json +78 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_hours.json +77 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_information.json +747 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_pricing_plans.json +160 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_regions.json +53 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/vehicle_types.json +82 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/free_bike_status.json +153 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/gbfs.json +84 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/gbfs_versions.json +67 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/geofencing_zones.json +145 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/station_information.json +216 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/station_status.json +145 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_alerts.json +116 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_calendar.json +78 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_hours.json +77 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_information.json +808 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_pricing_plans.json +160 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_regions.json +53 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/vehicle_types.json +204 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/gbfs.json +69 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/gbfs_versions.json +60 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/geofencing_zones.json +202 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/manifest.json +80 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/station_information.json +278 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/station_status.json +145 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_alerts.json +161 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_information.json +1451 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_pricing_plans.json +191 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_regions.json +68 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/vehicle_status.json +153 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/vehicle_types.json +268 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/gbfs.json +70 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/gbfs_versions.json +61 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/geofencing_zones.json +202 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/manifest.json +114 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/station_information.json +282 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/station_status.json +145 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_alerts.json +161 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_information.json +1451 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_pricing_plans.json +222 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_regions.json +68 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/vehicle_availability.json +91 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/vehicle_status.json +153 -0
- gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/vehicle_types.json +273 -0
- gbfs_validator-0.1.0/src/gbfs_validator/discovery.py +43 -0
- gbfs_validator-0.1.0/src/gbfs_validator/feed.py +281 -0
- gbfs_validator-0.1.0/src/gbfs_validator/feedhelpers.py +168 -0
- gbfs_validator-0.1.0/src/gbfs_validator/fetch.py +149 -0
- gbfs_validator-0.1.0/src/gbfs_validator/jstruth.py +16 -0
- gbfs_validator-0.1.0/src/gbfs_validator/jsvalues.py +79 -0
- gbfs_validator-0.1.0/src/gbfs_validator/partials.py +287 -0
- gbfs_validator-0.1.0/src/gbfs_validator/results.py +71 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/__init__.py +0 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/applicators.py +235 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/engine.py +63 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/errors.py +40 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/formats.py +94 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/jsontypes.py +79 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/keywords.py +246 -0
- gbfs_validator-0.1.0/src/gbfs_validator/schema/patching.py +70 -0
- gbfs_validator-0.1.0/src/gbfs_validator/validate.py +41 -0
- gbfs_validator-0.1.0/src/gbfs_validator/version.py +1 -0
- gbfs_validator-0.1.0/src/gbfs_validator/versions.py +94 -0
- gbfs_validator-0.1.0/src/gbfs_validator.egg-info/PKG-INFO +344 -0
- gbfs_validator-0.1.0/src/gbfs_validator.egg-info/SOURCES.txt +148 -0
- gbfs_validator-0.1.0/src/gbfs_validator.egg-info/dependency_links.txt +1 -0
- gbfs_validator-0.1.0/src/gbfs_validator.egg-info/entry_points.txt +2 -0
- gbfs_validator-0.1.0/src/gbfs_validator.egg-info/requires.txt +6 -0
- gbfs_validator-0.1.0/src/gbfs_validator.egg-info/top_level.txt +1 -0
- gbfs_validator-0.1.0/tests/test_ajv_goldens.py +60 -0
- gbfs_validator-0.1.0/tests/test_cli.py +188 -0
- gbfs_validator-0.1.0/tests/test_conditionals.py +89 -0
- gbfs_validator-0.1.0/tests/test_differential_corpus.py +93 -0
- gbfs_validator-0.1.0/tests/test_engine_applicators.py +101 -0
- gbfs_validator-0.1.0/tests/test_engine_core.py +107 -0
- gbfs_validator-0.1.0/tests/test_feed.py +292 -0
- gbfs_validator-0.1.0/tests/test_feed_auth.py +131 -0
- gbfs_validator-0.1.0/tests/test_feed_js_semantics.py +165 -0
- gbfs_validator-0.1.0/tests/test_feedhelpers.py +102 -0
- gbfs_validator-0.1.0/tests/test_fetch.py +297 -0
- gbfs_validator-0.1.0/tests/test_formats.py +21 -0
- gbfs_validator-0.1.0/tests/test_no_runtime_dependency.py +32 -0
- gbfs_validator-0.1.0/tests/test_partials.py +61 -0
- gbfs_validator-0.1.0/tests/test_patching.py +127 -0
- gbfs_validator-0.1.0/tests/test_results.py +204 -0
- gbfs_validator-0.1.0/tests/test_versions.py +131 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the gbfs-validator authors
|
|
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,31 @@
|
|
|
1
|
+
gbfs-validator (Python)
|
|
2
|
+
Copyright 2026 the gbfs-validator authors
|
|
3
|
+
|
|
4
|
+
Released under the MIT License; see LICENSE.
|
|
5
|
+
|
|
6
|
+
Throughout this file "upstream" means MobilityData's Node gbfs-validator
|
|
7
|
+
(https://github.com/MobilityData/gbfs-validator), and "this project" means the
|
|
8
|
+
Python distribution published as gbfs-validator.
|
|
9
|
+
|
|
10
|
+
This project is an independent Python implementation of the validation
|
|
11
|
+
behavior defined by upstream. It is not an official MobilityData project, is
|
|
12
|
+
not affiliated with MobilityData, and is not endorsed by them.
|
|
13
|
+
|
|
14
|
+
Material taken from upstream rather than derived from it. Every entry names
|
|
15
|
+
what came from where, at the pinned commit:
|
|
16
|
+
|
|
17
|
+
src/gbfs_validator/data/schemas/
|
|
18
|
+
The GBFS JSON Schemas, copied verbatim from MobilityData's
|
|
19
|
+
gbfs-json-schema (https://github.com/MobilityData/gbfs-json-schema),
|
|
20
|
+
Apache License 2.0, at commit
|
|
21
|
+
916327fe43ea79683f57c87bc54fdfc24797862f. Reachable through upstream as
|
|
22
|
+
the versions/gbfs-json-schema submodule.
|
|
23
|
+
|
|
24
|
+
src/gbfs_validator/partials.py
|
|
25
|
+
Ports of the schema-patch generators under versions/partials/ in upstream,
|
|
26
|
+
at commit b734086ccdf93b5acd72bb349c6fda0c9f64fd39. The upstream package
|
|
27
|
+
(gbfs-validator/package.json) declares the MIT License; the repository
|
|
28
|
+
root carries the Apache License 2.0.
|
|
29
|
+
|
|
30
|
+
The pinned commits are also recorded in src/gbfs_validator/data/PIN, written
|
|
31
|
+
by tools/sync_schemas.py, so an installed copy names its own provenance.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gbfs-validator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python validator for GBFS feeds. No Node.
|
|
5
|
+
Author: veodyn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/veodyn/gbfs-validator
|
|
8
|
+
Project-URL: Issues, https://github.com/veodyn/gbfs-validator/issues
|
|
9
|
+
Keywords: gbfs,micromobility,bikeshare,transit,validator
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
License-File: NOTICE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
26
|
+
Requires-Dist: coverage>=7; extra == "dev"
|
|
27
|
+
Requires-Dist: pyright>=1.1; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# gbfs-validator (Python)
|
|
31
|
+
|
|
32
|
+
[](https://github.com/veodyn/gbfs-validator/actions/workflows/ci.yml)
|
|
33
|
+
[](https://pypi.org/project/gbfs-validator/)
|
|
34
|
+
[](https://pypi.org/project/gbfs-validator/)
|
|
35
|
+
[](https://github.com/veodyn/gbfs-validator/blob/main/LICENSE)
|
|
36
|
+
|
|
37
|
+
Validate GBFS feeds from Python. No Node, and no dependencies.
|
|
38
|
+
|
|
39
|
+
The reference GBFS validator is [MobilityData's][upstream], written in
|
|
40
|
+
JavaScript. Using it from Python means Node somewhere in your stack: a base
|
|
41
|
+
image to maintain, a lockfile to keep current, and a subprocess and a file
|
|
42
|
+
handoff at the seam. Or you call their hosted lambda, whose own OpenAPI
|
|
43
|
+
document advises against building production systems on it.
|
|
44
|
+
|
|
45
|
+
This is the same rule set, reimplemented, for the case where you would rather
|
|
46
|
+
have none of that.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install gbfs-validator
|
|
50
|
+
gbfs-validator -u https://gbfs.example.com/gbfs.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Python 3.11 or newer. This project shares a name with the software it
|
|
54
|
+
reimplements; below, "upstream" always means MobilityData's Node validator.
|
|
55
|
+
|
|
56
|
+
## What you get
|
|
57
|
+
|
|
58
|
+
One JSON report on stdout, or in a file with `-s`. The summary says what was
|
|
59
|
+
validated and how badly it went:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"validatorVersion": "0.1.0",
|
|
64
|
+
"version": {"detected": "2.3", "validated": "2.3"},
|
|
65
|
+
"hasErrors": true,
|
|
66
|
+
"errorsCount": 1
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then one entry per file the feed's version declares, whether or not the feed
|
|
71
|
+
publishes it, so a missing required file is visible as a fact rather than as
|
|
72
|
+
an absence:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"file": "station_status.json",
|
|
77
|
+
"required": true,
|
|
78
|
+
"exists": false,
|
|
79
|
+
"hasErrors": true,
|
|
80
|
+
"errorsCount": 1
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Files the feed does publish carry their notices, each naming the exact field.
|
|
85
|
+
Three real ones, with their JSON flattened to a line apiece:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
system_information.json /data required must have required property 'rental_apps'
|
|
89
|
+
vehicle_types.json /data/vehicle_types/0/default_pricing_plan_id enum must be equal to one of the allowed values
|
|
90
|
+
free_bike_status.json /data/bikes/0 required must have required property 'current_range_meters'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Those three come from one feed, and none of them is a plain schema check.
|
|
94
|
+
Each fired because of what some *other* file in the feed said: one vehicle
|
|
95
|
+
carries an iOS `rental_uris`, which makes `rental_apps` required over in
|
|
96
|
+
`system_information.json`; the published pricing plans decide which
|
|
97
|
+
`default_pricing_plan_id` values are legal in `vehicle_types.json`; and that
|
|
98
|
+
vehicle's type is `electric_assist`, which makes `current_range_meters`
|
|
99
|
+
required on every vehicle referencing it. Nineteen such rules patch the
|
|
100
|
+
schemas at run time, and they are most of what a JSON Schema alone would
|
|
101
|
+
miss.
|
|
102
|
+
|
|
103
|
+
For pre-3.0 feeds, which publish one copy of each file per language, entries
|
|
104
|
+
carry a `languages` array and each language reports separately.
|
|
105
|
+
|
|
106
|
+
The exit code is 0 whether or not notices fired, because the report is the
|
|
107
|
+
output. Add `--fail-on-error` to exit 1 when the feed has notices, which is
|
|
108
|
+
what you want in CI. A tool failure exits 1, as does asking for no output at
|
|
109
|
+
all; an unrecognized flag exits 2, which is argparse's convention.
|
|
110
|
+
|
|
111
|
+
## Running it
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
gbfs-validator -u https://gbfs.example.com/gbfs.json # print the report
|
|
115
|
+
gbfs-validator -u ./feed-dir -s reports/report.json -pr no # save it instead
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`-u` takes an `http(s)` URL, a path to a `gbfs.json`, or a directory holding
|
|
119
|
+
one. The version comes from the feed; `--feed-version` overrides it.
|
|
120
|
+
|
|
121
|
+
Three files are required only for systems that have them, so the validator has
|
|
122
|
+
to be told which kind of system it is looking at, exactly as upstream's web
|
|
123
|
+
form asks:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
gbfs-validator -u <url> --docked # station_information, station_status
|
|
127
|
+
gbfs-validator -u <url> --free-floating # free_bike_status / vehicle_status
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Without either flag those files are optional and their absence is not a
|
|
131
|
+
notice. This is upstream's behaviour, and it is the one thing most likely to
|
|
132
|
+
make two runs of the same feed disagree.
|
|
133
|
+
|
|
134
|
+
## From Python
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from gbfs_validator import validate_feed
|
|
138
|
+
|
|
139
|
+
report = validate_feed("https://gbfs.example.com/gbfs.json", docked=True)
|
|
140
|
+
|
|
141
|
+
report["summary"]["hasErrors"] # True
|
|
142
|
+
report["summary"]["errorsCount"] # 1
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`GBFS(url, docked=..., freefloating=..., version=..., auth=...)` gives the
|
|
146
|
+
same report from `.validation()`. Both are synchronous and return plain dicts.
|
|
147
|
+
|
|
148
|
+
Authenticated feeds work through the Python API, in all four of upstream's
|
|
149
|
+
modes:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from gbfs_validator import GBFS
|
|
153
|
+
|
|
154
|
+
GBFS(url, auth={"type": "bearer_token", "bearerToken": {"token": "..."}}).validation()
|
|
155
|
+
GBFS(url, auth={"type": "headers", "headers": [{"key": "X-Api-Key", "value": "..."}]}).validation()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The others are `basic_auth` and `oauth_client_credentials_grant`, shaped as
|
|
159
|
+
upstream shapes them. There are no CLI flags for these yet, deliberately:
|
|
160
|
+
there is no fixture for a real authenticated feed to test them against.
|
|
161
|
+
|
|
162
|
+
## Which versions
|
|
163
|
+
|
|
164
|
+
v1.0, v1.1, v2.0, v2.1, v2.2, v2.3, v3.0 and v3.1-RC3, from 96 JSON Schemas
|
|
165
|
+
vendored out of MobilityData's [`gbfs-json-schema`][schemas] at a pinned
|
|
166
|
+
commit. That is upstream's range too, including the 3.1 release candidate,
|
|
167
|
+
which `--feed-version 3.1-RC3` selects.
|
|
168
|
+
|
|
169
|
+
The version normally comes from `gbfs.json` and is detected per feed. The
|
|
170
|
+
override exists because a feed can declare one version and be shaped like
|
|
171
|
+
another.
|
|
172
|
+
|
|
173
|
+
## How close is it?
|
|
174
|
+
|
|
175
|
+
Close enough that swapping the binary is meant to be the entire change, and
|
|
176
|
+
that is tested rather than asserted. Three harnesses cover different ground.
|
|
177
|
+
|
|
178
|
+
**32 feeds through both validators.** `tools/differential.py` serves each
|
|
179
|
+
fixture feed over HTTP, runs this validator and the pinned upstream against
|
|
180
|
+
it, and fails on any difference in which notices fired, on which file, at
|
|
181
|
+
which path, with which parameters. The corpus covers all eight versions, both
|
|
182
|
+
system kinds, every conditional rule, multi-language feeds with a language
|
|
183
|
+
missing, both ways a manifest can appear, and feeds malformed enough that the
|
|
184
|
+
interesting question is whether both validators crash in the same place.
|
|
185
|
+
|
|
186
|
+
**90 schema cases and 34 format samples, generated by upstream's own AJV
|
|
187
|
+
stack.** The engine here is a draft-07 subset written from scratch, so its
|
|
188
|
+
oracle is `upstream/gbfs-validator/validate.js` itself: cases go through it,
|
|
189
|
+
the errors it returns are committed, and the suite compares. The 19 conditional
|
|
190
|
+
rules are compared the same way, against the JavaScript generators that build
|
|
191
|
+
them. When a golden and this engine disagree, the engine is what changes.
|
|
192
|
+
|
|
193
|
+
**32 real systems, sampled from MobilityData's catalog.** Fixtures only cover
|
|
194
|
+
what someone thought to write down. `tools/real_feed_sweep.py` samples live
|
|
195
|
+
systems and compares both validators on them; the last sweeps agreed on every
|
|
196
|
+
feed sampled, including ones carrying 4,149 and 5,390 notices.
|
|
197
|
+
|
|
198
|
+
Live feeds carry a trap worth naming. Vehicle positions change between two
|
|
199
|
+
sequential fetches, so comparing one validator's view against the other's
|
|
200
|
+
invents differences that were never there. Every feed is snapshotted before
|
|
201
|
+
comparison for that reason, and the one divergence a sweep did report turned
|
|
202
|
+
out to be exactly this: 740 against 754 live, 762 against 762 once frozen.
|
|
203
|
+
|
|
204
|
+
## Deliberate differences
|
|
205
|
+
|
|
206
|
+
Everything here was measured against the pinned upstream and kept on purpose.
|
|
207
|
+
Anything *not* on this list is a bug worth reporting.
|
|
208
|
+
|
|
209
|
+
**A failed run exits non-zero.** Upstream returns 0 from its critical-error
|
|
210
|
+
path in every case; here a tool failure exits 1, so a broken run is visible to
|
|
211
|
+
a pipeline. Feeds with notices still exit 0 unless you pass `--fail-on-error`,
|
|
212
|
+
so swapping the binary cannot silently change what your pipeline does.
|
|
213
|
+
|
|
214
|
+
**The report is JSON.** Upstream's stdout is Node's `util.inspect` rendering,
|
|
215
|
+
which is meant for reading rather than parsing.
|
|
216
|
+
|
|
217
|
+
**Local feeds are accepted.** Upstream takes a URL; this also takes a
|
|
218
|
+
directory or a `gbfs.json` path, which is what you want in a test suite.
|
|
219
|
+
|
|
220
|
+
**Options upstream only exposes through its JS API.** `--docked`,
|
|
221
|
+
`--free-floating` and `--feed-version` are reachable from the CLI here;
|
|
222
|
+
upstream's CLI constructs `new GBFS(url)` and offers none of them.
|
|
223
|
+
|
|
224
|
+
**CLI output drops `body` and `schema`.** Upstream echoes every fetched file
|
|
225
|
+
body and every compiled schema back into the report, which makes it enormous
|
|
226
|
+
and mostly a copy of the feed you already have. `--include-schema` restores
|
|
227
|
+
the schema. This is a CLI concern only: the Python API returns the full
|
|
228
|
+
report.
|
|
229
|
+
|
|
230
|
+
**Files are fetched one at a time**, not with `Promise.all`. All files are
|
|
231
|
+
still collected before any cross-file rule runs, so reports are unaffected.
|
|
232
|
+
|
|
233
|
+
**HTTP is urllib's**: no retries, a 30 second timeout, no compression
|
|
234
|
+
negotiation. Any failure, network or status or decode, becomes `exists: false`
|
|
235
|
+
for that file, which is what upstream's `catch` does.
|
|
236
|
+
|
|
237
|
+
**Message strings are mimicked, not guaranteed.** AJV's wording is reproduced
|
|
238
|
+
where it is cheap, but the differential compares which notices fire and where,
|
|
239
|
+
not their prose.
|
|
240
|
+
|
|
241
|
+
## Edges worth knowing about
|
|
242
|
+
|
|
243
|
+
Some of what a report does is surprising enough to look like a defect here.
|
|
244
|
+
These are the cases worth knowing before you go looking for one. Each matches
|
|
245
|
+
upstream, and each has a test pinning it there, so a well-meant cleanup fails
|
|
246
|
+
the suite rather than quietly ending parity.
|
|
247
|
+
|
|
248
|
+
**An `errorsCount` can be `null`.** When per-language counts are summed, a
|
|
249
|
+
language that validated cleanly contributes `false` rather than a list, and
|
|
250
|
+
reading a length from it yields `NaN`, which JSON renders as `null`. One such
|
|
251
|
+
file makes the summary total `null` too. Any multi-language feed with one
|
|
252
|
+
clean language and one that has notices lands here.
|
|
253
|
+
|
|
254
|
+
**JavaScript truthiness decides the conditional rules.** They read feed data
|
|
255
|
+
before it has been validated, so `""` and `0` are falsy while `[]` and `{}`
|
|
256
|
+
are truthy. A vehicle whose `vehicle_type_id` is an empty string does not make
|
|
257
|
+
`vehicle_types.json` required; one whose `vehicle_type_id` is an empty array
|
|
258
|
+
does.
|
|
259
|
+
|
|
260
|
+
**A badly malformed feed ends the run instead of producing a notice.** Those
|
|
261
|
+
same rules reach into `data.vehicle_types` and `data.bikes` before those files
|
|
262
|
+
are validated, so a feed publishing an object where the spec wants an array
|
|
263
|
+
stops the run. Reporting it as a notice would be friendlier, and would not
|
|
264
|
+
match.
|
|
265
|
+
|
|
266
|
+
**A pre-v3 feed carrying a `manifest_url` also ends the run**, because the
|
|
267
|
+
manifest is fetched for every version but only has a schema from 3.0 onward.
|
|
268
|
+
|
|
269
|
+
**A missing OAuth `access_token` yields the literal header `Bearer
|
|
270
|
+
undefined`** and the run continues, rather than stopping there.
|
|
271
|
+
|
|
272
|
+
**The autodiscovery fallback matches loosely.** The check for a URL that
|
|
273
|
+
already points at the discovery document is the pattern `/gbfs.json$/`, whose
|
|
274
|
+
dot is unescaped and so matches any character. A feed served at `/gbfs_json`
|
|
275
|
+
counts as already being that document.
|
|
276
|
+
|
|
277
|
+
## Working on it
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
|
|
281
|
+
python -m pytest -q
|
|
282
|
+
ruff check . && ruff format --check . && pyright
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
336 tests, no Node required: the oracles are committed, so the suite checks
|
|
286
|
+
against what upstream said rather than needing upstream present. Branch
|
|
287
|
+
coverage is 93%.
|
|
288
|
+
|
|
289
|
+
To run the parity harnesses you need the pinned clone and Node, at least 18
|
|
290
|
+
because that is what upstream's `engines` field requires. CI uses 20, and the
|
|
291
|
+
committed goldens were generated on 24:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
bash tools/sync_upstream.sh # clone at the pin, frozen lockfile
|
|
295
|
+
python tools/sync_schemas.py # vendor the JSON Schemas
|
|
296
|
+
python tools/differential.py # 32 fixture feeds, both validators
|
|
297
|
+
python tools/real_feed_sweep.py # live systems from the catalog
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`tools/differential.py --write-expectations` records upstream's verdict for
|
|
301
|
+
every fixture feed into `tests/fixtures/feeds/expectations.json`, which is what
|
|
302
|
+
the Node-free suite compares against. Regenerate it in the same commit as any
|
|
303
|
+
fixture change, and read the diff: a changed expectation means upstream's
|
|
304
|
+
behaviour changed, and that deserves its own commit.
|
|
305
|
+
|
|
306
|
+
Goldens are generated, never hand-edited. Each generator under `tools/node/`
|
|
307
|
+
rewrites `tests/fixtures/META.json` alongside its output, recording the Node
|
|
308
|
+
version, both pins and the lockfile hash the goldens were produced with.
|
|
309
|
+
|
|
310
|
+
What none of this covers is input nobody thought to build. Every parity defect
|
|
311
|
+
found late was on a feed shape no fixture carried, which is why the real-feed
|
|
312
|
+
sweep exists and why a green run means "matches on the feeds we have".
|
|
313
|
+
|
|
314
|
+
## Relationship to upstream
|
|
315
|
+
|
|
316
|
+
An independent reimplementation, not an official port, not affiliated with
|
|
317
|
+
MobilityData and not endorsed by them. The JSON Schemas are vendored from
|
|
318
|
+
their Apache-2.0 project and the conditional rules are ported from it;
|
|
319
|
+
[`NOTICE`](https://github.com/veodyn/gbfs-validator/blob/main/NOTICE) names what is copied rather than derived. The shared name
|
|
320
|
+
describes what the software does; it is not a claim of origin.
|
|
321
|
+
|
|
322
|
+
| What | Pin |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `MobilityData/gbfs-validator` | `b734086` |
|
|
325
|
+
| `MobilityData/gbfs-json-schema` | `916327f` |
|
|
326
|
+
|
|
327
|
+
Both are pinned by commit because neither publishes releases that parity could
|
|
328
|
+
be stated against. `src/gbfs_validator/data/PIN` carries the same two SHAs
|
|
329
|
+
inside the installed package, so a report can be traced to the schemas that
|
|
330
|
+
produced it.
|
|
331
|
+
|
|
332
|
+
Where upstream and this project's intuition disagree, upstream wins. A check
|
|
333
|
+
that looks wrong is almost always faithful, and a red differential is the
|
|
334
|
+
deliverable rather than an obstacle.
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
MIT; see [`LICENSE`](https://github.com/veodyn/gbfs-validator/blob/main/LICENSE).
|
|
339
|
+
|
|
340
|
+
The vendored schemas stay under upstream's Apache-2.0 licence.
|
|
341
|
+
[`NOTICE`](https://github.com/veodyn/gbfs-validator/blob/main/NOTICE) names each file and explains the shared name.
|
|
342
|
+
|
|
343
|
+
[upstream]: https://github.com/MobilityData/gbfs-validator
|
|
344
|
+
[schemas]: https://github.com/MobilityData/gbfs-json-schema
|