aiomobilitydatabase 0.3.0__tar.gz → 0.5.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 (132) hide show
  1. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/PKG-INFO +159 -17
  2. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/README.md +157 -15
  3. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/const.py +1 -1
  4. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/__init__.py +14 -0
  5. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/client.py +31 -7
  6. aiomobilitydatabase-0.5.0/src/aiomobilitydatabase/feeds/gbfs.py +642 -0
  7. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/geo.py +24 -11
  8. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/models.py +221 -9
  9. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/rt.py +313 -13
  10. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/static_index.py +363 -65
  11. aiomobilitydatabase-0.5.0/src/aiomobilitydatabase/feeds/transit.py +1897 -0
  12. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/catalog/gbfs_feed.json +16 -0
  13. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/discovery_23.json +16 -0
  14. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/free_bike_status_23.json +6 -1
  15. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/station_info_23.json +7 -1
  16. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/station_status_23.json +7 -2
  17. aiomobilitydatabase-0.5.0/tests/feeds/data/gbfs/system_alerts.json +33 -0
  18. aiomobilitydatabase-0.5.0/tests/feeds/data/gbfs/system_info_23.json +22 -0
  19. aiomobilitydatabase-0.5.0/tests/feeds/data/gbfs/system_pricing_plans.json +19 -0
  20. aiomobilitydatabase-0.5.0/tests/feeds/data/gbfs/system_regions.json +15 -0
  21. aiomobilitydatabase-0.5.0/tests/feeds/data/gbfs/vehicle_types.json +24 -0
  22. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/fixtures.py +4 -0
  23. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_client.py +107 -0
  24. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_descriptive_attributes.py +16 -5
  25. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_direct_urls.py +5 -5
  26. aiomobilitydatabase-0.5.0/tests/feeds/test_gbfs_stations.py +589 -0
  27. aiomobilitydatabase-0.5.0/tests/feeds/test_gbfs_vehicles.py +218 -0
  28. aiomobilitydatabase-0.5.0/tests/feeds/test_geo.py +90 -0
  29. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_models.py +109 -4
  30. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_properties.py +3555 -435
  31. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_rt.py +140 -6
  32. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_sample_feed.py +17 -6
  33. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_static_arrivals.py +34 -0
  34. aiomobilitydatabase-0.5.0/tests/feeds/test_static_build.py +731 -0
  35. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_static_trips.py +12 -0
  36. aiomobilitydatabase-0.5.0/tests/feeds/test_transit_arrivals.py +886 -0
  37. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_transit_frequencies.py +84 -8
  38. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_transit_trips.py +136 -8
  39. aiomobilitydatabase-0.5.0/tests/feeds/test_transit_vehicles_alerts.py +725 -0
  40. aiomobilitydatabase-0.5.0/tests/test_catalog_properties.py +383 -0
  41. aiomobilitydatabase-0.3.0/src/aiomobilitydatabase/feeds/gbfs.py +0 -320
  42. aiomobilitydatabase-0.3.0/src/aiomobilitydatabase/feeds/transit.py +0 -1190
  43. aiomobilitydatabase-0.3.0/tests/feeds/data/gbfs/system_info_23.json +0 -10
  44. aiomobilitydatabase-0.3.0/tests/feeds/test_gbfs_stations.py +0 -205
  45. aiomobilitydatabase-0.3.0/tests/feeds/test_gbfs_vehicles.py +0 -80
  46. aiomobilitydatabase-0.3.0/tests/feeds/test_geo.py +0 -38
  47. aiomobilitydatabase-0.3.0/tests/feeds/test_static_build.py +0 -235
  48. aiomobilitydatabase-0.3.0/tests/feeds/test_transit_arrivals.py +0 -253
  49. aiomobilitydatabase-0.3.0/tests/feeds/test_transit_vehicles_alerts.py +0 -279
  50. aiomobilitydatabase-0.3.0/tests/test_catalog_properties.py +0 -211
  51. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/.github/workflows/ci.yml +0 -0
  52. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/.github/workflows/publish.yml +0 -0
  53. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/.gitignore +0 -0
  54. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/.pre-commit-config.yaml +0 -0
  55. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/LICENSE +0 -0
  56. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/pyproject.toml +0 -0
  57. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/scripts/generate_rt_fixtures.py +0 -0
  58. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/__init__.py +0 -0
  59. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/client.py +0 -0
  60. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/exceptions.py +0 -0
  61. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/const.py +0 -0
  62. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/feeds/exceptions.py +0 -0
  63. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/models.py +0 -0
  64. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/src/aiomobilitydatabase/py.typed +0 -0
  65. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/__init__.py +0 -0
  66. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/conftest.py +0 -0
  67. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/__init__.py +0 -0
  68. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/conftest.py +0 -0
  69. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/catalog/gtfs_feed.json +0 -0
  70. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/catalog/gtfs_rt_feed.json +0 -0
  71. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/catalog/token_response.json +0 -0
  72. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/discovery_30.json +0 -0
  73. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/system_info_30.json +0 -0
  74. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gbfs/vehicle_status_30.json +0 -0
  75. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/agency.txt +0 -0
  76. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/calendar.txt +0 -0
  77. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/calendar_dates.txt +0 -0
  78. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/routes.txt +0 -0
  79. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/stop_times.txt +0 -0
  80. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/stops.txt +0 -0
  81. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/gtfs/trips.txt +0 -0
  82. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/added_trips_s1.pb +0 -0
  83. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/alerts.pb +0 -0
  84. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/alerts_trip_scoped.pb +0 -0
  85. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_baseline.pb +0 -0
  86. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_freq_bare_cancel.pb +0 -0
  87. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_freq_canceled_tomorrow.pb +0 -0
  88. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_freq_matched.pb +0 -0
  89. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_freq_unmatched.pb +0 -0
  90. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_both_ends.pb +0 -0
  91. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_canceled.pb +0 -0
  92. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_canceled_tomorrow.pb +0 -0
  93. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_dated_tomorrow_delay.pb +0 -0
  94. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_delayed.pb +0 -0
  95. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_dest_arrival.pb +0 -0
  96. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_no_data_cut.pb +0 -0
  97. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_skip_s1.pb +0 -0
  98. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_skip_s2.pb +0 -0
  99. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_skip_s3.pb +0 -0
  100. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/trip_updates_t1_trip_delay.pb +0 -0
  101. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/vehicle_positions.pb +0 -0
  102. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/rt/vehicle_positions_status.pb +0 -0
  103. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/README.md +0 -0
  104. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/agency.txt +0 -0
  105. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/calendar.txt +0 -0
  106. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/calendar_dates.txt +0 -0
  107. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/fare_attributes.txt +0 -0
  108. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/fare_rules.txt +0 -0
  109. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/feed_info.txt +0 -0
  110. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/frequencies.txt +0 -0
  111. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/routes.txt +0 -0
  112. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/shapes.txt +0 -0
  113. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/stop_times.txt +0 -0
  114. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/stops.txt +0 -0
  115. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/transfers.txt +0 -0
  116. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/translations.txt +0 -0
  117. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/data/sample_feed/trips.txt +0 -0
  118. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_exceptions.py +0 -0
  119. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_extra_import_guard.py +0 -0
  120. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_static_frequencies.py +0 -0
  121. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_station_grouping.py +0 -0
  122. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/feeds/test_transit_resolution.py +0 -0
  123. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/fixtures.py +0 -0
  124. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/mock_server.py +0 -0
  125. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_client_request.py +0 -0
  126. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_client_session.py +0 -0
  127. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_client_token.py +0 -0
  128. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_endpoints.py +0 -0
  129. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_exceptions.py +0 -0
  130. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_models.py +0 -0
  131. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_params.py +0 -0
  132. {aiomobilitydatabase-0.3.0 → aiomobilitydatabase-0.5.0}/tests/test_public_api.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: aiomobilitydatabase
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Async Python client for the Mobility Database catalog API
5
5
  Project-URL: Source Code, https://github.com/raman325/aiomobilitydatabase
