42Cummer commited on
Commit
876b003
·
verified ·
1 Parent(s): 00f5c2a

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -388
app.py DELETED
@@ -1,388 +0,0 @@
1
- from datetime import datetime # type: ignore
2
- import sys # type: ignore
3
- from pathlib import Path
4
-
5
- # Add parent directory to path to allow imports from api/
6
- sys.path.insert(0, str(Path(__file__).parent.parent))
7
- # Add src directory to path to allow imports from same directory
8
- sys.path.insert(0, str(Path(__file__).parent))
9
-
10
- from api.bus_cache import AsyncBusCache # type: ignore
11
- from api.utils import hms_to_seconds, get_service_day_start_ts, translate_occupancy # type: ignore
12
- from db_manager import init_db # type: ignore
13
- from dotenv import load_dotenv # type: ignore
14
- from fastapi import FastAPI, HTTPException # type: ignore
15
- from fastapi.middleware.cors import CORSMiddleware # type: ignore
16
-
17
- load_dotenv()
18
-
19
- ttc_cache = AsyncBusCache(ttl=20)
20
-
21
- # Initialize database connection globally
22
- db = init_db()
23
-
24
- app = FastAPI(title="WheresMyBus v2.0 API")
25
-
26
- # Setup CORS for your React frontend
27
- app.add_middleware(
28
- CORSMiddleware,
29
- allow_origins=["*"], # In production, use your actual React URL
30
- allow_methods=["*"],
31
- allow_headers=["*"],
32
- )
33
-
34
- @app.get("/")
35
- async def health_check():
36
- """Simple health check endpoint"""
37
- return "backend is running"
38
-
39
- @app.get("/api/vehicles")
40
- async def get_vehicles():
41
- data = await ttc_cache.get_data()
42
- vehicles = data.get("vehicles", [])
43
- return {
44
- "status": "success",
45
- "count": len(vehicles),
46
- "vehicles": vehicles
47
- }
48
-
49
- @app.get("/api/routes")
50
- async def get_all_routes():
51
- """
52
- Returns a complete list of TTC routes with their display names and colors.
53
- """
54
- try:
55
- # Run the query against DuckDB
56
- # We handle missing colors by providing defaults (TTC Red: #FF0000)
57
- query = """
58
- SELECT
59
- route_id,
60
- route_short_name,
61
- route_long_name,
62
- COALESCE(route_color, 'FF0000') as route_color,
63
- COALESCE(route_text_color, 'FFFFFF') as route_text_color
64
- FROM routes
65
- ORDER BY
66
- CASE
67
- WHEN CAST(route_short_name AS VARCHAR) ~ '^[0-9]+$' THEN CAST(route_short_name AS INTEGER)
68
- ELSE 999
69
- END,
70
- route_short_name;
71
- """
72
-
73
- results = db.execute(query).fetchall()
74
-
75
- # Convert to a clean list of dictionaries
76
- route_list = [
77
- {
78
- "id": r[0],
79
- "number": r[1],
80
- "name": r[2],
81
- "color": f"#{r[3]}",
82
- "text_color": f"#{r[4]}"
83
- }
84
- for r in results
85
- ]
86
-
87
- return {
88
- "status": "success",
89
- "count": len(route_list),
90
- "routes": route_list
91
- }
92
-
93
- except Exception as e:
94
- return {"status": "error", "message": str(e)}
95
-
96
- @app.get("/api/routes/{route_id}")
97
- async def get_route_view(route_id: str):
98
- data = await ttc_cache.get_data()
99
- all_buses = data.get("vehicles", [])
100
- route_buses = [v for v in all_buses if v['route'] == route_id]
101
-
102
- if not route_buses:
103
- return {"route": route_id, "vehicles": []}
104
-
105
- # IMPORTANT: Cast Trip IDs to strings to ensure they match the DB
106
- trip_ids = [str(v['trip_id']) for v in route_buses]
107
- placeholders = ','.join(['?'] * len(trip_ids))
108
-
109
- # We use CAST(? AS VARCHAR) to force DuckDB to match strings to strings
110
- query = f"""
111
- SELECT
112
- CAST(st.trip_id AS VARCHAR),
113
- CAST(st.stop_id AS VARCHAR),
114
- st.arrival_time as scheduled_time,
115
- t.trip_headsign
116
- FROM stop_times st
117
- JOIN trips t ON CAST(st.trip_id AS VARCHAR) = CAST(t.trip_id AS VARCHAR)
118
- WHERE CAST(st.trip_id AS VARCHAR) IN ({placeholders})
119
- """
120
- db_rows = db.execute(query, trip_ids).fetchall()
121
-
122
- # Check if we got ANYTHING back from the DB
123
- if not db_rows:
124
- print(f"DEBUG: No matches in DB for Trip IDs: {trip_ids[:3]}")
125
-
126
- schedule_map = {(r[0], r[1]): r[2] for r in db_rows}
127
- name_map = {r[0]: r[3] for r in db_rows}
128
-
129
- service_day_ts = get_service_day_start_ts()
130
- enriched = []
131
-
132
- for bus in route_buses:
133
- # Default delay is 0 if no prediction exists
134
- raw_delay_mins = 0
135
-
136
- pred_time = bus.get('predicted_time')
137
- stop_id = bus.get('next_stop_id')
138
-
139
- if pred_time and stop_id:
140
- sched_hms = schedule_map.get((str(bus['trip_id']), str(stop_id)))
141
- if sched_hms:
142
- # DELAY = SCHEDULED - PREDICTED (negative = late, positive = early)
143
- # Handle GTFS times >= 24 hours (next day)
144
- h, m, s = map(int, sched_hms.split(':'))
145
- extra_days = h // 24
146
- plan_ts = service_day_ts + (extra_days * 86400) + hms_to_seconds(sched_hms)
147
- raw_delay_mins = round((plan_ts - pred_time) / 60)
148
-
149
- enriched.append({
150
- "number": bus['id'],
151
- "name": name_map.get(str(bus['trip_id']), "Not in Schedule"), # This is the destination
152
- "location": {"lat": bus['lat'], "lon": bus['lon']},
153
- "delay_mins": raw_delay_mins, # Actual integer: 5 = 5m late, -2 = 2m early
154
- "fullness": translate_occupancy(bus['occupancy'])
155
- })
156
-
157
- return {
158
- "route": route_id,
159
- "count": len(enriched),
160
- "vehicles": enriched
161
- }
162
-
163
- @app.get("/api/vehicles/{vehicle_id}")
164
- async def get_vehicle_view(vehicle_id: str):
165
- # 1. Pull latest from cache
166
- data = await ttc_cache.get_data()
167
- vehicles = data.get("vehicles", [])
168
-
169
- # 2. Find this specific bus in the list
170
- bus = next((v for v in vehicles if str(v['id']) == vehicle_id), None)
171
-
172
- if not bus:
173
- raise HTTPException(status_code=404, detail="Vehicle not active or not found")
174
-
175
- trip_id = str(bus['trip_id'])
176
- next_stop_id = bus.get('next_stop_id')
177
- predicted_time = bus.get('predicted_time')
178
-
179
- # 3. Handshake with Database (Cast to VARCHAR to avoid type errors)
180
- # We get the destination name and the specific scheduled arrival time
181
- destination = "Not in Schedule"
182
- delay_mins = 0
183
-
184
- if next_stop_id:
185
- query = """
186
- SELECT
187
- t.trip_headsign,
188
- st.arrival_time as scheduled_time
189
- FROM trips t
190
- JOIN stop_times st ON CAST(t.trip_id AS VARCHAR) = CAST(st.trip_id AS VARCHAR)
191
- WHERE CAST(t.trip_id AS VARCHAR) = ?
192
- AND CAST(st.stop_id AS VARCHAR) = ?
193
- LIMIT 1
194
- """
195
- row = db.execute(query, [trip_id, str(next_stop_id)]).fetchone()
196
-
197
- if row:
198
- destination = row[0]
199
- scheduled_hms = row[1]
200
-
201
- # DELAY = SCHEDULED - PREDICTED (negative = late, positive = early)
202
- if predicted_time:
203
- service_day_ts = get_service_day_start_ts()
204
- # Handle GTFS times >= 24 hours (next day)
205
- h, m, s = map(int, scheduled_hms.split(':'))
206
- extra_days = h // 24
207
- plan_ts = service_day_ts + (extra_days * 86400) + hms_to_seconds(scheduled_hms)
208
- delay_mins = round((plan_ts - predicted_time) / 60)
209
- else:
210
- # If no next_stop_id, try to get destination from trip_id only
211
- query = """
212
- SELECT trip_headsign
213
- FROM trips
214
- WHERE CAST(trip_id AS VARCHAR) = ?
215
- LIMIT 1
216
- """
217
- row = db.execute(query, [trip_id]).fetchone()
218
- if row:
219
- destination = row[0]
220
-
221
- return {
222
- "vehicle_number": vehicle_id,
223
- "route_id": bus['route'],
224
- "name": destination,
225
- "location": {
226
- "lat": bus['lat'],
227
- "lon": bus['lon']
228
- },
229
- "delay_mins": delay_mins,
230
- "fullness": translate_occupancy(bus['occupancy']),
231
- "trip_id": trip_id
232
- }
233
-
234
- @app.get("/api/stop/{stop_code}")
235
- async def get_stop_view(stop_code: str):
236
- # 1. Translate Pole Number to Database ID
237
- stop_info = db.execute("SELECT stop_id, stop_name, stop_lat, stop_lon FROM stops WHERE CAST(stop_code AS VARCHAR) = ? LIMIT 1", [str(stop_code)]).fetchone()
238
- if not stop_info:
239
- return {"error": "Stop code not found"}
240
-
241
- target_id = str(stop_info[0])
242
- stop_name = stop_info[1]
243
- stop_lat = stop_info[2]
244
- stop_lon = stop_info[3]
245
-
246
- # 2. Get the Cache structure (dict with vehicles, predictions, alerts)
247
- cached_data = await ttc_cache.get_data()
248
- vehicles_list = cached_data.get("vehicles", [])
249
- predictions = cached_data.get("predictions", {})
250
-
251
- # Build vehicles map for quick lookup
252
- vehicles = {str(v['trip_id']): v for v in vehicles_list}
253
-
254
- from datetime import timezone
255
- now = datetime.now(timezone.utc).timestamp()
256
- two_hours_out = now + 7200
257
- arrivals = []
258
-
259
- # 3. Search the FULL itineraries for our target_id
260
- for trip_id, itinerary in predictions.items():
261
- if target_id in itinerary:
262
- pred_time = itinerary[target_id]
263
-
264
- # Only include if the bus hasn't passed the stop yet and is within 2 hours
265
- if now <= pred_time <= two_hours_out:
266
-
267
- # 4. Handshake with DB for destination and schedule
268
- query = """
269
- SELECT t.trip_headsign, st.arrival_time as scheduled_time, r.route_short_name
270
- FROM trips t
271
- JOIN stop_times st ON CAST(t.trip_id AS VARCHAR) = CAST(st.trip_id AS VARCHAR)
272
- JOIN routes r ON t.route_id = r.route_id
273
- WHERE CAST(t.trip_id AS VARCHAR) = ? AND CAST(st.stop_id AS VARCHAR) = ?
274
- LIMIT 1
275
- """
276
- row = db.execute(query, [trip_id, target_id]).fetchone()
277
-
278
- if row:
279
- # Find the actual bus for fullness (if it's on the road)
280
- bus = vehicles.get(trip_id)
281
-
282
- # DELAY = SCHEDULED - PREDICTED (negative = late, positive = early)
283
- service_day_ts = get_service_day_start_ts()
284
- scheduled_hms = row[1]
285
- # Handle GTFS times >= 24 hours (next day)
286
- h, m, s = map(int, scheduled_hms.split(':'))
287
- extra_days = h // 24
288
- plan_ts = service_day_ts + (extra_days * 86400) + hms_to_seconds(scheduled_hms)
289
-
290
- arrivals.append({
291
- "route": row[2],
292
- "destination": row[0],
293
- "eta_mins": round((pred_time - now) / 60),
294
- "delay_mins": round((plan_ts - pred_time) / 60),
295
- "fullness": translate_occupancy(bus['occupancy']) if bus else "Unknown",
296
- "vehicle_id": bus['id'] if bus else "In Transit"
297
- })
298
-
299
- arrivals.sort(key=lambda x: x['eta_mins'])
300
- return {
301
- "stop_name": stop_name,
302
- "stop_code": stop_code,
303
- "location": {
304
- "lat": stop_lat,
305
- "lon": stop_lon
306
- },
307
- "arrivals": arrivals
308
- }
309
-
310
- @app.get("/api/alerts")
311
- async def get_all_alerts():
312
- """
313
- Returns every active service alert for the entire TTC network.
314
- """
315
- from datetime import timezone
316
- data = await ttc_cache.get_data()
317
- return {
318
- "timestamp": datetime.now(timezone.utc).timestamp(),
319
- "count": len(data["alerts"]),
320
- "alerts": data["alerts"]
321
- }
322
-
323
- @app.get("/api/alerts/{route_id}")
324
- async def get_alerts_for_route(route_id: str):
325
- data = await ttc_cache.get_data()
326
- alerts = data.get("alerts", {})
327
- route_alerts = alerts.get(route_id, [])
328
-
329
- if not route_alerts:
330
- return {
331
- "route_id": route_id,
332
- "count": 0,
333
- "alerts": "No alerts"
334
- }
335
-
336
- return {
337
- "route_id": route_id,
338
- "count": len(route_alerts),
339
- "alerts": route_alerts
340
- }
341
-
342
- @app.get("/api/nearby")
343
- async def get_nearby_context(lat: float, lon: float):
344
- # Bounding box approximation for ~500m radius (fast, no Haversine needed)
345
- # At Toronto's latitude (~43.6°): 1° lat ≈ 111km, 1° lon ≈ 55.4km
346
- # For 500m: lat_range = 0.0045°, lon_range ≈ 0.0072°
347
- lat_range = 0.0045 # ~500m in latitude (constant globally)
348
- lon_range = 0.0072 # ~500m in longitude at Toronto's latitude
349
-
350
- # 1. Find all stops within the bounding box
351
- query_stops = """
352
- SELECT stop_id, stop_code, stop_name, stop_lat, stop_lon
353
- FROM stops
354
- WHERE stop_lat BETWEEN ? AND ?
355
- AND stop_lon BETWEEN ? AND ?
356
- """
357
- stops = db.execute(query_stops, [lat - lat_range, lat + lat_range, lon - lon_range, lon + lon_range]).fetchall()
358
-
359
- stop_ids = [str(s[0]) for s in stops]
360
- if not stop_ids:
361
- return {"stops": [], "routes": []}
362
-
363
- # 2. Find all unique routes serving these specific stops
364
- placeholders = ','.join(['?'] * len(stop_ids))
365
- query_routes = f"""
366
- SELECT DISTINCT r.route_id, r.route_short_name, r.route_long_name, r.route_color
367
- FROM routes r
368
- JOIN trips t ON r.route_id = t.route_id
369
- JOIN stop_times st ON t.trip_id = st.trip_id
370
- WHERE CAST(st.stop_id AS VARCHAR) IN ({placeholders})
371
- """
372
- routes = db.execute(query_routes, stop_ids).fetchall()
373
-
374
- return {
375
- "stops": [
376
- {"id": s[0], "code": s[1], "name": s[2], "lat": s[3], "lon": s[4]}
377
- for s in stops
378
- ],
379
- "routes": [
380
- {"id": r[0], "short_name": r[1], "long_name": r[2], "color": f"#{r[3]}"}
381
- for r in routes
382
- ]
383
- }
384
-
385
- if __name__ == "__main__":
386
- import uvicorn # type: ignore
387
- # Start the server
388
- uvicorn.run(app, host="0.0.0.0", port=7860)