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.
Files changed (150) hide show
  1. gbfs_validator-0.1.0/LICENSE +21 -0
  2. gbfs_validator-0.1.0/NOTICE +31 -0
  3. gbfs_validator-0.1.0/PKG-INFO +344 -0
  4. gbfs_validator-0.1.0/README.md +315 -0
  5. gbfs_validator-0.1.0/pyproject.toml +57 -0
  6. gbfs_validator-0.1.0/setup.cfg +4 -0
  7. gbfs_validator-0.1.0/src/gbfs_validator/__init__.py +12 -0
  8. gbfs_validator-0.1.0/src/gbfs_validator/cli.py +168 -0
  9. gbfs_validator-0.1.0/src/gbfs_validator/conditionals.py +162 -0
  10. gbfs_validator-0.1.0/src/gbfs_validator/constants/__init__.py +0 -0
  11. gbfs_validator-0.1.0/src/gbfs_validator/constants/error_ids.py +56 -0
  12. gbfs_validator-0.1.0/src/gbfs_validator/data/PIN +2 -0
  13. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/free_bike_status.json +60 -0
  14. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/gbfs.json +56 -0
  15. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/station_information.json +98 -0
  16. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/station_status.json +84 -0
  17. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_alerts.json +101 -0
  18. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_calendar.json +69 -0
  19. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_hours.json +62 -0
  20. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_information.json +77 -0
  21. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_pricing_plans.json +73 -0
  22. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.0/system_regions.json +45 -0
  23. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/free_bike_status.json +99 -0
  24. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/gbfs.json +82 -0
  25. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/gbfs_versions.json +67 -0
  26. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/station_information.json +137 -0
  27. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/station_status.json +105 -0
  28. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_alerts.json +116 -0
  29. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_calendar.json +78 -0
  30. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_hours.json +77 -0
  31. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_information.json +148 -0
  32. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_pricing_plans.json +89 -0
  33. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v1.1/system_regions.json +53 -0
  34. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/free_bike_status.json +95 -0
  35. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/gbfs.json +82 -0
  36. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/gbfs_versions.json +67 -0
  37. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/station_information.json +137 -0
  38. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/station_status.json +98 -0
  39. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_alerts.json +116 -0
  40. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_calendar.json +78 -0
  41. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_hours.json +77 -0
  42. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_information.json +747 -0
  43. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_pricing_plans.json +87 -0
  44. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.0/system_regions.json +53 -0
  45. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/free_bike_status.json +135 -0
  46. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/gbfs.json +84 -0
  47. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/gbfs_versions.json +67 -0
  48. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/geofencing_zones.json +151 -0
  49. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/station_information.json +192 -0
  50. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/station_status.json +145 -0
  51. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_alerts.json +116 -0
  52. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_calendar.json +78 -0
  53. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_hours.json +77 -0
  54. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_information.json +747 -0
  55. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_pricing_plans.json +87 -0
  56. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/system_regions.json +53 -0
  57. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.1/vehicle_types.json +82 -0
  58. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/free_bike_status.json +140 -0
  59. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/gbfs.json +84 -0
  60. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/gbfs_versions.json +67 -0
  61. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/geofencing_zones.json +151 -0
  62. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/station_information.json +192 -0
  63. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/station_status.json +145 -0
  64. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_alerts.json +116 -0
  65. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_calendar.json +78 -0
  66. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_hours.json +77 -0
  67. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_information.json +747 -0
  68. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_pricing_plans.json +160 -0
  69. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/system_regions.json +53 -0
  70. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.2/vehicle_types.json +82 -0
  71. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/free_bike_status.json +153 -0
  72. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/gbfs.json +84 -0
  73. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/gbfs_versions.json +67 -0
  74. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/geofencing_zones.json +145 -0
  75. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/station_information.json +216 -0
  76. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/station_status.json +145 -0
  77. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_alerts.json +116 -0
  78. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_calendar.json +78 -0
  79. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_hours.json +77 -0
  80. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_information.json +808 -0
  81. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_pricing_plans.json +160 -0
  82. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/system_regions.json +53 -0
  83. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v2.3/vehicle_types.json +204 -0
  84. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/gbfs.json +69 -0
  85. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/gbfs_versions.json +60 -0
  86. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/geofencing_zones.json +202 -0
  87. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/manifest.json +80 -0
  88. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/station_information.json +278 -0
  89. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/station_status.json +145 -0
  90. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_alerts.json +161 -0
  91. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_information.json +1451 -0
  92. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_pricing_plans.json +191 -0
  93. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/system_regions.json +68 -0
  94. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/vehicle_status.json +153 -0
  95. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.0/vehicle_types.json +268 -0
  96. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/gbfs.json +70 -0
  97. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/gbfs_versions.json +61 -0
  98. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/geofencing_zones.json +202 -0
  99. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/manifest.json +114 -0
  100. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/station_information.json +282 -0
  101. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/station_status.json +145 -0
  102. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_alerts.json +161 -0
  103. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_information.json +1451 -0
  104. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_pricing_plans.json +222 -0
  105. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/system_regions.json +68 -0
  106. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/vehicle_availability.json +91 -0
  107. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/vehicle_status.json +153 -0
  108. gbfs_validator-0.1.0/src/gbfs_validator/data/schemas/v3.1-RC3/vehicle_types.json +273 -0
  109. gbfs_validator-0.1.0/src/gbfs_validator/discovery.py +43 -0
  110. gbfs_validator-0.1.0/src/gbfs_validator/feed.py +281 -0
  111. gbfs_validator-0.1.0/src/gbfs_validator/feedhelpers.py +168 -0
  112. gbfs_validator-0.1.0/src/gbfs_validator/fetch.py +149 -0
  113. gbfs_validator-0.1.0/src/gbfs_validator/jstruth.py +16 -0
  114. gbfs_validator-0.1.0/src/gbfs_validator/jsvalues.py +79 -0
  115. gbfs_validator-0.1.0/src/gbfs_validator/partials.py +287 -0
  116. gbfs_validator-0.1.0/src/gbfs_validator/results.py +71 -0
  117. gbfs_validator-0.1.0/src/gbfs_validator/schema/__init__.py +0 -0
  118. gbfs_validator-0.1.0/src/gbfs_validator/schema/applicators.py +235 -0
  119. gbfs_validator-0.1.0/src/gbfs_validator/schema/engine.py +63 -0
  120. gbfs_validator-0.1.0/src/gbfs_validator/schema/errors.py +40 -0
  121. gbfs_validator-0.1.0/src/gbfs_validator/schema/formats.py +94 -0
  122. gbfs_validator-0.1.0/src/gbfs_validator/schema/jsontypes.py +79 -0
  123. gbfs_validator-0.1.0/src/gbfs_validator/schema/keywords.py +246 -0
  124. gbfs_validator-0.1.0/src/gbfs_validator/schema/patching.py +70 -0
  125. gbfs_validator-0.1.0/src/gbfs_validator/validate.py +41 -0
  126. gbfs_validator-0.1.0/src/gbfs_validator/version.py +1 -0
  127. gbfs_validator-0.1.0/src/gbfs_validator/versions.py +94 -0
  128. gbfs_validator-0.1.0/src/gbfs_validator.egg-info/PKG-INFO +344 -0
  129. gbfs_validator-0.1.0/src/gbfs_validator.egg-info/SOURCES.txt +148 -0
  130. gbfs_validator-0.1.0/src/gbfs_validator.egg-info/dependency_links.txt +1 -0
  131. gbfs_validator-0.1.0/src/gbfs_validator.egg-info/entry_points.txt +2 -0
  132. gbfs_validator-0.1.0/src/gbfs_validator.egg-info/requires.txt +6 -0
  133. gbfs_validator-0.1.0/src/gbfs_validator.egg-info/top_level.txt +1 -0
  134. gbfs_validator-0.1.0/tests/test_ajv_goldens.py +60 -0
  135. gbfs_validator-0.1.0/tests/test_cli.py +188 -0
  136. gbfs_validator-0.1.0/tests/test_conditionals.py +89 -0
  137. gbfs_validator-0.1.0/tests/test_differential_corpus.py +93 -0
  138. gbfs_validator-0.1.0/tests/test_engine_applicators.py +101 -0
  139. gbfs_validator-0.1.0/tests/test_engine_core.py +107 -0
  140. gbfs_validator-0.1.0/tests/test_feed.py +292 -0
  141. gbfs_validator-0.1.0/tests/test_feed_auth.py +131 -0
  142. gbfs_validator-0.1.0/tests/test_feed_js_semantics.py +165 -0
  143. gbfs_validator-0.1.0/tests/test_feedhelpers.py +102 -0
  144. gbfs_validator-0.1.0/tests/test_fetch.py +297 -0
  145. gbfs_validator-0.1.0/tests/test_formats.py +21 -0
  146. gbfs_validator-0.1.0/tests/test_no_runtime_dependency.py +32 -0
  147. gbfs_validator-0.1.0/tests/test_partials.py +61 -0
  148. gbfs_validator-0.1.0/tests/test_patching.py +127 -0
  149. gbfs_validator-0.1.0/tests/test_results.py +204 -0
  150. 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
+ [![CI](https://github.com/veodyn/gbfs-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/veodyn/gbfs-validator/actions/workflows/ci.yml)
33
+ [![PyPI](https://img.shields.io/pypi/v/gbfs-validator)](https://pypi.org/project/gbfs-validator/)
34
+ [![Python versions](https://img.shields.io/pypi/pyversions/gbfs-validator)](https://pypi.org/project/gbfs-validator/)
35
+ [![License](https://img.shields.io/pypi/l/gbfs-validator)](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