6
6
  Project-URL: Bug Reports, https://github.com/raman325/aiomobilitydatabase/issues
@@ -104,7 +104,7 @@ aiomobilitydatabase[feeds]`.
104
104
  ```python
105
105
  import asyncio
106
106
 
107
- from aiomobilitydatabase.feeds import Circle, MobilityFeedsClient
107
+ from aiomobilitydatabase.feeds import ArrivalsQuery, Circle, MobilityFeedsClient
108
108
 
109
109
 
110
110
  async def main() -> None:
@@ -137,11 +137,20 @@ async def main() -> None:
137
137
 
138
138
  # Scheduled arrivals merged with realtime (delays, cancellations,
139
139
  # RT-added trips) when the feed publishes GTFS-RT TripUpdates.
140
- arrivals = await transit.get_arrivals([nearby_stops[0].id], limit=2)
140
+ # Pass several ArrivalsQuery objects to get several boards (each
141
+ # with its own stops, route/headsign filters, and limit) from one
142
+ # realtime fetch; the result has one list per query, in order.
143
+ # Delayed trips stay listed until their prediction passes.
144
+ [arrivals] = await transit.get_arrivals(
145
+ [ArrivalsQuery([nearby_stops[0].id], limit=2)]
146
+ )
141
147
  for arrival in arrivals:
142
148
  when = arrival.predicted_departure or arrival.scheduled_departure
143
149
  live = "live" if arrival.realtime else "scheduled"
144
- print(f"{arrival.route_name} -> {arrival.headsign}: {when} ({live})")
150
+ # .stop and .route are the whole static records: platform codes,
151
+ # coordinates, route_type and route colours all come along.
152
+ route = arrival.route.display_name if arrival.route else arrival.route_id
153
+ print(f"{route} -> {arrival.headsign}: {when} ({live})")
145
154
 
146
155
  # Origin→destination: the next departures from stop A on trips that
147
156
  # later reach stop B, with the same realtime overlay. is_first/is_last
@@ -150,7 +159,8 @@ async def main() -> None:
150
159
  nearby_stops[0].id, nearby_stops[1].id, limit=2
151
160
  )
152
161
  for trip in trips:
153
- print(trip.route_name, trip.scheduled_departure, trip.is_last)
162
+ name = trip.route.display_name if trip.route else trip.route_id
163
+ print(name, trip.scheduled_departure, trip.is_last)
154
164
 
155
165
  vehicles = await transit.get_vehicles()
156
166
  alerts = await transit.get_alerts()
@@ -174,7 +184,7 @@ catalog methods, so everything above works unchanged:
174
184
  ```python
175
185
  import asyncio
176
186
 
177
- from aiomobilitydatabase.feeds import MobilityFeedsClient
187
+ from aiomobilitydatabase.feeds import ArrivalsQuery, MobilityFeedsClient
178
188
 
179
189
 
180
190
  async def main() -> None:
@@ -186,7 +196,7 @@ async def main() -> None:
186
196
  vehicle_positions_urls=["https://agency.example/gtfs-rt/positions.pb"],
187
197
  headers={"Authorization": "Bearer PRODUCER_TOKEN"}, # optional
188
198
  )
189
- arrivals = await transit.get_arrivals([transit.stops[0].id])
199
+ [arrivals] = await transit.get_arrivals([ArrivalsQuery([transit.stops[0].id])])
190
200
 
191
201
  bikes = await client.get_gbfs_feed_from_url(
192
202
  "https://bikes.example/gbfs/gbfs.json"
@@ -226,6 +236,15 @@ Notes on direct mode:
226
236
  work with or without realtime coverage; GTFS-RT TripUpdates overlay delays,
227
237
  cancellations, and added trips when present, with cancellation always
228
238
  winning over stale predictions.
239
+ - **Arrivals are batched and delay-aware**: `get_arrivals()` takes a sequence
240
+ of `ArrivalsQuery` and returns one board per query from a single schedule
241
+ read and a single realtime fetch, so several stops with different
242
+ route/headsign filters cost one round trip instead of one each. Each query's
243
+ filters apply *before* its `limit`, and `limit` caps that board as a whole
244
+ rather than per GTFS stop — so a narrow filter can never come back empty
245
+ because busier routes used up the limit. A `grace` window (default one hour)
246
+ keeps a delayed trip listed past its scheduled time until its prediction
247
+ passes, while a schedule-only departure still drops at its scheduled time.
229
248
  - **frequencies.txt is materialized at build time**: headway-based trips
230
249
  (common for metro/BRT) expand into concrete repetitions under synthetic
231
250
  `{trip_id}#{start_secs}` ids, so every schedule query — arrivals,
