aiomobilitydatabase 0.2.0__tar.gz → 0.3.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.
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/.pre-commit-config.yaml +3 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/PKG-INFO +135 -21
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/README.md +134 -20
- aiomobilitydatabase-0.3.0/scripts/generate_rt_fixtures.py +507 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/const.py +1 -1
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/feeds/__init__.py +26 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/feeds/client.py +82 -8
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/feeds/gbfs.py +127 -16
- aiomobilitydatabase-0.3.0/src/aiomobilitydatabase/feeds/models.py +501 -0
- aiomobilitydatabase-0.3.0/src/aiomobilitydatabase/feeds/rt.py +655 -0
- aiomobilitydatabase-0.3.0/src/aiomobilitydatabase/feeds/static_index.py +1426 -0
- aiomobilitydatabase-0.3.0/src/aiomobilitydatabase/feeds/transit.py +1190 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/gbfs/discovery_23.json +27 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/gbfs/discovery_30.json +17 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gbfs/free_bike_status_23.json +5 -1
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gbfs/station_info_23.json +6 -1
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gbfs/vehicle_status_30.json +6 -1
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/alerts_trip_scoped.pb +9 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_freq_bare_cancel.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_freq_canceled_tomorrow.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_freq_matched.pb +10 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_freq_unmatched.pb +9 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_both_ends.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_canceled.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_canceled_tomorrow.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_dated_tomorrow_delay.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_dest_arrival.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_no_data_cut.pb +7 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_skip_s1.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_skip_s2.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_skip_s3.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/trip_updates_t1_trip_delay.pb +6 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/rt/vehicle_positions_status.pb +16 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/README.md +39 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/agency.txt +2 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/calendar.txt +3 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/calendar_dates.txt +2 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/fare_attributes.txt +3 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/fare_rules.txt +5 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/feed_info.txt +2 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/frequencies.txt +12 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/routes.txt +7 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/shapes.txt +1 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/stop_times.txt +29 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/stops.txt +10 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/transfers.txt +4 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/translations.txt +3 -0
- aiomobilitydatabase-0.3.0/tests/feeds/data/sample_feed/trips.txt +12 -0
- aiomobilitydatabase-0.3.0/tests/feeds/fixtures.py +247 -0
- aiomobilitydatabase-0.3.0/tests/feeds/test_descriptive_attributes.py +530 -0
- aiomobilitydatabase-0.3.0/tests/feeds/test_direct_urls.py +399 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_gbfs_stations.py +7 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_gbfs_vehicles.py +13 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_models.py +88 -7
- aiomobilitydatabase-0.3.0/tests/feeds/test_properties.py +3984 -0
- aiomobilitydatabase-0.3.0/tests/feeds/test_rt.py +678 -0
- aiomobilitydatabase-0.3.0/tests/feeds/test_sample_feed.py +555 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_static_arrivals.py +22 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_static_build.py +18 -5
- aiomobilitydatabase-0.3.0/tests/feeds/test_static_frequencies.py +232 -0
- aiomobilitydatabase-0.3.0/tests/feeds/test_static_trips.py +172 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_station_grouping.py +11 -2
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_transit_arrivals.py +109 -4
- aiomobilitydatabase-0.3.0/tests/feeds/test_transit_frequencies.py +168 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_transit_resolution.py +1 -1
- aiomobilitydatabase-0.3.0/tests/feeds/test_transit_trips.py +299 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_transit_vehicles_alerts.py +20 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/mock_server.py +17 -1
- aiomobilitydatabase-0.2.0/scripts/generate_rt_fixtures.py +0 -160
- aiomobilitydatabase-0.2.0/src/aiomobilitydatabase/feeds/models.py +0 -170
- aiomobilitydatabase-0.2.0/src/aiomobilitydatabase/feeds/rt.py +0 -302
- aiomobilitydatabase-0.2.0/src/aiomobilitydatabase/feeds/static_index.py +0 -661
- aiomobilitydatabase-0.2.0/src/aiomobilitydatabase/feeds/transit.py +0 -482
- aiomobilitydatabase-0.2.0/tests/feeds/fixtures.py +0 -96
- aiomobilitydatabase-0.2.0/tests/feeds/test_properties.py +0 -1213
- aiomobilitydatabase-0.2.0/tests/feeds/test_rt.py +0 -205
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/.github/workflows/ci.yml +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/.github/workflows/publish.yml +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/.gitignore +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/LICENSE +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/pyproject.toml +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/__init__.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/client.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/exceptions.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/feeds/const.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/feeds/exceptions.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/feeds/geo.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/models.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/src/aiomobilitydatabase/py.typed +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/__init__.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/conftest.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/__init__.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/conftest.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/catalog/gbfs_feed.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/catalog/gtfs_feed.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/catalog/gtfs_rt_feed.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/catalog/token_response.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gbfs/station_status_23.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gbfs/system_info_23.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gbfs/system_info_30.json +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/agency.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/calendar.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/calendar_dates.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/routes.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/stop_times.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/stops.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/gtfs/trips.txt +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/rt/added_trips_s1.pb +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/rt/alerts.pb +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/rt/trip_updates_baseline.pb +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/rt/trip_updates_t1_delayed.pb +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/data/rt/vehicle_positions.pb +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_client.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_exceptions.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_extra_import_guard.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/feeds/test_geo.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/fixtures.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_catalog_properties.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_client_request.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_client_session.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_client_token.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_endpoints.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_exceptions.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_models.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_params.py +0 -0
- {aiomobilitydatabase-0.2.0 → aiomobilitydatabase-0.3.0}/tests/test_public_api.py +0 -0
|
@@ -9,5 +9,8 @@ repos:
|
|
|
9
9
|
rev: v5.0.0
|
|
10
10
|
hooks:
|
|
11
11
|
- id: end-of-file-fixer
|
|
12
|
+
# Keep Google's canonical sample feed byte-identical to upstream.
|
|
13
|
+
exclude: ^tests/feeds/data/sample_feed/.*\.txt$
|
|
12
14
|
- id: trailing-whitespace
|
|
15
|
+
exclude: ^tests/feeds/data/sample_feed/.*\.txt$
|
|
13
16
|
- id: check-yaml
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiomobilitydatabase
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -33,8 +33,9 @@ The core package wraps the catalog's
|
|
|
33
33
|
with a fully typed asyncio client: search feeds, look up their metadata, find the
|
|
34
34
|
hosted dataset URL. An optional `feeds` layer goes one step further and consumes
|
|
35
35
|
the actual transit/bike-share data those feeds point to — scheduled and live
|
|
36
|
-
arrivals, vehicle positions, service alerts,
|
|
37
|
-
a feed ID
|
|
36
|
+
arrivals, origin→destination trip queries, vehicle positions, service alerts,
|
|
37
|
+
GBFS stations and vehicles — giving you a feed ID (or a plain feed URL) in and
|
|
38
|
+
typed snapshots out.
|
|
38
39
|
|
|
39
40
|
## Installation
|
|
40
41
|
|
|
@@ -65,6 +66,9 @@ access tokens automatically (including proactive refresh before expiry). Both
|
|
|
65
66
|
`MobilityDatabaseClient` and `MobilityFeedsClient` take the same refresh token —
|
|
66
67
|
the feeds client wraps a catalog client internally and shares its authentication.
|
|
67
68
|
|
|
69
|
+
The token is only required for catalog operations: `MobilityFeedsClient`'s
|
|
70
|
+
[direct-URL methods](#direct-urls-no-catalog-account) work without one.
|
|
71
|
+
|
|
68
72
|
## Quick start — catalog
|
|
69
73
|
|
|
70
74
|
```python
|
|
@@ -116,10 +120,11 @@ async def main() -> None:
|
|
|
116
120
|
# api_key authenticates with the PRODUCER (distinct from your catalog
|
|
117
121
|
# refresh token); on_progress reports download/index build progress
|
|
118
122
|
# for the first (uncached) fetch of a feed's static dataset.
|
|
123
|
+
# (p.fraction is None when the server doesn't announce a total.)
|
|
119
124
|
transit = await client.get_transit_feed(
|
|
120
125
|
"mdb-100",
|
|
121
126
|
api_key="PRODUCER_API_KEY",
|
|
122
|
-
on_progress=lambda p: print(
|
|
127
|
+
on_progress=lambda p: print(p.phase, p.fraction),
|
|
123
128
|
)
|
|
124
129
|
|
|
125
130
|
# Picker-style helpers: stops in a zone, routes serving a stop.
|
|
@@ -138,6 +143,15 @@ async def main() -> None:
|
|
|
138
143
|
live = "live" if arrival.realtime else "scheduled"
|
|
139
144
|
print(f"{arrival.route_name} -> {arrival.headsign}: {when} ({live})")
|
|
140
145
|
|
|
146
|
+
# Origin→destination: the next departures from stop A on trips that
|
|
147
|
+
# later reach stop B, with the same realtime overlay. is_first/is_last
|
|
148
|
+
# flag the first/last such departure of the service day.
|
|
149
|
+
trips = await transit.upcoming_trips(
|
|
150
|
+
nearby_stops[0].id, nearby_stops[1].id, limit=2
|
|
151
|
+
)
|
|
152
|
+
for trip in trips:
|
|
153
|
+
print(trip.route_name, trip.scheduled_departure, trip.is_last)
|
|
154
|
+
|
|
141
155
|
vehicles = await transit.get_vehicles()
|
|
142
156
|
alerts = await transit.get_alerts()
|
|
143
157
|
|
|
@@ -150,24 +164,108 @@ async def main() -> None:
|
|
|
150
164
|
asyncio.run(main())
|
|
151
165
|
```
|
|
152
166
|
|
|
167
|
+
### Direct URLs (no catalog account)
|
|
168
|
+
|
|
169
|
+
If you already know your feed URLs, you can skip the catalog — and the
|
|
170
|
+
refresh token — entirely. `get_transit_feed_from_urls` and
|
|
171
|
+
`get_gbfs_feed_from_url` return the exact same handle objects as the
|
|
172
|
+
catalog methods, so everything above works unchanged:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import asyncio
|
|
176
|
+
|
|
177
|
+
from aiomobilitydatabase.feeds import MobilityFeedsClient
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def main() -> None:
|
|
181
|
+
# No refresh token: only catalog operations need one.
|
|
182
|
+
async with MobilityFeedsClient(cache_dir="/path/to/cache") as client:
|
|
183
|
+
transit = await client.get_transit_feed_from_urls(
|
|
184
|
+
"https://agency.example/gtfs.zip",
|
|
185
|
+
trip_updates_urls=["https://agency.example/gtfs-rt/trip-updates.pb"],
|
|
186
|
+
vehicle_positions_urls=["https://agency.example/gtfs-rt/positions.pb"],
|
|
187
|
+
headers={"Authorization": "Bearer PRODUCER_TOKEN"}, # optional
|
|
188
|
+
)
|
|
189
|
+
arrivals = await transit.get_arrivals([transit.stops[0].id])
|
|
190
|
+
|
|
191
|
+
bikes = await client.get_gbfs_feed_from_url(
|
|
192
|
+
"https://bikes.example/gbfs/gbfs.json"
|
|
193
|
+
)
|
|
194
|
+
stations = await bikes.get_stations()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
asyncio.run(main())
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Notes on direct mode:
|
|
201
|
+
|
|
202
|
+
- Catalog static datasets are served from the Mobility Database's own
|
|
203
|
+
storage; direct mode fetches **your** URL, with the optional `headers`
|
|
204
|
+
applied to the static download and every GTFS-RT/GBFS fetch the handle
|
|
205
|
+
makes.
|
|
206
|
+
- RT URLs are declared per layer (`trip_updates_urls`,
|
|
207
|
+
`vehicle_positions_urls`, `service_alerts_urls`), so each operation only
|
|
208
|
+
fetches sources that can serve it. A combined feed listed under several
|
|
209
|
+
layers is deduplicated and fetched once per operation; if you don't know
|
|
210
|
+
a producer's layer split, pass the same URL to every layer.
|
|
211
|
+
- Dataset identity comes from HTTP validators (`ETag`, then
|
|
212
|
+
`Last-Modified`, via a `HEAD` probe) instead of catalog dataset IDs;
|
|
213
|
+
servers offering neither fall back to hashing the downloaded bytes, so
|
|
214
|
+
`refresh_static()` still only rebuilds on real changes. The cache key is
|
|
215
|
+
derived from the static URL (`url-<hash>`, exposed as
|
|
216
|
+
`transit.static_feed_id` and accepted by `purge_cache()`), and
|
|
217
|
+
`transit.static_dataset` is `None` (there is no catalog metadata).
|
|
218
|
+
- Accessing `client.catalog` (or any catalog-backed method) on a tokenless
|
|
219
|
+
client raises `MobilityDatabaseError`.
|
|
220
|
+
|
|
153
221
|
## How it works
|
|
154
222
|
|
|
155
223
|
- **Scheduled arrivals for every feed**: the hosted GTFS zip is indexed into
|
|
156
|
-
SQLite (stops, routes, trips, stop_times, calendars
|
|
157
|
-
event loop is never blocked. Arrivals
|
|
158
|
-
GTFS-RT TripUpdates overlay delays,
|
|
159
|
-
present, with cancellation always
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
224
|
+
SQLite (agencies, stops, routes, trips, stop_times, calendars, frequencies,
|
|
225
|
+
feed_info) in a worker thread — the event loop is never blocked. Arrivals
|
|
226
|
+
work with or without realtime coverage; GTFS-RT TripUpdates overlay delays,
|
|
227
|
+
cancellations, and added trips when present, with cancellation always
|
|
228
|
+
winning over stale predictions.
|
|
229
|
+
- **frequencies.txt is materialized at build time**: headway-based trips
|
|
230
|
+
(common for metro/BRT) expand into concrete repetitions under synthetic
|
|
231
|
+
`{trip_id}#{start_secs}` ids, so every schedule query — arrivals,
|
|
232
|
+
origin→destination, routes-serving — sees them as ordinary trips.
|
|
233
|
+
- **Spec-correct realtime matching**: GTFS-RT updates are matched by
|
|
234
|
+
`(trip_id, start_date, start_time)`, so an update addresses exactly one
|
|
235
|
+
repetition of a frequency trip and exactly one service-day instance — a
|
|
236
|
+
"trip X is canceled tomorrow" posting never cancels today's run. A
|
|
237
|
+
StopTimeUpdate's delay propagates to subsequent stops until newer
|
|
238
|
+
information, `NO_DATA` makes a stop schedule-only, `SKIPPED` suppresses it
|
|
239
|
+
(and any origin→destination row boarding or alighting there), and the
|
|
240
|
+
trip-level delay covers stops no StopTimeUpdate reaches.
|
|
241
|
+
- **The full descriptive surface is exposed, typed by shape**: closed GTFS
|
|
242
|
+
vocabularies are enums (`WheelchairAccess`, `BikesAllowed`,
|
|
243
|
+
`PickupDropOffType`, `StopLocationType`, alert cause/effect/severity,
|
|
244
|
+
vehicle status/congestion/occupancy), `timepoint` is a bool with the spec's
|
|
245
|
+
absent-means-exact default, and open vocabularies (`route_type`,
|
|
246
|
+
`direction_id`) stay raw ints. Out-of-vocabulary values degrade to `None` —
|
|
247
|
+
descriptive metadata never fails a build. Consumers decide what to keep.
|
|
248
|
+
- **Static metadata accessors**: `transit.agencies`, `transit.feed_info`
|
|
249
|
+
(publisher, version, validity dates — useful for staleness checks), and
|
|
250
|
+
`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.
|
|
256
|
+
- **`cache_dir` strongly recommended**: the static index is cached on disk
|
|
257
|
+
keyed by feed, validated against the dataset ID, so restarts are instant and
|
|
258
|
+
rebuilds only happen when the agency publishes a new dataset
|
|
259
|
+
(`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.
|
|
166
262
|
- **Pull, not push**: `TransitFeedHandle`/`GbfsFeedHandle` return snapshots on
|
|
167
263
|
demand — there's no built-in polling loop or scheduler. Bring your own (e.g.
|
|
168
264
|
Home Assistant's `DataUpdateCoordinator`).
|
|
169
265
|
- **`purge_cache()`** deletes a feed's cached static data (or all feeds' when
|
|
170
266
|
called with no argument) — call it on cleanup/removal of a configured feed.
|
|
267
|
+
`transit.close()` releases a handle's SQLite connection — call it when you
|
|
268
|
+
are done with a handle; the client's `close()` does not do it for you.
|
|
171
269
|
- **Producer authentication**: pass `api_key=` to `get_transit_feed()`; it is
|
|
172
270
|
applied per the catalog's `authentication_type` (query parameter or header) —
|
|
173
271
|
distinct from the refresh token used to authenticate with the catalog itself.
|
|
@@ -209,21 +307,28 @@ feeds fetches (GTFS-RT polls, GBFS documents, dataset zip downloads) when
|
|
|
209
307
|
passed to `MobilityFeedsClient`:
|
|
210
308
|
|
|
211
309
|
```python
|
|
310
|
+
import asyncio
|
|
311
|
+
|
|
212
312
|
import aiohttp
|
|
213
313
|
|
|
214
314
|
from aiomobilitydatabase import MobilityDatabaseClient
|
|
215
315
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
316
|
+
|
|
317
|
+
async def main() -> None:
|
|
318
|
+
async with aiohttp.ClientSession() as session:
|
|
319
|
+
client = MobilityDatabaseClient("YOUR_REFRESH_TOKEN", session)
|
|
320
|
+
try:
|
|
321
|
+
metadata = await client.get_metadata()
|
|
322
|
+
finally:
|
|
323
|
+
await client.close() # session remains open; you own it
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
asyncio.run(main())
|
|
222
327
|
```
|
|
223
328
|
|
|
224
329
|
## Testing methodology
|
|
225
330
|
|
|
226
|
-
The suite
|
|
331
|
+
The suite combines example-based, property-based, and conformance testing:
|
|
227
332
|
|
|
228
333
|
- **Example-based tests** cover the catalog's endpoint methods and the feeds
|
|
229
334
|
layer's client/transit/GBFS/static-index modules against a real
|
|
@@ -240,6 +345,11 @@ The suite (174 tests) combines example-based and property-based testing:
|
|
|
240
345
|
arithmetic against a naive oracle, calendar/calendar_dates service-id
|
|
241
346
|
resolution, DST-transition handling) — this is where the original
|
|
242
347
|
hardcoded `±1 day` scan-window bug was found;
|
|
348
|
+
- frequencies.txt materialization (arithmetic-progression oracles,
|
|
349
|
+
descriptor carry-through), GTFS-RT delay propagation (an independent
|
|
350
|
+
piecewise oracle over generated StopTimeUpdate sets, run against the pure
|
|
351
|
+
resolver at volume), and `start_date`/`start_time` instance matching over
|
|
352
|
+
windows containing two service-day instances of the same trip;
|
|
243
353
|
- the circular-zone geometry (`in_circle`'s bbox prefilter is checked
|
|
244
354
|
against the exact haversine distance at generated boundary points);
|
|
245
355
|
- GBFS field parsing (`_localized`, `_version_key`, station/vehicle merging)
|
|
@@ -253,6 +363,10 @@ The suite (174 tests) combines example-based and property-based testing:
|
|
|
253
363
|
|
|
254
364
|
Hypothesis is a `dev` dependency and part of the default suite (no separate
|
|
255
365
|
opt-in marker); `.hypothesis/` (its example database) is gitignored.
|
|
366
|
+
- **Conformance tests** (`tests/feeds/test_sample_feed.py`) run the index
|
|
367
|
+
against Google's canonical GTFS sample feed — the feed the rest of the GTFS
|
|
368
|
+
ecosystem validates against — with hand-computed expected schedules,
|
|
369
|
+
frequency repetitions, and calendar exceptions pinned from the raw CSVs.
|
|
256
370
|
|
|
257
371
|
## Development
|
|
258
372
|
|
|
@@ -9,8 +9,9 @@ The core package wraps the catalog's
|
|
|
9
9
|
with a fully typed asyncio client: search feeds, look up their metadata, find the
|
|
10
10
|
hosted dataset URL. An optional `feeds` layer goes one step further and consumes
|
|
11
11
|
the actual transit/bike-share data those feeds point to — scheduled and live
|
|
12
|
-
arrivals, vehicle positions, service alerts,
|
|
13
|
-
a feed ID
|
|
12
|
+
arrivals, origin→destination trip queries, vehicle positions, service alerts,
|
|
13
|
+
GBFS stations and vehicles — giving you a feed ID (or a plain feed URL) in and
|
|
14
|
+
typed snapshots out.
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -41,6 +42,9 @@ access tokens automatically (including proactive refresh before expiry). Both
|
|
|
41
42
|
`MobilityDatabaseClient` and `MobilityFeedsClient` take the same refresh token —
|
|
42
43
|
the feeds client wraps a catalog client internally and shares its authentication.
|
|
43
44
|
|
|
45
|
+
The token is only required for catalog operations: `MobilityFeedsClient`'s
|
|
46
|
+
[direct-URL methods](#direct-urls-no-catalog-account) work without one.
|
|
47
|
+
|
|
44
48
|
## Quick start — catalog
|
|
45
49
|
|
|
46
50
|
```python
|
|
@@ -92,10 +96,11 @@ async def main() -> None:
|
|
|
92
96
|
# api_key authenticates with the PRODUCER (distinct from your catalog
|
|
93
97
|
# refresh token); on_progress reports download/index build progress
|
|
94
98
|
# for the first (uncached) fetch of a feed's static dataset.
|
|
99
|
+
# (p.fraction is None when the server doesn't announce a total.)
|
|
95
100
|
transit = await client.get_transit_feed(
|
|
96
101
|
"mdb-100",
|
|
97
102
|
api_key="PRODUCER_API_KEY",
|
|
98
|
-
on_progress=lambda p: print(
|
|
103
|
+
on_progress=lambda p: print(p.phase, p.fraction),
|
|
99
104
|
)
|
|
100
105
|
|
|
101
106
|
# Picker-style helpers: stops in a zone, routes serving a stop.
|
|
@@ -114,6 +119,15 @@ async def main() -> None:
|
|
|
114
119
|
live = "live" if arrival.realtime else "scheduled"
|
|
115
120
|
print(f"{arrival.route_name} -> {arrival.headsign}: {when} ({live})")
|
|
116
121
|
|
|
122
|
+
# Origin→destination: the next departures from stop A on trips that
|
|
123
|
+
# later reach stop B, with the same realtime overlay. is_first/is_last
|
|
124
|
+
# flag the first/last such departure of the service day.
|
|
125
|
+
trips = await transit.upcoming_trips(
|
|
126
|
+
nearby_stops[0].id, nearby_stops[1].id, limit=2
|
|
127
|
+
)
|
|
128
|
+
for trip in trips:
|
|
129
|
+
print(trip.route_name, trip.scheduled_departure, trip.is_last)
|
|
130
|
+
|
|
117
131
|
vehicles = await transit.get_vehicles()
|
|
118
132
|
alerts = await transit.get_alerts()
|
|
119
133
|
|
|
@@ -126,24 +140,108 @@ async def main() -> None:
|
|
|
126
140
|
asyncio.run(main())
|
|
127
141
|
```
|
|
128
142
|
|
|
143
|
+
### Direct URLs (no catalog account)
|
|
144
|
+
|
|
145
|
+
If you already know your feed URLs, you can skip the catalog — and the
|
|
146
|
+
refresh token — entirely. `get_transit_feed_from_urls` and
|
|
147
|
+
`get_gbfs_feed_from_url` return the exact same handle objects as the
|
|
148
|
+
catalog methods, so everything above works unchanged:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import asyncio
|
|
152
|
+
|
|
153
|
+
from aiomobilitydatabase.feeds import MobilityFeedsClient
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def main() -> None:
|
|
157
|
+
# No refresh token: only catalog operations need one.
|
|
158
|
+
async with MobilityFeedsClient(cache_dir="/path/to/cache") as client:
|
|
159
|
+
transit = await client.get_transit_feed_from_urls(
|
|
160
|
+
"https://agency.example/gtfs.zip",
|
|
161
|
+
trip_updates_urls=["https://agency.example/gtfs-rt/trip-updates.pb"],
|
|
162
|
+
vehicle_positions_urls=["https://agency.example/gtfs-rt/positions.pb"],
|
|
163
|
+
headers={"Authorization": "Bearer PRODUCER_TOKEN"}, # optional
|
|
164
|
+
)
|
|
165
|
+
arrivals = await transit.get_arrivals([transit.stops[0].id])
|
|
166
|
+
|
|
167
|
+
bikes = await client.get_gbfs_feed_from_url(
|
|
168
|
+
"https://bikes.example/gbfs/gbfs.json"
|
|
169
|
+
)
|
|
170
|
+
stations = await bikes.get_stations()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
asyncio.run(main())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Notes on direct mode:
|
|
177
|
+
|
|
178
|
+
- Catalog static datasets are served from the Mobility Database's own
|
|
179
|
+
storage; direct mode fetches **your** URL, with the optional `headers`
|
|
180
|
+
applied to the static download and every GTFS-RT/GBFS fetch the handle
|
|
181
|
+
makes.
|
|
182
|
+
- RT URLs are declared per layer (`trip_updates_urls`,
|
|
183
|
+
`vehicle_positions_urls`, `service_alerts_urls`), so each operation only
|
|
184
|
+
fetches sources that can serve it. A combined feed listed under several
|
|
185
|
+
layers is deduplicated and fetched once per operation; if you don't know
|
|
186
|
+
a producer's layer split, pass the same URL to every layer.
|
|
187
|
+
- Dataset identity comes from HTTP validators (`ETag`, then
|
|
188
|
+
`Last-Modified`, via a `HEAD` probe) instead of catalog dataset IDs;
|
|
189
|
+
servers offering neither fall back to hashing the downloaded bytes, so
|
|
190
|
+
`refresh_static()` still only rebuilds on real changes. The cache key is
|
|
191
|
+
derived from the static URL (`url-<hash>`, exposed as
|
|
192
|
+
`transit.static_feed_id` and accepted by `purge_cache()`), and
|
|
193
|
+
`transit.static_dataset` is `None` (there is no catalog metadata).
|
|
194
|
+
- Accessing `client.catalog` (or any catalog-backed method) on a tokenless
|
|
195
|
+
client raises `MobilityDatabaseError`.
|
|
196
|
+
|
|
129
197
|
## How it works
|
|
130
198
|
|
|
131
199
|
- **Scheduled arrivals for every feed**: the hosted GTFS zip is indexed into
|
|
132
|
-
SQLite (stops, routes, trips, stop_times, calendars
|
|
133
|
-
event loop is never blocked. Arrivals
|
|
134
|
-
GTFS-RT TripUpdates overlay delays,
|
|
135
|
-
present, with cancellation always
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
SQLite (agencies, stops, routes, trips, stop_times, calendars, frequencies,
|
|
201
|
+
feed_info) in a worker thread — the event loop is never blocked. Arrivals
|
|
202
|
+
work with or without realtime coverage; GTFS-RT TripUpdates overlay delays,
|
|
203
|
+
cancellations, and added trips when present, with cancellation always
|
|
204
|
+
winning over stale predictions.
|
|
205
|
+
- **frequencies.txt is materialized at build time**: headway-based trips
|
|
206
|
+
(common for metro/BRT) expand into concrete repetitions under synthetic
|
|
207
|
+
`{trip_id}#{start_secs}` ids, so every schedule query — arrivals,
|
|
208
|
+
origin→destination, routes-serving — sees them as ordinary trips.
|
|
209
|
+
- **Spec-correct realtime matching**: GTFS-RT updates are matched by
|
|
210
|
+
`(trip_id, start_date, start_time)`, so an update addresses exactly one
|
|
211
|
+
repetition of a frequency trip and exactly one service-day instance — a
|
|
212
|
+
"trip X is canceled tomorrow" posting never cancels today's run. A
|
|
213
|
+
StopTimeUpdate's delay propagates to subsequent stops until newer
|
|
214
|
+
information, `NO_DATA` makes a stop schedule-only, `SKIPPED` suppresses it
|
|
215
|
+
(and any origin→destination row boarding or alighting there), and the
|
|
216
|
+
trip-level delay covers stops no StopTimeUpdate reaches.
|
|
217
|
+
- **The full descriptive surface is exposed, typed by shape**: closed GTFS
|
|
218
|
+
vocabularies are enums (`WheelchairAccess`, `BikesAllowed`,
|
|
219
|
+
`PickupDropOffType`, `StopLocationType`, alert cause/effect/severity,
|
|
220
|
+
vehicle status/congestion/occupancy), `timepoint` is a bool with the spec's
|
|
221
|
+
absent-means-exact default, and open vocabularies (`route_type`,
|
|
222
|
+
`direction_id`) stay raw ints. Out-of-vocabulary values degrade to `None` —
|
|
223
|
+
descriptive metadata never fails a build. Consumers decide what to keep.
|
|
224
|
+
- **Static metadata accessors**: `transit.agencies`, `transit.feed_info`
|
|
225
|
+
(publisher, version, validity dates — useful for staleness checks), and
|
|
226
|
+
`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.
|
|
232
|
+
- **`cache_dir` strongly recommended**: the static index is cached on disk
|
|
233
|
+
keyed by feed, validated against the dataset ID, so restarts are instant and
|
|
234
|
+
rebuilds only happen when the agency publishes a new dataset
|
|
235
|
+
(`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.
|
|
142
238
|
- **Pull, not push**: `TransitFeedHandle`/`GbfsFeedHandle` return snapshots on
|
|
143
239
|
demand — there's no built-in polling loop or scheduler. Bring your own (e.g.
|
|
144
240
|
Home Assistant's `DataUpdateCoordinator`).
|
|
145
241
|
- **`purge_cache()`** deletes a feed's cached static data (or all feeds' when
|
|
146
242
|
called with no argument) — call it on cleanup/removal of a configured feed.
|
|
243
|
+
`transit.close()` releases a handle's SQLite connection — call it when you
|
|
244
|
+
are done with a handle; the client's `close()` does not do it for you.
|
|
147
245
|
- **Producer authentication**: pass `api_key=` to `get_transit_feed()`; it is
|
|
148
246
|
applied per the catalog's `authentication_type` (query parameter or header) —
|
|
149
247
|
distinct from the refresh token used to authenticate with the catalog itself.
|
|
@@ -185,21 +283,28 @@ feeds fetches (GTFS-RT polls, GBFS documents, dataset zip downloads) when
|
|
|
185
283
|
passed to `MobilityFeedsClient`:
|
|
186
284
|
|
|
187
285
|
```python
|
|
286
|
+
import asyncio
|
|
287
|
+
|
|
188
288
|
import aiohttp
|
|
189
289
|
|
|
190
290
|
from aiomobilitydatabase import MobilityDatabaseClient
|
|
191
291
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
292
|
+
|
|
293
|
+
async def main() -> None:
|
|
294
|
+
async with aiohttp.ClientSession() as session:
|
|
295
|
+
client = MobilityDatabaseClient("YOUR_REFRESH_TOKEN", session)
|
|
296
|
+
try:
|
|
297
|
+
metadata = await client.get_metadata()
|
|
298
|
+
finally:
|
|
299
|
+
await client.close() # session remains open; you own it
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
asyncio.run(main())
|
|
198
303
|
```
|
|
199
304
|
|
|
200
305
|
## Testing methodology
|
|
201
306
|
|
|
202
|
-
The suite
|
|
307
|
+
The suite combines example-based, property-based, and conformance testing:
|
|
203
308
|
|
|
204
309
|
- **Example-based tests** cover the catalog's endpoint methods and the feeds
|
|
205
310
|
layer's client/transit/GBFS/static-index modules against a real
|
|
@@ -216,6 +321,11 @@ The suite (174 tests) combines example-based and property-based testing:
|
|
|
216
321
|
arithmetic against a naive oracle, calendar/calendar_dates service-id
|
|
217
322
|
resolution, DST-transition handling) — this is where the original
|
|
218
323
|
hardcoded `±1 day` scan-window bug was found;
|
|
324
|
+
- frequencies.txt materialization (arithmetic-progression oracles,
|
|
325
|
+
descriptor carry-through), GTFS-RT delay propagation (an independent
|
|
326
|
+
piecewise oracle over generated StopTimeUpdate sets, run against the pure
|
|
327
|
+
resolver at volume), and `start_date`/`start_time` instance matching over
|
|
328
|
+
windows containing two service-day instances of the same trip;
|
|
219
329
|
- the circular-zone geometry (`in_circle`'s bbox prefilter is checked
|
|
220
330
|
against the exact haversine distance at generated boundary points);
|
|
221
331
|
- GBFS field parsing (`_localized`, `_version_key`, station/vehicle merging)
|
|
@@ -229,6 +339,10 @@ The suite (174 tests) combines example-based and property-based testing:
|
|
|
229
339
|
|
|
230
340
|
Hypothesis is a `dev` dependency and part of the default suite (no separate
|
|
231
341
|
opt-in marker); `.hypothesis/` (its example database) is gitignored.
|
|
342
|
+
- **Conformance tests** (`tests/feeds/test_sample_feed.py`) run the index
|
|
343
|
+
against Google's canonical GTFS sample feed — the feed the rest of the GTFS
|
|
344
|
+
ecosystem validates against — with hand-computed expected schedules,
|
|
345
|
+
frequency repetitions, and calendar exceptions pinned from the raw CSVs.
|
|
232
346
|
|
|
233
347
|
## Development
|
|
234
348
|
|