aiomobilitydatabase 0.2.1__tar.gz → 0.4.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 (136) hide show
  1. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/.pre-commit-config.yaml +3 -0
  2. aiomobilitydatabase-0.4.0/PKG-INFO +438 -0
  3. aiomobilitydatabase-0.4.0/README.md +414 -0
  4. aiomobilitydatabase-0.4.0/scripts/generate_rt_fixtures.py +507 -0
  5. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/const.py +1 -1
  6. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/feeds/__init__.py +28 -0
  7. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/feeds/client.py +108 -10
  8. aiomobilitydatabase-0.4.0/src/aiomobilitydatabase/feeds/gbfs.py +429 -0
  9. aiomobilitydatabase-0.4.0/src/aiomobilitydatabase/feeds/models.py +531 -0
  10. aiomobilitydatabase-0.4.0/src/aiomobilitydatabase/feeds/rt.py +659 -0
  11. aiomobilitydatabase-0.4.0/src/aiomobilitydatabase/feeds/static_index.py +1546 -0
  12. aiomobilitydatabase-0.4.0/src/aiomobilitydatabase/feeds/transit.py +1370 -0
  13. aiomobilitydatabase-0.4.0/tests/feeds/data/gbfs/discovery_23.json +27 -0
  14. aiomobilitydatabase-0.4.0/tests/feeds/data/gbfs/discovery_30.json +17 -0
  15. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gbfs/free_bike_status_23.json +5 -1
  16. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gbfs/station_info_23.json +6 -1
  17. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gbfs/vehicle_status_30.json +6 -1
  18. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/alerts_trip_scoped.pb +9 -0
  19. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_freq_bare_cancel.pb +6 -0
  20. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_freq_canceled_tomorrow.pb +6 -0
  21. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_freq_matched.pb +10 -0
  22. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_freq_unmatched.pb +9 -0
  23. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_both_ends.pb +6 -0
  24. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_canceled.pb +6 -0
  25. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_canceled_tomorrow.pb +6 -0
  26. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_dated_tomorrow_delay.pb +6 -0
  27. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_dest_arrival.pb +6 -0
  28. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_no_data_cut.pb +7 -0
  29. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_skip_s1.pb +6 -0
  30. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_skip_s2.pb +6 -0
  31. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_skip_s3.pb +6 -0
  32. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/trip_updates_t1_trip_delay.pb +6 -0
  33. aiomobilitydatabase-0.4.0/tests/feeds/data/rt/vehicle_positions_status.pb +16 -0
  34. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/README.md +39 -0
  35. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/agency.txt +2 -0
  36. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/calendar.txt +3 -0
  37. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/calendar_dates.txt +2 -0
  38. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/fare_attributes.txt +3 -0
  39. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/fare_rules.txt +5 -0
  40. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/feed_info.txt +2 -0
  41. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/frequencies.txt +12 -0
  42. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/routes.txt +7 -0
  43. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/shapes.txt +1 -0
  44. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/stop_times.txt +29 -0
  45. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/stops.txt +10 -0
  46. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/transfers.txt +4 -0
  47. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/translations.txt +3 -0
  48. aiomobilitydatabase-0.4.0/tests/feeds/data/sample_feed/trips.txt +12 -0
  49. aiomobilitydatabase-0.4.0/tests/feeds/fixtures.py +247 -0
  50. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_client.py +107 -0
  51. aiomobilitydatabase-0.4.0/tests/feeds/test_descriptive_attributes.py +530 -0
  52. aiomobilitydatabase-0.4.0/tests/feeds/test_direct_urls.py +399 -0
  53. aiomobilitydatabase-0.4.0/tests/feeds/test_gbfs_stations.py +453 -0
  54. aiomobilitydatabase-0.4.0/tests/feeds/test_gbfs_vehicles.py +150 -0
  55. aiomobilitydatabase-0.4.0/tests/feeds/test_models.py +256 -0
  56. aiomobilitydatabase-0.4.0/tests/feeds/test_properties.py +7085 -0
  57. aiomobilitydatabase-0.4.0/tests/feeds/test_rt.py +694 -0
  58. aiomobilitydatabase-0.4.0/tests/feeds/test_sample_feed.py +555 -0
  59. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_static_arrivals.py +56 -0
  60. aiomobilitydatabase-0.4.0/tests/feeds/test_static_build.py +596 -0
  61. aiomobilitydatabase-0.4.0/tests/feeds/test_static_frequencies.py +232 -0
  62. aiomobilitydatabase-0.4.0/tests/feeds/test_static_trips.py +184 -0
  63. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_station_grouping.py +11 -2
  64. aiomobilitydatabase-0.4.0/tests/feeds/test_transit_arrivals.py +482 -0
  65. aiomobilitydatabase-0.4.0/tests/feeds/test_transit_frequencies.py +169 -0
  66. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_transit_resolution.py +1 -1
  67. aiomobilitydatabase-0.4.0/tests/feeds/test_transit_trips.py +339 -0
  68. aiomobilitydatabase-0.4.0/tests/feeds/test_transit_vehicles_alerts.py +579 -0
  69. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/mock_server.py +17 -1
  70. aiomobilitydatabase-0.4.0/tests/test_catalog_properties.py +383 -0
  71. aiomobilitydatabase-0.2.1/PKG-INFO +0 -268
  72. aiomobilitydatabase-0.2.1/README.md +0 -244
  73. aiomobilitydatabase-0.2.1/scripts/generate_rt_fixtures.py +0 -160
  74. aiomobilitydatabase-0.2.1/src/aiomobilitydatabase/feeds/gbfs.py +0 -209
  75. aiomobilitydatabase-0.2.1/src/aiomobilitydatabase/feeds/models.py +0 -170
  76. aiomobilitydatabase-0.2.1/src/aiomobilitydatabase/feeds/rt.py +0 -302
  77. aiomobilitydatabase-0.2.1/src/aiomobilitydatabase/feeds/static_index.py +0 -661
  78. aiomobilitydatabase-0.2.1/src/aiomobilitydatabase/feeds/transit.py +0 -492
  79. aiomobilitydatabase-0.2.1/tests/feeds/fixtures.py +0 -96
  80. aiomobilitydatabase-0.2.1/tests/feeds/test_gbfs_stations.py +0 -198
  81. aiomobilitydatabase-0.2.1/tests/feeds/test_gbfs_vehicles.py +0 -67
  82. aiomobilitydatabase-0.2.1/tests/feeds/test_models.py +0 -135
  83. aiomobilitydatabase-0.2.1/tests/feeds/test_properties.py +0 -1213
  84. aiomobilitydatabase-0.2.1/tests/feeds/test_rt.py +0 -205
  85. aiomobilitydatabase-0.2.1/tests/feeds/test_static_build.py +0 -222
  86. aiomobilitydatabase-0.2.1/tests/feeds/test_transit_arrivals.py +0 -148
  87. aiomobilitydatabase-0.2.1/tests/feeds/test_transit_vehicles_alerts.py +0 -259
  88. aiomobilitydatabase-0.2.1/tests/test_catalog_properties.py +0 -211
  89. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/.github/workflows/ci.yml +0 -0
  90. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/.github/workflows/publish.yml +0 -0
  91. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/.gitignore +0 -0
  92. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/LICENSE +0 -0
  93. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/pyproject.toml +0 -0
  94. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/__init__.py +0 -0
  95. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/client.py +0 -0
  96. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/exceptions.py +0 -0
  97. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/feeds/const.py +0 -0
  98. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/feeds/exceptions.py +0 -0
  99. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/feeds/geo.py +0 -0
  100. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/models.py +0 -0
  101. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/src/aiomobilitydatabase/py.typed +0 -0
  102. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/__init__.py +0 -0
  103. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/conftest.py +0 -0
  104. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/__init__.py +0 -0
  105. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/conftest.py +0 -0
  106. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/catalog/gbfs_feed.json +0 -0
  107. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/catalog/gtfs_feed.json +0 -0
  108. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/catalog/gtfs_rt_feed.json +0 -0
  109. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/catalog/token_response.json +0 -0
  110. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gbfs/station_status_23.json +0 -0
  111. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gbfs/system_info_23.json +0 -0
  112. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gbfs/system_info_30.json +0 -0
  113. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/agency.txt +0 -0
  114. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/calendar.txt +0 -0
  115. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/calendar_dates.txt +0 -0
  116. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/routes.txt +0 -0
  117. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/stop_times.txt +0 -0
  118. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/stops.txt +0 -0
  119. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/gtfs/trips.txt +0 -0
  120. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/rt/added_trips_s1.pb +0 -0
  121. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/rt/alerts.pb +0 -0
  122. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/rt/trip_updates_baseline.pb +0 -0
  123. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/rt/trip_updates_t1_delayed.pb +0 -0
  124. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/data/rt/vehicle_positions.pb +0 -0
  125. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_exceptions.py +0 -0
  126. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_extra_import_guard.py +0 -0
  127. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/feeds/test_geo.py +0 -0
  128. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/fixtures.py +0 -0
  129. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_client_request.py +0 -0
  130. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_client_session.py +0 -0
  131. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_client_token.py +0 -0
  132. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_endpoints.py +0 -0
  133. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_exceptions.py +0 -0
  134. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_models.py +0 -0
  135. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.0}/tests/test_params.py +0 -0
  136. {aiomobilitydatabase-0.2.1 → aiomobilitydatabase-0.4.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
@@ -0,0 +1,438 @@
1
+ Metadata-Version: 2.5
2
+ Name: aiomobilitydatabase
3
+ Version: 0.4.0
4
+ Summary: Async Python client for the Mobility Database catalog API
5
+ Project-URL: Source Code, https://github.com/raman325/aiomobilitydatabase
6
+ Project-URL: Bug Reports, https://github.com/raman325/aiomobilitydatabase/issues
7
+ Author: Raman Gupta
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: asyncio,gbfs,gtfs,gtfs-rt,mobility-database,transit
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: aiohttp>=3.9
20
+ Requires-Dist: mashumaro>=3.13
21
+ Provides-Extra: feeds
22
+ Requires-Dist: gtfs-realtime-bindings>=1.0.0; extra == 'feeds'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # aiomobilitydatabase
26
+
27
+ Async Python client for the [Mobility Database](https://mobilitydatabase.org/) — the
28
+ catalog MobilityData stewards of GTFS, GTFS Realtime, and GBFS feeds from transit
29
+ agencies and bike/scooter share systems around the world.
30
+
31
+ The core package wraps the catalog's
32
+ [REST API](https://mobilitydata.github.io/mobility-feed-api/SwaggerUI/index.html)
33
+ with a fully typed asyncio client: search feeds, look up their metadata, find the
34
+ hosted dataset URL. An optional `feeds` layer goes one step further and consumes
35
+ the actual transit/bike-share data those feeds point to — scheduled and live
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.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install aiomobilitydatabase
44
+ ```
45
+
46
+ That installs the catalog client only. To also consume feed data (schedules,
47
+ GTFS-RT, GBFS), install the `feeds` extra, which pulls in
48
+ [`gtfs-realtime-bindings`](https://pypi.org/project/gtfs-realtime-bindings/) for
49
+ protobuf parsing:
50
+
51
+ ```bash
52
+ pip install aiomobilitydatabase[feeds]
53
+ ```
54
+
55
+ Importing `aiomobilitydatabase.feeds` without the extra installed raises an
56
+ `ImportError` telling you to install it — there is no silent partial-functionality
57
+ mode.
58
+
59
+ Requires Python 3.12+.
60
+
61
+ ## Authentication
62
+
63
+ Sign up at [mobilitydatabase.org](https://mobilitydatabase.org) and copy your
64
+ **refresh token** from the account page. The client exchanges it for short-lived
65
+ access tokens automatically (including proactive refresh before expiry). Both
66
+ `MobilityDatabaseClient` and `MobilityFeedsClient` take the same refresh token —
67
+ the feeds client wraps a catalog client internally and shares its authentication.
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
+
72
+ ## Quick start — catalog
73
+
74
+ ```python
75
+ import asyncio
76
+
77
+ from aiomobilitydatabase import DataType, MobilityDatabaseClient
78
+
79
+
80
+ async def main() -> None:
81
+ async with MobilityDatabaseClient("YOUR_REFRESH_TOKEN") as client:
82
+ # Free-text search across feed name, provider, and location
83
+ results = await client.search_feeds(
84
+ search_query="new york", data_types=[DataType.GTFS_RT], limit=10
85
+ )
86
+ for item in results.results:
87
+ print(item.id, item.provider, item.status)
88
+
89
+ # Full detail for one feed, including the producer URL
90
+ feed = await client.get_gtfs_rt_feed(results.results[0].id)
91
+ assert feed.source_info is not None
92
+ print(feed.source_info.producer_url)
93
+
94
+
95
+ asyncio.run(main())
96
+ ```
97
+
98
+ ## Quick start — feeds (optional extra)
99
+
100
+ `MobilityFeedsClient` resolves a feed ID (GTFS, GTFS-RT, or GBFS) into a handle
101
+ that fetches and caches the actual data. Requires `pip install
102
+ aiomobilitydatabase[feeds]`.
103
+
104
+ ```python
105
+ import asyncio
106
+
107
+ from aiomobilitydatabase.feeds import ArrivalsQuery, Circle, MobilityFeedsClient
108
+
109
+
110
+ async def main() -> None:
111
+ async with MobilityFeedsClient(
112
+ "YOUR_REFRESH_TOKEN", cache_dir="/path/to/cache"
113
+ ) as client:
114
+ # The full catalog client is available via .catalog:
115
+ results = await client.catalog.search_feeds(search_query="portland", limit=5)
116
+ for item in results.results:
117
+ print(item.id, item.provider, item.data_type)
118
+
119
+ # Transit: accepts a GTFS or GTFS-RT feed ID; resolves the sibling.
120
+ # api_key authenticates with the PRODUCER (distinct from your catalog
121
+ # refresh token); on_progress reports download/index build progress
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.)
124
+ transit = await client.get_transit_feed(
125
+ "mdb-100",
126
+ api_key="PRODUCER_API_KEY",
127
+ on_progress=lambda p: print(p.phase, p.fraction),
128
+ )
129
+
130
+ # Picker-style helpers: stops in a zone, routes serving a stop.
131
+ # stations_in collapses GTFS station hierarchies (platforms group
132
+ # under their parent station, entrances are dropped) for clean UIs.
133
+ home = Circle(latitude=45.52, longitude=-122.68, radius_m=800)
134
+ stations = transit.stations_in(home)
135
+ nearby_stops = transit.stops_in(home)
136
+ routes = await transit.routes_serving(stations[0].stop_ids[0])
137
+
138
+ # Scheduled arrivals merged with realtime (delays, cancellations,
139
+ # RT-added trips) when the feed publishes GTFS-RT TripUpdates.
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
+ )
147
+ for arrival in arrivals:
148
+ when = arrival.predicted_departure or arrival.scheduled_departure
149
+ live = "live" if arrival.realtime else "scheduled"
150
+ print(f"{arrival.route_name} -> {arrival.headsign}: {when} ({live})")
151
+
152
+ # Origin→destination: the next departures from stop A on trips that
153
+ # later reach stop B, with the same realtime overlay. is_first/is_last
154
+ # flag the first/last such departure of the service day.
155
+ trips = await transit.upcoming_trips(
156
+ nearby_stops[0].id, nearby_stops[1].id, limit=2
157
+ )
158
+ for trip in trips:
159
+ print(trip.route_name, trip.scheduled_departure, trip.is_last)
160
+
161
+ vehicles = await transit.get_vehicles()
162
+ alerts = await transit.get_alerts()
163
+
164
+ # GBFS bike/scooter share:
165
+ bikes = await client.get_gbfs_feed("gbfs-300")
166
+ stations = await bikes.get_stations(zone=home)
167
+ free_vehicles = await bikes.get_vehicles(zone=home)
168
+
169
+
170
+ asyncio.run(main())
171
+ ```
172
+
173
+ ### Direct URLs (no catalog account)
174
+
175
+ If you already know your feed URLs, you can skip the catalog — and the
176
+ refresh token — entirely. `get_transit_feed_from_urls` and
177
+ `get_gbfs_feed_from_url` return the exact same handle objects as the
178
+ catalog methods, so everything above works unchanged:
179
+
180
+ ```python
181
+ import asyncio
182
+
183
+ from aiomobilitydatabase.feeds import ArrivalsQuery, MobilityFeedsClient
184
+
185
+
186
+ async def main() -> None:
187
+ # No refresh token: only catalog operations need one.
188
+ async with MobilityFeedsClient(cache_dir="/path/to/cache") as client:
189
+ transit = await client.get_transit_feed_from_urls(
190
+ "https://agency.example/gtfs.zip",
191
+ trip_updates_urls=["https://agency.example/gtfs-rt/trip-updates.pb"],
192
+ vehicle_positions_urls=["https://agency.example/gtfs-rt/positions.pb"],
193
+ headers={"Authorization": "Bearer PRODUCER_TOKEN"}, # optional
194
+ )
195
+ [arrivals] = await transit.get_arrivals([ArrivalsQuery([transit.stops[0].id])])
196
+
197
+ bikes = await client.get_gbfs_feed_from_url(
198
+ "https://bikes.example/gbfs/gbfs.json"
199
+ )
200
+ stations = await bikes.get_stations()
201
+
202
+
203
+ asyncio.run(main())
204
+ ```
205
+
206
+ Notes on direct mode:
207
+
208
+ - Catalog static datasets are served from the Mobility Database's own
209
+ storage; direct mode fetches **your** URL, with the optional `headers`
210
+ applied to the static download and every GTFS-RT/GBFS fetch the handle
211
+ makes.
212
+ - RT URLs are declared per layer (`trip_updates_urls`,
213
+ `vehicle_positions_urls`, `service_alerts_urls`), so each operation only
214
+ fetches sources that can serve it. A combined feed listed under several
215
+ layers is deduplicated and fetched once per operation; if you don't know
216
+ a producer's layer split, pass the same URL to every layer.
217
+ - Dataset identity comes from HTTP validators (`ETag`, then
218
+ `Last-Modified`, via a `HEAD` probe) instead of catalog dataset IDs;
219
+ servers offering neither fall back to hashing the downloaded bytes, so
220
+ `refresh_static()` still only rebuilds on real changes. The cache key is
221
+ derived from the static URL (`url-<hash>`, exposed as
222
+ `transit.static_feed_id` and accepted by `purge_cache()`), and
223
+ `transit.static_dataset` is `None` (there is no catalog metadata).
224
+ - Accessing `client.catalog` (or any catalog-backed method) on a tokenless
225
+ client raises `MobilityDatabaseError`.
226
+
227
+ ## How it works
228
+
229
+ - **Scheduled arrivals for every feed**: the hosted GTFS zip is indexed into
230
+ SQLite (agencies, stops, routes, trips, stop_times, calendars, frequencies,
231
+ feed_info) in a worker thread — the event loop is never blocked. Arrivals
232
+ work with or without realtime coverage; GTFS-RT TripUpdates overlay delays,
233
+ cancellations, and added trips when present, with cancellation always
234
+ winning over stale predictions.
235
+ - **Arrivals are batched and delay-aware**: `get_arrivals()` takes a sequence
236
+ of `ArrivalsQuery` and returns one board per query from a single schedule
237
+ read and a single realtime fetch, so several stops with different
238
+ route/headsign filters cost one round trip instead of one each. Each query's
239
+ filters apply *before* its `limit`, and `limit` caps that board as a whole
240
+ rather than per GTFS stop — so a narrow filter can never come back empty
241
+ because busier routes used up the limit. A `grace` window (default one hour)
242
+ keeps a delayed trip listed past its scheduled time until its prediction
243
+ passes, while a schedule-only departure still drops at its scheduled time.
244
+ - **frequencies.txt is materialized at build time**: headway-based trips
245
+ (common for metro/BRT) expand into concrete repetitions under synthetic
246
+ `{trip_id}#{start_secs}` ids, so every schedule query — arrivals,
247
+ origin→destination, routes-serving — sees them as ordinary trips.
248
+ - **Spec-correct realtime matching**: GTFS-RT updates are matched by
249
+ `(trip_id, start_date, start_time)`, so an update addresses exactly one
250
+ repetition of a frequency trip and exactly one service-day instance — a
251
+ "trip X is canceled tomorrow" posting never cancels today's run. A
252
+ StopTimeUpdate's delay propagates to subsequent stops until newer
253
+ information, `NO_DATA` makes a stop schedule-only, `SKIPPED` suppresses it
254
+ (and any origin→destination row boarding or alighting there), and the
255
+ trip-level delay covers stops no StopTimeUpdate reaches.
256
+ - **The full descriptive surface is exposed, typed by shape**: closed GTFS
257
+ vocabularies are enums (`WheelchairAccess`, `BikesAllowed`,
258
+ `PickupDropOffType`, `StopLocationType`, alert cause/effect/severity,
259
+ vehicle status/congestion/occupancy), `timepoint` is a bool with the spec's
260
+ absent-means-exact default, and open vocabularies (`route_type`,
261
+ `direction_id`) stay raw ints. Out-of-vocabulary values degrade to `None` —
262
+ descriptive metadata never fails a build. Consumers decide what to keep.
263
+ - **Numbers are ASCII-strict, and strictness follows the field's role**: a
264
+ GTFS number must be plain ASCII digits, so a Unicode decimal (`٨`), a sign
265
+ prefix, or Python's `1_0` underscore grouping is never quietly reinterpreted
266
+ as a plausible value. A malformed **structural** cell — `stop_times` times,
267
+ `stop_sequence`, `headway_secs`, `exception_type`, calendar weekday flags —
268
+ fails the build, because a wrong departure time or a wrong call order is
269
+ worse than no feed at all. A malformed **descriptive** cell still degrades
270
+ to `None`.
271
+ - **Static metadata accessors**: `transit.agencies`, `transit.feed_info`
272
+ (publisher, version, validity dates — useful for staleness checks), and
273
+ `headsigns_serving()` alongside the stop/route helpers.
274
+ - **Alert scoping**: `ServiceAlert` carries `route_ids`, `stop_ids`, and
275
+ `trip_ids`; an alert is agency-wide only when all three are empty.
276
+ `is_active(at)` evaluates its active periods.
277
+ - **GBFS extras**: `rental_uris` deep links on stations and vehicles,
278
+ `get_system_info()`, and TTL-based document caching.
279
+ - **`cache_dir` strongly recommended**: the static index is cached on disk
280
+ keyed by feed, validated against the dataset ID, so restarts are instant and
281
+ rebuilds only happen when the agency publishes a new dataset
282
+ (`await transit.refresh_static()` — call it daily; it swaps in the new index
283
+ only after it's built, so lookups never see a half-built database, and it is
284
+ safe to call while queries are in flight: the swap waits for in-flight reads
285
+ to finish, and two overlapping refreshes run back to back rather than
286
+ double-building). A corrupt or unreadable cache is treated as a cache miss
287
+ and rebuilt rather than raising. Without a `cache_dir` the index is built in
288
+ memory on every startup.
289
+ - **Pull, not push**: `TransitFeedHandle`/`GbfsFeedHandle` return snapshots on
290
+ demand — there's no built-in polling loop or scheduler. Bring your own (e.g.
291
+ Home Assistant's `DataUpdateCoordinator`).
292
+ - **`purge_cache()`** deletes a feed's cached static data (or all feeds' when
293
+ called with no argument) — call it on cleanup/removal of a configured feed.
294
+ A `feed_id` that is not a bare child name of the cache directory raises
295
+ `ValueError` instead of deleting anything, so an absolute path, a `..`
296
+ component, a symlinked entry, or a degenerate `""`/`"."` cannot take out
297
+ every feed's cache. Purging a feed with no cache directory is a no-op, so
298
+ repeated calls are idempotent.
299
+ `transit.close()` releases a handle's SQLite connection — call it when you
300
+ are done with a handle; the client's `close()` does not do it for you.
301
+ - **Producer authentication**: pass `api_key=` to `get_transit_feed()`; it is
302
+ applied per the catalog's `authentication_type` (query parameter or header) —
303
+ distinct from the refresh token used to authenticate with the catalog itself.
304
+ - **Zones are circles** (matching Home Assistant zone entities); GBFS vehicle
305
+ filtering happens client-side because GBFS feeds are full-system dumps by
306
+ design, with no server-side geo-query.
307
+ - **Timestamps are tz-aware UTC** throughout the feeds layer.
308
+
309
+ ## Error handling
310
+
311
+ All errors derive from `MobilityDatabaseError`. The catalog and feeds layers
312
+ each define their own subtree beneath it, so you can catch broadly
313
+ (`MobilityDatabaseError`) or narrowly per layer:
314
+
315
+ | Exception | Layer | Meaning |
316
+ | --- | --- | --- |
317
+ | `MobilityDatabaseConnectionError` | catalog | Network failure, timeout, or invalid response body |
318
+ | `MobilityDatabaseAuthenticationError` | catalog | Invalid refresh token, or unauthorized after a token refresh |
319
+ | `MobilityDatabaseNotFoundError` | catalog | Resource not found (404) |
320
+ | `MobilityDatabaseRateLimitError` | catalog | Rate limited (429) |
321
+ | `MobilityDatabaseApiError` | catalog | Any other 4xx/5xx (carries `.status` and `.body`) |
322
+ | `MobilityFeedsError` | feeds | Base class for all feeds errors (subclasses `MobilityDatabaseError`) |
323
+ | `SourceConnectionError` | feeds | Producer/GBFS endpoint unreachable or errored (`.status` when HTTP) |
324
+ | `SourceAuthenticationError` | feeds | Producer rejected the feed's `api_key` |
325
+ | `FeedParseError` | feeds | Undecodable protobuf, malformed GBFS JSON or envelope, unreadable GTFS zip, or a malformed scalar in a structural GTFS column |
326
+ | `StaticDataUnavailableError` | feeds | Feed has no usable hosted static dataset |
327
+
328
+ The GBFS and GTFS-RT parse paths are **total** with respect to this table:
329
+ any producer document, however malformed, either returns a typed result or
330
+ raises one of the exceptions above — never a bare `KeyError`, `TypeError`,
331
+ `ValueError` or `OverflowError` from inside the library. The one deliberate
332
+ exception is `purge_cache()`, which raises the builtin `ValueError` for a
333
+ `feed_id` it will not accept (see above), because that is a caller mistake
334
+ rather than a feed problem.
335
+
336
+ Note the two independent auth flows: a `MobilityDatabaseAuthenticationError`
337
+ means your **catalog refresh token** is invalid; a `SourceAuthenticationError`
338
+ means the **producer** rejected the feed-specific `api_key` you passed to
339
+ `get_transit_feed()`. They fail independently and are never confused for one
340
+ another.
341
+
342
+ ## Session injection
343
+
344
+ With an externally managed session (e.g. Home Assistant's shared session), the
345
+ client never closes it. One shared session serves both catalog requests and
346
+ feeds fetches (GTFS-RT polls, GBFS documents, dataset zip downloads) when
347
+ passed to `MobilityFeedsClient`:
348
+
349
+ ```python
350
+ import asyncio
351
+
352
+ import aiohttp
353
+
354
+ from aiomobilitydatabase import MobilityDatabaseClient
355
+
356
+
357
+ async def main() -> None:
358
+ async with aiohttp.ClientSession() as session:
359
+ client = MobilityDatabaseClient("YOUR_REFRESH_TOKEN", session)
360
+ try:
361
+ metadata = await client.get_metadata()
362
+ finally:
363
+ await client.close() # session remains open; you own it
364
+
365
+
366
+ asyncio.run(main())
367
+ ```
368
+
369
+ ## Testing methodology
370
+
371
+ The suite combines example-based, property-based, and conformance testing:
372
+
373
+ - **Example-based tests** cover the catalog's endpoint methods and the feeds
374
+ layer's client/transit/GBFS/static-index modules against a real
375
+ `aiohttp.test_utils.TestServer` running on loopback (`tests/mock_server.py`)
376
+ — scripted per-`(method, path)` responses, with every request recorded for
377
+ assertions on headers, query params, and bodies. This replaces
378
+ [aioresponses](https://github.com/pnuckowski/aioresponses), which cannot
379
+ construct mocked responses under aiohttp >=3.14.
380
+ - **Property-based tests** (`tests/feeds/test_properties.py`, via
381
+ [Hypothesis](https://hypothesis.readthedocs.io/)) target the modules where
382
+ correctness is a matter of an invariant holding across a wide input space
383
+ rather than a handful of hand-picked cases:
384
+ - the static-index date/time math (GTFS elapsed-seconds-since-service-day
385
+ arithmetic against a naive oracle, calendar/calendar_dates service-id
386
+ resolution, DST-transition handling) — this is where the original
387
+ hardcoded `±1 day` scan-window bug was found;
388
+ - frequencies.txt materialization (arithmetic-progression oracles,
389
+ descriptor carry-through), GTFS-RT delay propagation (an independent
390
+ piecewise oracle over generated StopTimeUpdate sets, run against the pure
391
+ resolver at volume), and `start_date`/`start_time` instance matching over
392
+ windows containing two service-day instances of the same trip;
393
+ - the circular-zone geometry (`in_circle`'s bbox prefilter is checked
394
+ against the exact haversine distance at generated boundary points);
395
+ - GBFS field parsing (`_localized`, `_version_key`, station/vehicle merging)
396
+ and GTFS-RT protobuf parsing, both checked for **totality** — every
397
+ generated input must produce a typed result or a documented
398
+ `MobilityFeedsError` subclass, never an unhandled exception;
399
+ - the batched arrivals contract: that batching is pure query factorization
400
+ (`get_arrivals([q1, q2])` equals the two single-query calls), that a batch
401
+ costs exactly one realtime fetch however many queries it carries, that
402
+ route and headsign filters apply before the limit, and that every returned
403
+ row's effective departure is still ahead of `now`;
404
+ - the origin→destination query against an oracle built from the stored
405
+ stop-call table: the self-join's direction rule (no return-trip rows) and
406
+ the earliest-destination-call rule for loop trips, neither of which any
407
+ test asserted before;
408
+ - window monotonicity — widening `lookahead` yields a prefix and widening
409
+ `grace` yields a superset — which guards the scan-window regression class
410
+ directly, without a hand-built oracle;
411
+ - the cache as a round trip: a built index and the same index reopened from
412
+ disk answer every query identically, including the stored timezone, and
413
+ `purge_cache` removes exactly the named feed and nothing else for any
414
+ generated `feed_id`;
415
+ - error-contract fuzzing for malformed GTFS zips, RT payloads, and GBFS
416
+ documents (garbage bytes, wrong types, out-of-range values) — the
417
+ contract under test is "raises `FeedParseError`/`SourceConnectionError`",
418
+ not "does anything in particular with the garbage."
419
+
420
+ Hypothesis is a `dev` dependency and part of the default suite (no separate
421
+ opt-in marker); `.hypothesis/` (its example database) is gitignored.
422
+ - **Conformance tests** (`tests/feeds/test_sample_feed.py`) run the index
423
+ against Google's canonical GTFS sample feed — the feed the rest of the GTFS
424
+ ecosystem validates against — with hand-computed expected schedules,
425
+ frequency repetitions, and calendar exceptions pinned from the raw CSVs.
426
+
427
+ ## Development
428
+
429
+ ```bash
430
+ uv sync --dev
431
+ uv run pytest
432
+ uv run ruff check . && uv run ruff format --check .
433
+ uv run mypy src
434
+ ```
435
+
436
+ ## License
437
+
438
+ [Apache-2.0](LICENSE)