@@ -238,6 +257,19 @@ Notes on direct mode:
238
257
  information, `NO_DATA` makes a stop schedule-only, `SKIPPED` suppresses it
239
258
  (and any origin→destination row boarding or alighting there), and the
240
259
  trip-level delay covers stops no StopTimeUpdate reaches.
260
+ - **Rows carry whole records, not copied columns**: `StopArrival`,
261
+ `UpcomingTrip`, and `VehiclePosition` each expose the full `Stop` and
262
+ `Route` they refer to, so platform codes, coordinates, route colours and
263
+ `route_type` are reachable without a second lookup — use
264
+ `route.display_name` / `stop.name` for the human-readable strings. The raw
265
+ `stop_id`/`route_id` stay alongside, since those are what feeds reference
266
+ and what filters key on; a `route_id` naming a route absent from
267
+ `routes.txt` leaves `route` as `None` rather than inventing one.
268
+ - **Vehicle positions name their trip instance**: `VehiclePosition` carries
269
+ the parsed `trip_start_date`/`trip_start_secs`, the pair a GTFS-RT
270
+ `TripDescriptor` uses to address one run. That is what lets two buses out
271
+ on the same headway-based trip be told apart — without it, concurrent
272
+ repetitions of one template trip are indistinguishable.
241
273
  - **The full descriptive surface is exposed, typed by shape**: closed GTFS
242
274
  vocabularies are enums (`WheelchairAccess`, `BikesAllowed`,
243
275
  `PickupDropOffType`, `StopLocationType`, alert cause/effect/severity,
@@ -245,25 +277,72 @@ Notes on direct mode:
245
277
  absent-means-exact default, and open vocabularies (`route_type`,
246
278
  `direction_id`) stay raw ints. Out-of-vocabulary values degrade to `None` —
247
279
  descriptive metadata never fails a build. Consumers decide what to keep.
280
+ - **Numbers are ASCII-strict, and strictness follows the field's role**: a
281
+ GTFS number must be plain ASCII digits, so a Unicode decimal (`٨`), a sign
282
+ prefix, or Python's `1_0` underscore grouping is never quietly reinterpreted
283
+ as a plausible value. A malformed **structural** cell — `stop_times` times,
284
+ `stop_sequence`, `headway_secs`, `exception_type`, calendar weekday flags —
285
+ fails the build, because a wrong departure time or a wrong call order is
286
+ worse than no feed at all. A malformed **descriptive** cell still degrades
287
+ to `None`.
288
+ - **Detours are applied, not annotated**: a GTFS-RT `TripModifications`
289
+ entity REPLACES a span of a trip's calls, so a board that ignored it
290
+ would keep showing departures at stops the vehicle no longer serves, and
291
+ would miss `propagated_modification_delay` on every stop after the
292
+ detour. Replacement times are measured from the reference stop the spec
293
+ names, a replacement stop may be defined only by an RT `Stop` entity, and
294
+ a detour that routes a trip through a stop its static schedule never
295
+ served shows up there.
296
+ - **Schedule over a range**: `scheduled_departures(stops, start, end)` is
297
+ the calendar query — every departure between two instants, schedule only,
298
+ no limit and no grace, rather than `get_arrivals` stretched to days.
299
+ - **Service calendars are answerable**: `services_on(date)` returns the
300
+ service ids running on a GTFS service date — `calendar.txt` resolved
301
+ against `calendar_dates` exceptions, removals winning when a producer
302
+ lists both types for one date — and every row carries its `service_id`,
303
+ so "does this trip run next Tuesday" needs no second arrivals query.
304
+ - **GBFS ids resolve**: `get_vehicle_types()`, `get_pricing_plans()`,
305
+ `get_regions()`, and `get_system_alerts()` turn the `vehicle_type_id`,
306
+ `pricing_plan_id`, and `region_id` on stations and vehicles into records.
307
+ All four documents are optional in GBFS, so an unpublished one is `[]`
308
+ rather than an error.
248
309
  - **Static metadata accessors**: `transit.agencies`, `transit.feed_info`
249
310
  (publisher, version, validity dates — useful for staleness checks), and
250
311
  `headsigns_serving()` alongside the stop/route helpers.
251
- - **Alert scoping**: `ServiceAlert` carries `route_ids`, `stop_ids`, and
252
- `trip_ids`; an alert is agency-wide only when all three are empty.
253
- `is_active(at)` evaluates its active periods.
254
- - **GBFS extras**: `rental_uris` deep links on stations and vehicles,
255
- `get_system_info()`, and TTL-based document caching.
312
+ - **Alert scoping covers every selector GTFS-RT offers**: `ServiceAlert`
313
+ carries `agency_ids`, `route_ids`, `route_types`, `direction_ids`,
314
+ `stop_ids`, and `trip_ids` — an alert is feed-wide only when *all* of them
315
+ are empty. Collecting a subset is not a missing nicety but a correctness
316
+ bug: an alert scoped to one agency, or to a whole mode, would otherwise
317
+ read as applying everywhere. `is_active(at)` evaluates its active periods.
318
+ - **GBFS exposes what the documents carry**: the full station surface
319
+ (`is_installed` alongside `is_renting`/`is_returning` — a station removed
320
+ from the street is not "operational"; disabled counts, address, region,
321
+ virtual-station flag), free-floating vehicle docking and fuel state, and a
322
+ `SystemInfo` with the operator's contact, licensing, and terms URLs.
323
+ `last_reported` is exposed on both stations and vehicles so you can tell
324
+ fresh data from stale, and is parsed from either GBFS 2.x POSIX seconds or
325
+ 3.0 RFC3339. Plus `rental_uris` deep links and TTL-based document caching.
256
326
  - **`cache_dir` strongly recommended**: the static index is cached on disk
257
327
  keyed by feed, validated against the dataset ID, so restarts are instant and
258
328
  rebuilds only happen when the agency publishes a new dataset
259
329
  (`await transit.refresh_static()` — call it daily; it swaps in the new index
260
- only after it's built, so lookups never see a half-built database). Without
261
- a `cache_dir` the index is built in memory on every startup.
330
+ only after it's built, so lookups never see a half-built database, and it is
331
+ safe to call while queries are in flight: the swap waits for in-flight reads
332
+ to finish, and two overlapping refreshes run back to back rather than
333
+ double-building). A corrupt or unreadable cache is treated as a cache miss
334
+ and rebuilt rather than raising. Without a `cache_dir` the index is built in
335
+ memory on every startup.
262
336
  - **Pull, not push**: `TransitFeedHandle`/`GbfsFeedHandle` return snapshots on
263
337
  demand — there's no built-in polling loop or scheduler. Bring your own (e.g.
264
338
  Home Assistant's `DataUpdateCoordinator`).
265
339
  - **`purge_cache()`** deletes a feed's cached static data (or all feeds' when
266
340
  called with no argument) — call it on cleanup/removal of a configured feed.
