mta-subway 1.0.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.
- mta_subway-1.0.0/.gitignore +23 -0
- mta_subway-1.0.0/PKG-INFO +184 -0
- mta_subway-1.0.0/README.md +159 -0
- mta_subway-1.0.0/app/__init__.py +0 -0
- mta_subway-1.0.0/app/cache.py +61 -0
- mta_subway-1.0.0/app/main.py +126 -0
- mta_subway-1.0.0/app/models.py +38 -0
- mta_subway-1.0.0/app/mta_service.py +227 -0
- mta_subway-1.0.0/app/stations.py +55 -0
- mta_subway-1.0.0/data/stations.json +3623 -0
- mta_subway-1.0.0/mta/__init__.py +1 -0
- mta_subway-1.0.0/mta/cli.py +145 -0
- mta_subway-1.0.0/mta/commands/__init__.py +13 -0
- mta_subway-1.0.0/mta/commands/routes.py +51 -0
- mta_subway-1.0.0/mta/commands/stations.py +78 -0
- mta_subway-1.0.0/mta/commands/trains.py +75 -0
- mta_subway-1.0.0/mta/mcp.py +172 -0
- mta_subway-1.0.0/mta/output.py +102 -0
- mta_subway-1.0.0/pyproject.toml +52 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mta-subway
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Real-time NYC subway arrivals CLI and MCP server
|
|
5
|
+
Project-URL: Homepage, https://github.com/reichertjalex/mta-subway
|
|
6
|
+
Project-URL: Repository, https://github.com/reichertjalex/mta-subway
|
|
7
|
+
Author: Alex Reichert
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: cli,gtfs,mcp,mta,nyc,subway,transit
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: click>=8.1.0
|
|
21
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
|
+
Requires-Dist: nyct-gtfs>=1.3.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# MTA API v3
|
|
27
|
+
|
|
28
|
+
Real-time NYC subway arrivals API with async concurrent feed fetching.
|
|
29
|
+
|
|
30
|
+
## Key Differences from v2
|
|
31
|
+
|
|
32
|
+
- **Concurrent fetching**: All 8 MTA feeds fetched in parallel using `asyncio.to_thread()`
|
|
33
|
+
- **Async cache**: Uses `asyncio.Lock` to prevent concurrent refresh storms
|
|
34
|
+
- **Lazy refresh**: Cache updates on first request after expiry
|
|
35
|
+
|
|
36
|
+
## Performance
|
|
37
|
+
|
|
38
|
+
- Sequential fetch (v2): ~2-4 seconds
|
|
39
|
+
- Concurrent fetch (v3): ~500ms-1s
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m venv venv
|
|
45
|
+
source venv/bin/activate
|
|
46
|
+
pip install -r requirements.txt
|
|
47
|
+
uvicorn app.main:app --reload
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API Endpoints
|
|
51
|
+
|
|
52
|
+
Same as v2 - see `/docs` for OpenAPI documentation.
|
|
53
|
+
|
|
54
|
+
## Docker
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
docker build -t mta-api-v3 .
|
|
58
|
+
docker run -p 8000:8000 mta-api-v3
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Deploy to Fly.io
|
|
62
|
+
|
|
63
|
+
[Fly.io](https://fly.io) is recommended for v3 since it supports persistent servers with the async caching model.
|
|
64
|
+
|
|
65
|
+
### First-time setup
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Install flyctl
|
|
69
|
+
curl -L https://fly.io/install.sh | sh
|
|
70
|
+
|
|
71
|
+
# Login
|
|
72
|
+
fly auth login
|
|
73
|
+
|
|
74
|
+
# Launch app (creates fly.toml)
|
|
75
|
+
cd mta-api-v3
|
|
76
|
+
fly launch --name mta-api-v3 --region ewr --no-deploy
|
|
77
|
+
|
|
78
|
+
# Deploy
|
|
79
|
+
fly deploy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### fly.toml
|
|
83
|
+
|
|
84
|
+
If you need to create it manually:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
app = "mta-api-v3"
|
|
88
|
+
primary_region = "ewr" # Newark, close to MTA servers
|
|
89
|
+
|
|
90
|
+
[build]
|
|
91
|
+
dockerfile = "Dockerfile"
|
|
92
|
+
|
|
93
|
+
[http_service]
|
|
94
|
+
internal_port = 8000
|
|
95
|
+
force_https = true
|
|
96
|
+
|
|
97
|
+
[[http_service.checks]]
|
|
98
|
+
path = "/"
|
|
99
|
+
interval = "30s"
|
|
100
|
+
timeout = "5s"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Useful commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
fly status # Check app status
|
|
107
|
+
fly logs # View logs
|
|
108
|
+
fly ssh console # SSH into the container
|
|
109
|
+
fly scale count 2 # Scale to 2 instances
|
|
110
|
+
fly secrets set KEY=val # Set environment variables
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## CLI
|
|
114
|
+
|
|
115
|
+
Query subway data from the command line. Output is JSON by default.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Setup
|
|
119
|
+
source venv/bin/activate
|
|
120
|
+
|
|
121
|
+
# List all stations
|
|
122
|
+
python -m mta.cli stations
|
|
123
|
+
|
|
124
|
+
# Search stations by name
|
|
125
|
+
python -m mta.cli stations -q "Times Sq"
|
|
126
|
+
|
|
127
|
+
# Get station by ID (with arrivals)
|
|
128
|
+
python -m mta.cli stations 127
|
|
129
|
+
|
|
130
|
+
# Get multiple stations
|
|
131
|
+
python -m mta.cli stations 127,A32
|
|
132
|
+
|
|
133
|
+
# Search by location
|
|
134
|
+
python -m mta.cli stations --lat 40.7580 --lon -73.9855
|
|
135
|
+
|
|
136
|
+
# List all routes
|
|
137
|
+
python -m mta.cli routes
|
|
138
|
+
|
|
139
|
+
# Get stations on a route
|
|
140
|
+
python -m mta.cli routes A
|
|
141
|
+
|
|
142
|
+
# Find next trains from a station
|
|
143
|
+
python -m mta.cli find "Times Sq"
|
|
144
|
+
|
|
145
|
+
# Find specific route from a station
|
|
146
|
+
python -m mta.cli find "Times Sq" -r A
|
|
147
|
+
|
|
148
|
+
# Human-readable output (--pretty goes before the command)
|
|
149
|
+
python -m mta.cli --pretty stations 127
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## MCP Server
|
|
153
|
+
|
|
154
|
+
Run as a Model Context Protocol server for AI assistants:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
python -m mta.mcp
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Available Tools
|
|
161
|
+
|
|
162
|
+
| Tool | Description |
|
|
163
|
+
| -------------- | ----------------------------------- |
|
|
164
|
+
| `get_stations` | Search stations by name or location |
|
|
165
|
+
| `get_station` | Get station(s) by ID with arrivals |
|
|
166
|
+
| `get_routes` | List all active subway routes |
|
|
167
|
+
| `get_route` | Get all stations on a route |
|
|
168
|
+
| `find_train` | Find next trains from a station |
|
|
169
|
+
|
|
170
|
+
### Claude Desktop Configuration
|
|
171
|
+
|
|
172
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"mcpServers": {
|
|
177
|
+
"mta-subway": {
|
|
178
|
+
"command": "python",
|
|
179
|
+
"args": ["-m", "mta.mcp"],
|
|
180
|
+
"cwd": "/path/to/mta-api-v3"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# MTA API v3
|
|
2
|
+
|
|
3
|
+
Real-time NYC subway arrivals API with async concurrent feed fetching.
|
|
4
|
+
|
|
5
|
+
## Key Differences from v2
|
|
6
|
+
|
|
7
|
+
- **Concurrent fetching**: All 8 MTA feeds fetched in parallel using `asyncio.to_thread()`
|
|
8
|
+
- **Async cache**: Uses `asyncio.Lock` to prevent concurrent refresh storms
|
|
9
|
+
- **Lazy refresh**: Cache updates on first request after expiry
|
|
10
|
+
|
|
11
|
+
## Performance
|
|
12
|
+
|
|
13
|
+
- Sequential fetch (v2): ~2-4 seconds
|
|
14
|
+
- Concurrent fetch (v3): ~500ms-1s
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m venv venv
|
|
20
|
+
source venv/bin/activate
|
|
21
|
+
pip install -r requirements.txt
|
|
22
|
+
uvicorn app.main:app --reload
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API Endpoints
|
|
26
|
+
|
|
27
|
+
Same as v2 - see `/docs` for OpenAPI documentation.
|
|
28
|
+
|
|
29
|
+
## Docker
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker build -t mta-api-v3 .
|
|
33
|
+
docker run -p 8000:8000 mta-api-v3
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Deploy to Fly.io
|
|
37
|
+
|
|
38
|
+
[Fly.io](https://fly.io) is recommended for v3 since it supports persistent servers with the async caching model.
|
|
39
|
+
|
|
40
|
+
### First-time setup
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Install flyctl
|
|
44
|
+
curl -L https://fly.io/install.sh | sh
|
|
45
|
+
|
|
46
|
+
# Login
|
|
47
|
+
fly auth login
|
|
48
|
+
|
|
49
|
+
# Launch app (creates fly.toml)
|
|
50
|
+
cd mta-api-v3
|
|
51
|
+
fly launch --name mta-api-v3 --region ewr --no-deploy
|
|
52
|
+
|
|
53
|
+
# Deploy
|
|
54
|
+
fly deploy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### fly.toml
|
|
58
|
+
|
|
59
|
+
If you need to create it manually:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
app = "mta-api-v3"
|
|
63
|
+
primary_region = "ewr" # Newark, close to MTA servers
|
|
64
|
+
|
|
65
|
+
[build]
|
|
66
|
+
dockerfile = "Dockerfile"
|
|
67
|
+
|
|
68
|
+
[http_service]
|
|
69
|
+
internal_port = 8000
|
|
70
|
+
force_https = true
|
|
71
|
+
|
|
72
|
+
[[http_service.checks]]
|
|
73
|
+
path = "/"
|
|
74
|
+
interval = "30s"
|
|
75
|
+
timeout = "5s"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Useful commands
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
fly status # Check app status
|
|
82
|
+
fly logs # View logs
|
|
83
|
+
fly ssh console # SSH into the container
|
|
84
|
+
fly scale count 2 # Scale to 2 instances
|
|
85
|
+
fly secrets set KEY=val # Set environment variables
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## CLI
|
|
89
|
+
|
|
90
|
+
Query subway data from the command line. Output is JSON by default.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Setup
|
|
94
|
+
source venv/bin/activate
|
|
95
|
+
|
|
96
|
+
# List all stations
|
|
97
|
+
python -m mta.cli stations
|
|
98
|
+
|
|
99
|
+
# Search stations by name
|
|
100
|
+
python -m mta.cli stations -q "Times Sq"
|
|
101
|
+
|
|
102
|
+
# Get station by ID (with arrivals)
|
|
103
|
+
python -m mta.cli stations 127
|
|
104
|
+
|
|
105
|
+
# Get multiple stations
|
|
106
|
+
python -m mta.cli stations 127,A32
|
|
107
|
+
|
|
108
|
+
# Search by location
|
|
109
|
+
python -m mta.cli stations --lat 40.7580 --lon -73.9855
|
|
110
|
+
|
|
111
|
+
# List all routes
|
|
112
|
+
python -m mta.cli routes
|
|
113
|
+
|
|
114
|
+
# Get stations on a route
|
|
115
|
+
python -m mta.cli routes A
|
|
116
|
+
|
|
117
|
+
# Find next trains from a station
|
|
118
|
+
python -m mta.cli find "Times Sq"
|
|
119
|
+
|
|
120
|
+
# Find specific route from a station
|
|
121
|
+
python -m mta.cli find "Times Sq" -r A
|
|
122
|
+
|
|
123
|
+
# Human-readable output (--pretty goes before the command)
|
|
124
|
+
python -m mta.cli --pretty stations 127
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## MCP Server
|
|
128
|
+
|
|
129
|
+
Run as a Model Context Protocol server for AI assistants:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python -m mta.mcp
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Available Tools
|
|
136
|
+
|
|
137
|
+
| Tool | Description |
|
|
138
|
+
| -------------- | ----------------------------------- |
|
|
139
|
+
| `get_stations` | Search stations by name or location |
|
|
140
|
+
| `get_station` | Get station(s) by ID with arrivals |
|
|
141
|
+
| `get_routes` | List all active subway routes |
|
|
142
|
+
| `get_route` | Get all stations on a route |
|
|
143
|
+
| `find_train` | Find next trains from a station |
|
|
144
|
+
|
|
145
|
+
### Claude Desktop Configuration
|
|
146
|
+
|
|
147
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"mcpServers": {
|
|
152
|
+
"mta-subway": {
|
|
153
|
+
"command": "python",
|
|
154
|
+
"args": ["-m", "mta.mcp"],
|
|
155
|
+
"cwd": "/path/to/mta-api-v3"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Async TTL cache with lock to prevent concurrent refreshes."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import TypeVar, Generic, Callable, Awaitable
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
TZ = ZoneInfo("America/New_York")
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncTTLCache(Generic[T]):
|
|
13
|
+
"""Thread-safe async cache with TTL expiration."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, ttl_seconds: int = 60):
|
|
16
|
+
self.ttl_seconds = ttl_seconds
|
|
17
|
+
self._value: T | None = None
|
|
18
|
+
self._last_update: datetime | None = None
|
|
19
|
+
self._lock = asyncio.Lock()
|
|
20
|
+
|
|
21
|
+
def is_expired(self) -> bool:
|
|
22
|
+
"""Check if cache has expired."""
|
|
23
|
+
if self._last_update is None:
|
|
24
|
+
return True
|
|
25
|
+
age = datetime.now(TZ) - self._last_update
|
|
26
|
+
return age.total_seconds() > self.ttl_seconds
|
|
27
|
+
|
|
28
|
+
async def get(self) -> T | None:
|
|
29
|
+
"""Get cached value (may be stale)."""
|
|
30
|
+
return self._value
|
|
31
|
+
|
|
32
|
+
async def set(self, value: T) -> None:
|
|
33
|
+
"""Set cached value."""
|
|
34
|
+
async with self._lock:
|
|
35
|
+
self._value = value
|
|
36
|
+
self._last_update = datetime.now(TZ)
|
|
37
|
+
|
|
38
|
+
async def get_or_refresh(
|
|
39
|
+
self, refresh_fn: Callable[[], Awaitable[T]]
|
|
40
|
+
) -> T | None:
|
|
41
|
+
"""Get cached value, refreshing if expired.
|
|
42
|
+
|
|
43
|
+
Uses lock to prevent multiple concurrent refreshes.
|
|
44
|
+
"""
|
|
45
|
+
if not self.is_expired():
|
|
46
|
+
return self._value
|
|
47
|
+
|
|
48
|
+
async with self._lock:
|
|
49
|
+
# Double-check after acquiring lock
|
|
50
|
+
if not self.is_expired():
|
|
51
|
+
return self._value
|
|
52
|
+
|
|
53
|
+
# Refresh
|
|
54
|
+
self._value = await refresh_fn()
|
|
55
|
+
self._last_update = datetime.now(TZ)
|
|
56
|
+
return self._value
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def last_update(self) -> datetime | None:
|
|
60
|
+
"""Get timestamp of last update."""
|
|
61
|
+
return self._last_update
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Async FastAPI application for MTA API v3."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI, Query, HTTPException
|
|
9
|
+
|
|
10
|
+
# Configure logging
|
|
11
|
+
logging.basicConfig(
|
|
12
|
+
level=logging.INFO,
|
|
13
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
14
|
+
)
|
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
16
|
+
|
|
17
|
+
from app.mta_service import AsyncMTAService
|
|
18
|
+
from app.models import StationsResponse, RoutesResponse
|
|
19
|
+
|
|
20
|
+
# Configuration from environment
|
|
21
|
+
STATIONS_FILE = Path(os.getenv("STATIONS_FILE", "data/stations.json"))
|
|
22
|
+
CACHE_SECONDS = int(os.getenv("CACHE_SECONDS", "60"))
|
|
23
|
+
MAX_TRAINS = int(os.getenv("MAX_TRAINS", "10"))
|
|
24
|
+
MAX_MINUTES = int(os.getenv("MAX_MINUTES", "30"))
|
|
25
|
+
|
|
26
|
+
# Global service instance
|
|
27
|
+
mta_service: AsyncMTAService | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@asynccontextmanager
|
|
31
|
+
async def lifespan(app: FastAPI):
|
|
32
|
+
"""Initialize async MTA service on startup."""
|
|
33
|
+
global mta_service
|
|
34
|
+
mta_service = AsyncMTAService(
|
|
35
|
+
stations_file=STATIONS_FILE,
|
|
36
|
+
cache_seconds=CACHE_SECONDS,
|
|
37
|
+
max_trains=MAX_TRAINS,
|
|
38
|
+
max_minutes=MAX_MINUTES,
|
|
39
|
+
)
|
|
40
|
+
yield
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
app = FastAPI(
|
|
44
|
+
title="MTA API v3",
|
|
45
|
+
description="Real-time NYC subway arrivals (async)",
|
|
46
|
+
version="3.0.0",
|
|
47
|
+
lifespan=lifespan,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
app.add_middleware(
|
|
51
|
+
CORSMiddleware,
|
|
52
|
+
allow_origins=["*"],
|
|
53
|
+
allow_credentials=True,
|
|
54
|
+
allow_methods=["*"],
|
|
55
|
+
allow_headers=["*"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.get("/")
|
|
60
|
+
async def root():
|
|
61
|
+
"""API info."""
|
|
62
|
+
return {
|
|
63
|
+
"title": "MTA API v3",
|
|
64
|
+
"description": "Real-time NYC subway arrivals (async with concurrent fetching)",
|
|
65
|
+
"docs": "/docs",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.get("/api/stations", response_model=StationsResponse)
|
|
70
|
+
async def get_stations(
|
|
71
|
+
query: str | None = Query(None, description="Search by station name"),
|
|
72
|
+
latitude: float | None = Query(None, description="Latitude for location search"),
|
|
73
|
+
longitude: float | None = Query(None, description="Longitude for location search"),
|
|
74
|
+
limit: int = Query(10, ge=1, le=100, description="Max results"),
|
|
75
|
+
):
|
|
76
|
+
"""Get stations by name or location."""
|
|
77
|
+
if mta_service is None:
|
|
78
|
+
raise HTTPException(status_code=503, detail="Service not initialized")
|
|
79
|
+
|
|
80
|
+
if latitude is not None and longitude is not None:
|
|
81
|
+
data = await mta_service.get_by_location(latitude, longitude, limit)
|
|
82
|
+
elif query:
|
|
83
|
+
data = await mta_service.get_stations(query, limit)
|
|
84
|
+
else:
|
|
85
|
+
data = await mta_service.get_stations(limit=limit)
|
|
86
|
+
|
|
87
|
+
return StationsResponse(data=data, updated=mta_service.last_update())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.get("/api/stations/{station_id}", response_model=StationsResponse)
|
|
91
|
+
async def get_stations_by_id(station_id: str):
|
|
92
|
+
"""Get station(s) by ID (comma-separated)."""
|
|
93
|
+
if mta_service is None:
|
|
94
|
+
raise HTTPException(status_code=503, detail="Service not initialized")
|
|
95
|
+
|
|
96
|
+
ids = [s.strip() for s in station_id.split(",")]
|
|
97
|
+
data = await mta_service.get_by_ids(ids)
|
|
98
|
+
|
|
99
|
+
if not data:
|
|
100
|
+
raise HTTPException(status_code=404, detail="Station(s) not found")
|
|
101
|
+
|
|
102
|
+
return StationsResponse(data=data, updated=mta_service.last_update())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.get("/api/routes", response_model=RoutesResponse)
|
|
106
|
+
async def get_routes():
|
|
107
|
+
"""Get all active subway routes."""
|
|
108
|
+
if mta_service is None:
|
|
109
|
+
raise HTTPException(status_code=503, detail="Service not initialized")
|
|
110
|
+
|
|
111
|
+
routes = await mta_service.get_routes()
|
|
112
|
+
return RoutesResponse(data=routes, updated=mta_service.last_update())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.get("/api/routes/{route}", response_model=StationsResponse)
|
|
116
|
+
async def get_stations_by_route(route: str):
|
|
117
|
+
"""Get all stations on a route."""
|
|
118
|
+
if mta_service is None:
|
|
119
|
+
raise HTTPException(status_code=503, detail="Service not initialized")
|
|
120
|
+
|
|
121
|
+
data = await mta_service.get_by_route(route)
|
|
122
|
+
|
|
123
|
+
if not data:
|
|
124
|
+
raise HTTPException(status_code=404, detail=f"Route {route} not found")
|
|
125
|
+
|
|
126
|
+
return StationsResponse(data=data, updated=mta_service.last_update())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Pydantic models for API responses."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Train(BaseModel):
|
|
8
|
+
"""A train arrival."""
|
|
9
|
+
|
|
10
|
+
route: str
|
|
11
|
+
time: datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Station(BaseModel):
|
|
15
|
+
"""A subway station with train arrivals."""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
name: str
|
|
19
|
+
location: tuple[float, float]
|
|
20
|
+
routes: list[str]
|
|
21
|
+
N: list[Train] # Northbound/Uptown trains
|
|
22
|
+
S: list[Train] # Southbound/Downtown trains
|
|
23
|
+
stops: dict[str, tuple[float, float]]
|
|
24
|
+
last_update: datetime | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StationsResponse(BaseModel):
|
|
28
|
+
"""API response containing stations."""
|
|
29
|
+
|
|
30
|
+
data: list[Station]
|
|
31
|
+
updated: datetime | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RoutesResponse(BaseModel):
|
|
35
|
+
"""API response containing route list."""
|
|
36
|
+
|
|
37
|
+
data: list[str]
|
|
38
|
+
updated: datetime | None
|