Real-time public transit data (vehicle positions, trip updates, service alerts) via GTFS-Realtime protobuf feeds.
No setup required — defaults to the MBTA (Boston) open feed.
uv run python examples/gtfs_example.pyExpected output:
Polling GTFS feed: https://cdn.mbta.com/realtime/VehiclePositions.pb
Press Ctrl+C to stop.
09:20:31 | Vehicle y3285 | route 66 | STOPPED_AT | (42.3292, -71.0839)
09:20:31 | Vehicle y1863 | route 28 | STOPPED_AT | (42.3291, -71.0839)
09:20:31 | Vehicle y2008 | route 111 | STOPPED_AT | (42.4093, -71.0301)
09:20:31 | Vehicle y3347 | route 116 | IN_TRANSIT_TO | (42.4460, -70.9844)
09:20:31 | Vehicle y1992 | route 8993 | STOPPED_AT | (42.3973, -71.1040)
# .env
# Feed URL — defaults to MBTA (Boston), no auth required
GTFS_FEED_URL=https://cdn.mbta.com/realtime/VehiclePositions.pb
# Short label for this feed, used as event.source
GTFS_FEED_ID=mbta
# API key — leave blank for open feeds, required for MTA / Bay Area 511
GTFS_API_KEY=All three variables are optional when using an open feed.
Both of these were verified working with no API key:
GTFS_FEED_URL=https://cdn.mbta.com/realtime/VehiclePositions.pb
GTFS_FEED_ID=mbta| Feed type | URL |
|---|---|
| Vehicle Positions | https://cdn.mbta.com/realtime/VehiclePositions.pb |
| Trip Updates | https://cdn.mbta.com/realtime/TripUpdates.pb |
| Alerts | https://cdn.mbta.com/realtime/Alerts.pb |
GTFS_FEED_URL=https://gtfsrt.api.translink.com.au/api/realtime/SEQ/VehiclePositions
GTFS_FEED_ID=translink| Feed type | URL |
|---|---|
| Vehicle Positions | https://gtfsrt.api.translink.com.au/api/realtime/SEQ/VehiclePositions |
| Trip Updates | https://gtfsrt.api.translink.com.au/api/realtime/SEQ/TripUpdates |
| Agency | Auth | Setup instructions |
|---|---|---|
| NYC MTA | Free key | See SETUP.md |
| Bay Area 511 | Free key | See SETUP.md |
| LA Metro | Swiftly API key | See developer.metro.net/api |
from streams.gtfs import GTFSStream, VehiclePosition, TripUpdate, ServiceAlert
stream = GTFSStream()
await stream.start()
async for event in stream:
entity = event.payload
if isinstance(entity, VehiclePosition):
print(f"Vehicle {entity.vehicle_id} on route {entity.route_id}")
if entity.position:
print(f" at ({entity.position.latitude:.4f}, {entity.position.longitude:.4f})")
elif isinstance(entity, TripUpdate):
print(f"Trip {entity.trip_id}: {len(entity.stop_time_updates)} stop updates")
elif isinstance(entity, ServiceAlert):
print(f"Alert [{entity.effect}]: {entity.header}")To use a non-default feed without changing .env, pass config directly:
from streams.config import GTFSConfig
from streams.gtfs import GTFSStream
stream = GTFSStream(config=GTFSConfig(
feed_url="https://gtfsrt.api.translink.com.au/api/realtime/SEQ/VehiclePositions",
feed_id="translink",
))| Field | Type | Description |
|---|---|---|
feed_id |
str |
Feed identifier (from GTFS_FEED_ID) |
vehicle_id |
str |
Vehicle identifier |
trip_id |
str | None |
Current trip |
route_id |
str | None |
Route |
position |
Position | None |
Lat/lon/bearing/speed |
current_status |
str | None |
INCOMING_AT, STOPPED_AT, IN_TRANSIT_TO |
timestamp |
datetime | None |
Position timestamp |
occupancy_status |
str | None |
EMPTY, MANY_SEATS_AVAILABLE, FULL, etc. |
| Field | Type | Description |
|---|---|---|
feed_id |
str |
Feed identifier |
trip_id |
str |
Trip identifier |
route_id |
str | None |
Route |
vehicle_id |
str | None |
Vehicle |
stop_time_updates |
tuple[StopTimeUpdate] |
Per-stop arrival/departure delays |
timestamp |
datetime | None |
Feed timestamp |
| Field | Type | Description |
|---|---|---|
feed_id |
str |
Feed identifier |
alert_id |
str |
Alert identifier |
cause |
str | None |
STRIKE, WEATHER, MAINTENANCE, etc. |
effect |
str | None |
NO_SERVICE, DETOUR, SIGNIFICANT_DELAYS, etc. |
header |
str |
Short alert text |
description |
str |
Full alert text |
active_period |
tuple |
(start, end) unix timestamp pairs |
route_ids |
tuple[str] |
Affected routes |
stop_ids |
tuple[str] |
Affected stops |
- Default poll interval is 30 seconds
- Entities are deduplicated across polls by vehicle/trip ID + timestamp
- The
GTFSStreampolls a single feed URL; run multiple instances for multiple feed types - Use
isinstance()checks to handle the three entity types