341
+ A `feed_id` that is not a bare child name of the cache directory raises
342
+ `ValueError` instead of deleting anything, so an absolute path, a `..`
343
+ component, a symlinked entry, or a degenerate `""`/`"."` cannot take out
344
+ every feed's cache. Purging a feed with no cache directory is a no-op, so
345
+ repeated calls are idempotent.
267
346
  `transit.close()` releases a handle's SQLite connection — call it when you
268
347
  are done with a handle; the client's `close()` does not do it for you.
269
348
  - **Producer authentication**: pass `api_key=` to `get_transit_feed()`; it is
@@ -274,6 +353,45 @@ Notes on direct mode:
274
353
  design, with no server-side geo-query.
275
354
  - **Timestamps are tz-aware UTC** throughout the feeds layer.
276
355
 
356
+ ## What this does not expose
357
+
358
+ Deliberate omissions, so you can tell "not supported" from "not found yet".
359
+
360
+ **GTFS files not ingested.** The static index reads `agency.txt`, `stops.txt`,
361
+ `routes.txt`, `trips.txt`, `stop_times.txt`, `calendar.txt`,
362
+ `calendar_dates.txt`, `frequencies.txt`, and `feed_info.txt`. Everything else
363
+ in the zip is ignored:
364
+
365
+ - `shapes.txt` — route geometry. Nothing here draws a line on a map, so trips
366
+ carry no `shape_id`.
367
+ - `transfers.txt` — no transfer rules, minimum transfer times, or
368
+ trip-to-trip continuations, and no journey planning across a transfer.
369
+ - `pathways.txt` / `levels.txt` — no in-station walking graph.
370
+ - `fare_attributes.txt` / `fare_rules.txt` and all GTFS-Fares v2 files — no
371
+ fares, zone pricing, or rider categories. (`Stop.zone_id` is exposed as a
372
+ raw cell, but nothing interprets it.)
373
+ - `translations.txt` — text is returned as the feed wrote it.
374
+
375
+ Service calendars **are** answerable — `services_on(date)` resolves
376
+ `calendar.txt` against `calendar_dates` exceptions, and rows carry their
377
+ `service_id` — but the raw weekday pattern and validity range behind a
378
+ service are not exposed as a model. `scheduled_departures(stops, start, end)`
379
+ covers the range query a calendar view needs.
380
+
381
+ **GTFS-RT: trip shapes.** The RT `shape` entity (a detour's new geometry) is
382
+ not read, for the same reason `shapes.txt` is not: nothing here draws a line
383
+ on a map. Detours themselves ARE applied — see *Detours are applied, not
384
+ annotated* — and the `stop` entities they define are read.
385
+
386
+ **GBFS endpoints not fetched.** `geofencing_zones`, because it is GeoJSON
387
+ polygon geometry while zones here are deliberately circles — supporting it
388
+ means polygon containment, which is a geometry feature rather than another
389
+ document. And `system_hours`, which GBFS 3.0 removed in favour of
390
+ `system_information.opening_hours` (exposed as `SystemInfo.opening_hours`).
391
+
392
+ **Behaviour, not data.** There is no polling loop or scheduler (see *Pull, not
393
+ push*), no write access of any kind, and no cross-feed journey planning.
394
+
277
395
  ## Error handling
278
396
 
279
397
  All errors derive from `MobilityDatabaseError`. The catalog and feeds layers
@@ -290,9 +408,17 @@ each define their own subtree beneath it, so you can catch broadly
290
408
  | `MobilityFeedsError` | feeds | Base class for all feeds errors (subclasses `MobilityDatabaseError`) |
291
409
  | `SourceConnectionError` | feeds | Producer/GBFS endpoint unreachable or errored (`.status` when HTTP) |
292
410
  | `SourceAuthenticationError` | feeds | Producer rejected the feed's `api_key` |
293
- | `FeedParseError` | feeds | Undecodable protobuf, malformed GBFS JSON, or unreadable GTFS zip |
411
+ | `FeedParseError` | feeds | Undecodable protobuf, malformed GBFS JSON or envelope, unreadable GTFS zip, or a malformed scalar in a structural GTFS column |
294
412
  | `StaticDataUnavailableError` | feeds | Feed has no usable hosted static dataset |
295
413
 
414
+ The GBFS and GTFS-RT parse paths are **total** with respect to this table:
415
+ any producer document, however malformed, either returns a typed result or
416
+ raises one of the exceptions above — never a bare `KeyError`, `TypeError`,
417
+ `ValueError` or `OverflowError` from inside the library. The one deliberate
418
+ exception is `purge_cache()`, which raises the builtin `ValueError` for a
419
+ `feed_id` it will not accept (see above), because that is a caller mistake
420
+ rather than a feed problem.
421
+
296
422
  Note the two independent auth flows: a `MobilityDatabaseAuthenticationError`
297
423
  means your **catalog refresh token** is invalid; a `SourceAuthenticationError`
298
424
  means the **producer** rejected the feed-specific `api_key` you passed to
@@ -351,11 +477,27 @@ The suite combines example-based, property-based, and conformance testing:
351
477
  resolver at volume), and `start_date`/`start_time` instance matching over
352
478
  windows containing two service-day instances of the same trip;
353
479
  - the circular-zone geometry (`in_circle`'s bbox prefilter is checked
354
- against the exact haversine distance at generated boundary points);
480
+ against the exact great-circle distance at generated boundary points);
355
481
  - GBFS field parsing (`_localized`, `_version_key`, station/vehicle merging)
356
482
  and GTFS-RT protobuf parsing, both checked for **totality** — every
357
483
  generated input must produce a typed result or a documented
358
484
  `MobilityFeedsError` subclass, never an unhandled exception;
485
+ - the batched arrivals contract: that batching is pure query factorization
486
+ (`get_arrivals([q1, q2])` equals the two single-query calls), that a batch
487
+ costs exactly one realtime fetch however many queries it carries, that
488
+ route and headsign filters apply before the limit, and that every returned
489
+ row's effective departure is still ahead of `now`;
490
+ - the origin→destination query against an oracle built from the stored
491
+ stop-call table: the self-join's direction rule (no return-trip rows) and
492
+ the earliest-destination-call rule for loop trips, neither of which any
493
+ test asserted before;
494
+ - window monotonicity — widening `lookahead` yields a prefix and widening
495
+ `grace` yields a superset — which guards the scan-window regression class
496
+ directly, without a hand-built oracle;
497
+ - the cache as a round trip: a built index and the same index reopened from
498
+ disk answer every query identically, including the stored timezone, and
499
+ `purge_cache` removes exactly the named feed and nothing else for any
500
+ generated `feed_id`;
359
501
  - error-contract fuzzing for malformed GTFS zips, RT payloads, and GBFS
360
502
  documents (garbage bytes, wrong types, out-of-range values) — the
361
503
  contract under test is "raises `FeedParseError`/`SourceConnectionError`",
@@ -80,7 +80,7 @@ aiomobilitydatabase[feeds]`.
80
80
  ```python
81
81
  import asyncio
82
82
 
83
- from aiomobilitydatabase.feeds import Circle, MobilityFeedsClient
83
+ from aiomobilitydatabase.feeds import ArrivalsQuery, Circle, MobilityFeedsClient
84
84
 
85
85
 
86
86
  async def main() -> None:
@@ -113,11 +113,20 @@ async def main() -> None:
113
113
 
114
114
  # Scheduled arrivals merged with realtime (delays, cancellations,
115
115
  # RT-added trips) when the feed publishes GTFS-RT TripUpdates.
116
- arrivals = await transit.get_arrivals([nearby_stops[0].id], limit=2)
116
+ # Pass several ArrivalsQuery objects to get several boards (each
117
+ # with its own stops, route/headsign filters, and limit) from one
118
+ # realtime fetch; the result has one list per query, in order.
119
+ # Delayed trips stay listed until their prediction passes.
120
+ [arrivals] = await transit.get_arrivals(
121
+ [ArrivalsQuery([nearby_stops[0].id], limit=2)]
122
+ )
117
123
  for arrival in arrivals:
118
124
  when = arrival.predicted_departure or arrival.scheduled_departure
119
125
  live = "live" if arrival.realtime else "scheduled"
120
- print(f"{arrival.route_name} -> {arrival.headsign}: {when} ({live})")
126
+ # .stop and .route are the whole static records: platform codes,
127
+ # coordinates, route_type and route colours all come along.
128
+ route = arrival.route.display_name if arrival.route else arrival.route_id
129
+ print(f"{route} -> {arrival.headsign}: {when} ({live})")
121
130
 
122
131
  # Origin→destination: the next departures from stop A on trips that
123
132
  # later reach stop B, with the same realtime overlay. is_first/is_last
@@ -126,7 +135,8 @@ async def main() -> None:
126
135
  nearby_stops[0].id, nearby_stops[1].id, limit=2
127
136
  )
128
137
  for trip in trips:
129
- print(trip.route_name, trip.scheduled_departure, trip.is_last)
138
+ name = trip.route.display_name if trip.route else trip.route_id
139
+ print(name, trip.scheduled_departure, trip.is_last)
130
140
 
131
141
  vehicles = await transit.get_vehicles()
132
142
  alerts = await transit.get_alerts()
@@ -150,7 +160,7 @@ catalog methods, so everything above works unchanged:
150
160
  ```python
151
161
  import asyncio
152
162
 
153
- from aiomobilitydatabase.feeds import MobilityFeedsClient
163
+ from aiomobilitydatabase.feeds import ArrivalsQuery, MobilityFeedsClient
154
164
 
155
165
 
156
166
  async def main() -> None:
@@ -162,7 +172,7 @@ async def main() -> None:
162
172
  vehicle_positions_urls=["https://agency.example/gtfs-rt/positions.pb"],
163
173
  headers={"Authorization": "Bearer PRODUCER_TOKEN"}, # optional
164
174
  )
165
- arrivals = await transit.get_arrivals([transit.stops[0].id])
175
+ [arrivals] = await transit.get_arrivals([ArrivalsQuery([transit.stops[0].id])])
166
176
 
167
177
  bikes = await client.get_gbfs_feed_from_url(
168
178
  "https://bikes.example/gbfs/gbfs.json"
@@ -202,6 +212,15 @@ Notes on direct mode:
202
212
  work with or without realtime coverage; GTFS-RT TripUpdates overlay delays,
203
213
  cancellations, and added trips when present, with cancellation always
204
214
  winning over stale predictions.
215
+ - **Arrivals are batched and delay-aware**: `get_arrivals()` takes a sequence
216
+ of `ArrivalsQuery` and returns one board per query from a single schedule
217
+ read and a single realtime fetch, so several stops with different
218
+ route/headsign filters cost one round trip instead of one each. Each query's
219
+ filters apply *before* its `limit`, and `limit` caps that board as a whole
220
+ rather than per GTFS stop — so a narrow filter can never come back empty
221
+ because busier routes used up the limit. A `grace` window (default one hour)
222
+ keeps a delayed trip listed past its scheduled time until its prediction
223
+ passes, while a schedule-only departure still drops at its scheduled time.
205
224
  - **frequencies.txt is materialized at build time**: headway-based trips
206
225
  (common for metro/BRT) expand into concrete repetitions under synthetic
207
226
  `{trip_id}#{start_secs}` ids, so every schedule query — arrivals,
@@ -214,6 +233,19 @@ Notes on direct mode:
214
233
  information, `NO_DATA` makes a stop schedule-only, `SKIPPED` suppresses it
215
234
  (and any origin→destination row boarding or alighting there), and the
216
235
  trip-level delay covers stops no StopTimeUpdate reaches.
236
+ - **Rows carry whole records, not copied columns**: `StopArrival`,
237
+ `UpcomingTrip`, and `VehiclePosition` each expose the full `Stop` and
238
+ `Route` they refer to, so platform codes, coordinates, route colours and
239
+ `route_type` are reachable without a second lookup — use
240
+ `route.display_name` / `stop.name` for the human-readable strings. The raw
241
+ `stop_id`/`route_id` stay alongside, since those are what feeds reference
242
+ and what filters key on; a `route_id` naming a route absent from
243
+ `routes.txt` leaves `route` as `None` rather than inventing one.
244
+ - **Vehicle positions name their trip instance**: `VehiclePosition` carries
245
+ the parsed `trip_start_date`/`trip_start_secs`, the pair a GTFS-RT
246
+ `TripDescriptor` uses to address one run. That is what lets two buses out
247
+ on the same headway-based trip be told apart — without it, concurrent
248
+ repetitions of one template trip are indistinguishable.
217
249
  - **The full descriptive surface is exposed, typed by shape**: closed GTFS
218
250
  vocabularies are enums (`WheelchairAccess`, `BikesAllowed`,
219
251
  `PickupDropOffType`, `StopLocationType`, alert cause/effect/severity,
@@ -221,25 +253,72 @@ Notes on direct mode:
221
253
  absent-means-exact default, and open vocabularies (`route_type`,
222
254
  `direction_id`) stay raw ints. Out-of-vocabulary values degrade to `None` —
223
255
  descriptive metadata never fails a build. Consumers decide what to keep.
256
+ - **Numbers are ASCII-strict, and strictness follows the field's role**: a
257
+ GTFS number must be plain ASCII digits, so a Unicode decimal (`٨`), a sign
258
+ prefix, or Python's `1_0` underscore grouping is never quietly reinterpreted
259
+ as a plausible value. A malformed **structural** cell — `stop_times` times,
260
+ `stop_sequence`, `headway_secs`, `exception_type`, calendar weekday flags —
261
+ fails the build, because a wrong departure time or a wrong call order is
262
+ worse than no feed at all. A malformed **descriptive** cell still degrades
263
+ to `None`.
264
+ - **Detours are applied, not annotated**: a GTFS-RT `TripModifications`
265
+ entity REPLACES a span of a trip's calls, so a board that ignored it
266
+ would keep showing departures at stops the vehicle no longer serves, and
267
+ would miss `propagated_modification_delay` on every stop after the
268
+ detour. Replacement times are measured from the reference stop the spec
269
+ names, a replacement stop may be defined only by an RT `Stop` entity, and
270
+ a detour that routes a trip through a stop its static schedule never
271
+ served shows up there.
272
+ - **Schedule over a range**: `scheduled_departures(stops, start, end)` is
273
+ the calendar query — every departure between two instants, schedule only,
274
+ no limit and no grace, rather than `get_arrivals` stretched to days.
275
+ - **Service calendars are answerable**: `services_on(date)` returns the
276
+ service ids running on a GTFS service date — `calendar.txt` resolved
277
+ against `calendar_dates` exceptions, removals winning when a producer
278
+ lists both types for one date — and every row carries its `service_id`,
279
+ so "does this trip run next Tuesday" needs no second arrivals query.
280
+ - **GBFS ids resolve**: `get_vehicle_types()`, `get_pricing_plans()`,
281
+ `get_regions()`, and `get_system_alerts()` turn the `vehicle_type_id`,
282
+ `pricing_plan_id`, and `region_id` on stations and vehicles into records.
283
+ All four documents are optional in GBFS, so an unpublished one is `[]`
284
+ rather than an error.
224
285
  - **Static metadata accessors**: `transit.agencies`, `transit.feed_info`
225
286
  (publisher, version, validity dates — useful for staleness checks), and
226
287
  `headsigns_serving()` alongside the stop/route helpers.
227
- - **Alert scoping**: `ServiceAlert` carries `route_ids`, `stop_ids`, and
228
- `trip_ids`; an alert is agency-wide only when all three are empty.
229
- `is_active(at)` evaluates its active periods.
230
- - **GBFS extras**: `rental_uris` deep links on stations and vehicles,
231
- `get_system_info()`, and TTL-based document caching.
288
+ - **Alert scoping covers every selector GTFS-RT offers**: `ServiceAlert`
289
+ carries `agency_ids`, `route_ids`, `route_types`, `direction_ids`,
290
+ `stop_ids`, and `trip_ids` — an alert is feed-wide only when *all* of them
291
+ are empty. Collecting a subset is not a missing nicety but a correctness
292
+ bug: an alert scoped to one agency, or to a whole mode, would otherwise
293
+ read as applying everywhere. `is_active(at)` evaluates its active periods.
294
+ - **GBFS exposes what the documents carry**: the full station surface
295
+ (`is_installed` alongside `is_renting`/`is_returning` — a station removed
296
+ from the street is not "operational"; disabled counts, address, region,
297
+ virtual-station flag), free-floating vehicle docking and fuel state, and a
298
+ `SystemInfo` with the operator's contact, licensing, and terms URLs.
299
+ `last_reported` is exposed on both stations and vehicles so you can tell
300
+ fresh data from stale, and is parsed from either GBFS 2.x POSIX seconds or
301
+ 3.0 RFC3339. Plus `rental_uris` deep links and TTL-based document caching.
232
302
  - **`cache_dir` strongly recommended**: the static index is cached on disk
233
303
  keyed by feed, validated against the dataset ID, so restarts are instant and
234
304
  rebuilds only happen when the agency publishes a new dataset
235
305
  (`await transit.refresh_static()` — call it daily; it swaps in the new index
236
- only after it's built, so lookups never see a half-built database). Without
237
- a `cache_dir` the index is built in memory on every startup.
306
+ only after it's built, so lookups never see a half-built database, and it is
307
+ safe to call while queries are in flight: the swap waits for in-flight reads
308
+ to finish, and two overlapping refreshes run back to back rather than
309
+ double-building). A corrupt or unreadable cache is treated as a cache miss
310
+ and rebuilt rather than raising. Without a `cache_dir` the index is built in
311
+ memory on every startup.
238
312
  - **Pull, not push**: `TransitFeedHandle`/`GbfsFeedHandle` return snapshots on
239
313
  demand — there's no built-in polling loop or scheduler. Bring your own (e.g.
240
314
  Home Assistant's `DataUpdateCoordinator`).
241
315
  - **`purge_cache()`** deletes a feed's cached static data (or all feeds' when
242
316
  called with no argument) — call it on cleanup/removal of a configured feed.
317
+ A `feed_id` that is not a bare child name of the cache directory raises
318
+ `ValueError` instead of deleting anything, so an absolute path, a `..`
319
+ component, a symlinked entry, or a degenerate `""`/`"."` cannot take out
320
+ every feed's cache. Purging a feed with no cache directory is a no-op, so
321
+ repeated calls are idempotent.
243
322
  `transit.close()` releases a handle's SQLite connection — call it when you
244
323
  are done with a handle; the client's `close()` does not do it for you.
245
324
  - **Producer authentication**: pass `api_key=` to `get_transit_feed()`; it is
@@ -250,6 +329,45 @@ Notes on direct mode:
250
329
  design, with no server-side geo-query.
251
330
  - **Timestamps are tz-aware UTC** throughout the feeds layer.
252
331
 
332
+ ## What this does not expose
333
+
334
+ Deliberate omissions, so you can tell "not supported" from "not found yet".
335
+
336
+ **GTFS files not ingested.** The static index reads `agency.txt`, `stops.txt`,
337
+ `routes.txt`, `trips.txt`, `stop_times.txt`, `calendar.txt`,
338
+ `calendar_dates.txt`, `frequencies.txt`, and `feed_info.txt`. Everything else
339
+ in the zip is ignored:
340
+
341
+ - `shapes.txt` — route geometry. Nothing here draws a line on a map, so trips
342
+ carry no `shape_id`.
343
+ - `transfers.txt` — no transfer rules, minimum transfer times, or
344
+ trip-to-trip continuations, and no journey planning across a transfer.
345
+ - `pathways.txt` / `levels.txt` — no in-station walking graph.
346
+ - `fare_attributes.txt` / `fare_rules.txt` and all GTFS-Fares v2 files — no
347
+ fares, zone pricing, or rider categories. (`Stop.zone_id` is exposed as a
348
+ raw cell, but nothing interprets it.)
349
+ - `translations.txt` — text is returned as the feed wrote it.
350
+
351
+ Service calendars **are** answerable — `services_on(date)` resolves
352
+ `calendar.txt` against `calendar_dates` exceptions, and rows carry their
353
+ `service_id` — but the raw weekday pattern and validity range behind a
354
+ service are not exposed as a model. `scheduled_departures(stops, start, end)`
355
+ covers the range query a calendar view needs.
356
+
357
+ **GTFS-RT: trip shapes.** The RT `shape` entity (a detour's new geometry) is
358
+ not read, for the same reason `shapes.txt` is not: nothing here draws a line
359
+ on a map. Detours themselves ARE applied — see *Detours are applied, not
360
+ annotated* — and the `stop` entities they define are read.
361
+
362
+ **GBFS endpoints not fetched.** `geofencing_zones`, because it is GeoJSON
363
+ polygon geometry while zones here are deliberately circles — supporting it
364
+ means polygon containment, which is a geometry feature rather than another
365
+ document. And `system_hours`, which GBFS 3.0 removed in favour of
366
+ `system_information.opening_hours` (exposed as `SystemInfo.opening_hours`).
367
+
368
+ **Behaviour, not data.** There is no polling loop or scheduler (see *Pull, not
369
+ push*), no write access of any kind, and no cross-feed journey planning.
370
+
253
371
  ## Error handling
254
372
 
255
373
  All errors derive from `MobilityDatabaseError`. The catalog and feeds layers
@@ -266,9 +384,17 @@ each define their own subtree beneath it, so you can catch broadly
266
384
  | `MobilityFeedsError` | feeds | Base class for all feeds errors (subclasses `MobilityDatabaseError`) |
267
385
  | `SourceConnectionError` | feeds | Producer/GBFS endpoint unreachable or errored (`.status` when HTTP) |
268
386
  | `SourceAuthenticationError` | feeds | Producer rejected the feed's `api_key` |
269
- | `FeedParseError` | feeds | Undecodable protobuf, malformed GBFS JSON, or unreadable GTFS zip |
387
+ | `FeedParseError` | feeds | Undecodable protobuf, malformed GBFS JSON or envelope, unreadable GTFS zip, or a malformed scalar in a structural GTFS column |
270
388
  | `StaticDataUnavailableError` | feeds | Feed has no usable hosted static dataset |
271
389
 
390
+ The GBFS and GTFS-RT parse paths are **total** with respect to this table:
391
+ any producer document, however malformed, either returns a typed result or
392
+ raises one of the exceptions above — never a bare `KeyError`, `TypeError`,
393
+ `ValueError` or `OverflowError` from inside the library. The one deliberate
394
+ exception is `purge_cache()`, which raises the builtin `ValueError` for a
395
+ `feed_id` it will not accept (see above), because that is a caller mistake
396
+ rather than a feed problem.
397
+
272
398
  Note the two independent auth flows: a `MobilityDatabaseAuthenticationError`
273
399
  means your **catalog refresh token** is invalid; a `SourceAuthenticationError`
274
400
  means the **producer** rejected the feed-specific `api_key` you passed to
@@ -327,11 +453,27 @@ The suite combines example-based, property-based, and conformance testing:
327
453
  resolver at volume), and `start_date`/`start_time` instance matching over
328
454
  windows containing two service-day instances of the same trip;
329
455
  - the circular-zone geometry (`in_circle`'s bbox prefilter is checked
330
- against the exact haversine distance at generated boundary points);
456
+ against the exact great-circle distance at generated boundary points);
331
457
  - GBFS field parsing (`_localized`, `_version_key`, station/vehicle merging)
332
458
  and GTFS-RT protobuf parsing, both checked for **totality** — every
333
459
  generated input must produce a typed result or a documented
334
460
  `MobilityFeedsError` subclass, never an unhandled exception;
461
+ - the batched arrivals contract: that batching is pure query factorization
462
+ (`get_arrivals([q1, q2])` equals the two single-query calls), that a batch
463
+ costs exactly one realtime fetch however many queries it carries, that
464
+ route and headsign filters apply before the limit, and that every returned
465
+ row's effective departure is still ahead of `now`;
466
+ - the origin→destination query against an oracle built from the stored
467
+ stop-call table: the self-join's direction rule (no return-trip rows) and
468
+ the earliest-destination-call rule for loop trips, neither of which any
469
+ test asserted before;
470
+ - window monotonicity — widening `lookahead` yields a prefix and widening
471
+ `grace` yields a superset — which guards the scan-window regression class
472
+ directly, without a hand-built oracle;
473
+ - the cache as a round trip: a built index and the same index reopened from
474
+ disk answer every query identically, including the stored timezone, and
475
+ `purge_cache` removes exactly the named feed and nothing else for any
476
+ generated `feed_id`;
335
477
  - error-contract fuzzing for malformed GTFS zips, RT payloads, and GBFS
336
478
  documents (garbage bytes, wrong types, out-of-range values) — the
337
479
  contract under test is "raises `FeedParseError`/`SourceConnectionError`",
@@ -1,6 +1,6 @@
1
1
  """Constants for aiomobilitydatabase."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.5.0"
4
4
 
5
5
  PROD_BASE_URL = "https://api.mobilitydatabase.org"
6
6
  QA_BASE_URL = "https://api-qa.mobilitydatabase.org"
@@ -26,13 +26,18 @@ from .models import (
26
26
  Agency,
27
27
  AlertCause,
28
28
  AlertEffect,
29
+ AlertImage,
29
30
  AlertSeverity,
31
+ ArrivalsQuery,
30
32
  BikesAllowed,
33
+ CarriageDetail,
31
34
  CongestionLevel,
32
35
  FeedInfo,
36
+ GbfsAlert,
33
37
  GbfsVehicle,
34
38
  OccupancyStatus,
35
39
  PickupDropOffType,
40
+ PricingPlan,
36
41
  Route,
37
42
  ServiceAlert,
38
43
  StaticBuildProgress,
@@ -42,9 +47,11 @@ from .models import (
42
47
  StopArrival,
43
48
  StopLocationType,
44
49
  SystemInfo,
50
+ SystemRegion,
45
51
  UpcomingTrip,
46
52
  VehiclePosition,
47
53
  VehicleStopStatus,
54
+ VehicleType,
48
55
  WheelchairAccess,
49
56
  )
50
57
  from .transit import TransitFeedHandle
@@ -53,18 +60,23 @@ __all__ = [
53
60
  "Agency",
54
61
  "AlertCause",
55
62
  "AlertEffect",
63
+ "AlertImage",
56
64
  "AlertSeverity",
65
+ "ArrivalsQuery",
57
66
  "BikesAllowed",
67
+ "CarriageDetail",
58
68
  "Circle",
59
69
  "CongestionLevel",
60
70
  "FeedInfo",
61
71
  "FeedParseError",
72
+ "GbfsAlert",
62
73
  "GbfsFeedHandle",
63
74
  "GbfsVehicle",
64
75
  "MobilityFeedsClient",
65
76
  "MobilityFeedsError",
66
77
  "OccupancyStatus",
67
78
  "PickupDropOffType",
79
+ "PricingPlan",
68
80
  "Route",
69
81
  "ServiceAlert",
70
82
  "SourceAuthenticationError",
@@ -77,9 +89,11 @@ __all__ = [
77
89
  "StopArrival",
78
90
  "StopLocationType",
79
91
  "SystemInfo",
92
+ "SystemRegion",
80
93
  "TransitFeedHandle",
81
94
  "UpcomingTrip",
82
95
  "VehiclePosition",
83
96
  "VehicleStopStatus",
97
+ "VehicleType",
84
98
  "WheelchairAccess",
85
99
  ]