42Cummer commited on
Commit
adde0f1
·
verified ·
1 Parent(s): 4b24c92

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -322
app.py DELETED
@@ -1,322 +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
- plan_ts = service_day_ts + hms_to_seconds(sched_hms)
144
- raw_delay_mins = round((plan_ts - pred_time) / 60)
145
-
146
- enriched.append({
147
- "number": bus['id'],
148
- "name": name_map.get(str(bus['trip_id']), "Not in Schedule"), # This is the destination
149
- "location": {"lat": bus['lat'], "lon": bus['lon']},
150
- "delay_mins": raw_delay_mins, # Actual integer: 5 = 5m late, -2 = 2m early
151
- "fullness": translate_occupancy(bus['occupancy'])
152
- })
153
-
154
- return {
155
- "route": route_id,
156
- "count": len(enriched),
157
- "vehicles": enriched
158
- }
159
-
160
- @app.get("/api/vehicles/{vehicle_id}")
161
- async def get_vehicle_view(vehicle_id: str):
162
- # 1. Pull latest from cache
163
- data = await ttc_cache.get_data()
164
- vehicles = data.get("vehicles", [])
165
-
166
- # 2. Find this specific bus in the list
167
- bus = next((v for v in vehicles if str(v['id']) == vehicle_id), None)
168
-
169
- if not bus:
170
- raise HTTPException(status_code=404, detail="Vehicle not active or not found")
171
-
172
- trip_id = str(bus['trip_id'])
173
- next_stop_id = bus.get('next_stop_id')
174
- predicted_time = bus.get('predicted_time')
175
-
176
- # 3. Handshake with Database (Cast to VARCHAR to avoid type errors)
177
- # We get the destination name and the specific scheduled arrival time
178
- destination = "Not in Schedule"
179
- delay_mins = 0
180
-
181
- if next_stop_id:
182
- query = """
183
- SELECT
184
- t.trip_headsign,
185
- st.arrival_time as scheduled_time
186
- FROM trips t
187
- JOIN stop_times st ON CAST(t.trip_id AS VARCHAR) = CAST(st.trip_id AS VARCHAR)
188
- WHERE CAST(t.trip_id AS VARCHAR) = ?
189
- AND CAST(st.stop_id AS VARCHAR) = ?
190
- LIMIT 1
191
- """
192
- row = db.execute(query, [trip_id, str(next_stop_id)]).fetchone()
193
-
194
- if row:
195
- destination = row[0]
196
- scheduled_hms = row[1]
197
-
198
- # DELAY = SCHEDULED - PREDICTED (negative = late, positive = early)
199
- if predicted_time:
200
- service_day_ts = get_service_day_start_ts()
201
- plan_ts = service_day_ts + hms_to_seconds(scheduled_hms)
202
- delay_mins = round((plan_ts - predicted_time) / 60)
203
- else:
204
- # If no next_stop_id, try to get destination from trip_id only
205
- query = """
206
- SELECT trip_headsign
207
- FROM trips
208
- WHERE CAST(trip_id AS VARCHAR) = ?
209
- LIMIT 1
210
- """
211
- row = db.execute(query, [trip_id]).fetchone()
212
- if row:
213
- destination = row[0]
214
-
215
- return {
216
- "vehicle_number": vehicle_id,
217
- "route_id": bus['route'],
218
- "name": destination,
219
- "location": {
220
- "lat": bus['lat'],
221
- "lon": bus['lon']
222
- },
223
- "delay_mins": delay_mins,
224
- "fullness": translate_occupancy(bus['occupancy']),
225
- "trip_id": trip_id
226
- }
227
-
228
- @app.get("/api/stop/{stop_code}")
229
- async def get_stop_view(stop_code: str):
230
- # 1. Translate Pole Number to Database ID
231
- stop_info = db.execute("SELECT stop_id, stop_name FROM stops WHERE CAST(stop_code AS VARCHAR) = ? LIMIT 1", [str(stop_code)]).fetchone()
232
- if not stop_info:
233
- return {"error": "Stop code not found"}
234
-
235
- target_id = str(stop_info[0])
236
- stop_name = stop_info[1]
237
-
238
- # 2. Get the Cache structure (dict with vehicles, predictions, alerts)
239
- cached_data = await ttc_cache.get_data()
240
- vehicles_list = cached_data.get("vehicles", [])
241
- predictions = cached_data.get("predictions", {})
242
-
243
- # Build vehicles map for quick lookup
244
- vehicles = {str(v['trip_id']): v for v in vehicles_list}
245
-
246
- now = datetime.now().timestamp()
247
- two_hours_out = now + 7200
248
- arrivals = []
249
-
250
- # 3. Search the FULL itineraries for our target_id
251
- for trip_id, itinerary in predictions.items():
252
- if target_id in itinerary:
253
- pred_time = itinerary[target_id]
254
-
255
- # Only include if the bus hasn't passed the stop yet and is within 2 hours
256
- if now <= pred_time <= two_hours_out:
257
-
258
- # 4. Handshake with DB for destination and schedule
259
- query = """
260
- SELECT t.trip_headsign, st.arrival_time as scheduled_time, r.route_short_name
261
- FROM trips t
262
- JOIN stop_times st ON CAST(t.trip_id AS VARCHAR) = CAST(st.trip_id AS VARCHAR)
263
- JOIN routes r ON t.route_id = r.route_id
264
- WHERE CAST(t.trip_id AS VARCHAR) = ? AND CAST(st.stop_id AS VARCHAR) = ?
265
- LIMIT 1
266
- """
267
- row = db.execute(query, [trip_id, target_id]).fetchone()
268
-
269
- if row:
270
- # Find the actual bus for fullness (if it's on the road)
271
- bus = vehicles.get(trip_id)
272
-
273
- # DELAY = SCHEDULED - PREDICTED (negative = late, positive = early)
274
- plan_ts = get_service_day_start_ts() + hms_to_seconds(row[1])
275
-
276
- arrivals.append({
277
- "route": row[2],
278
- "destination": row[0],
279
- "eta_mins": round((pred_time - now) / 60),
280
- "delay_mins": round((plan_ts - pred_time) / 60),
281
- "fullness": translate_occupancy(bus['occupancy']) if bus else "Unknown",
282
- "vehicle_id": bus['id'] if bus else "In Transit"
283
- })
284
-
285
- arrivals.sort(key=lambda x: x['eta_mins'])
286
- return {"stop_name": stop_name, "stop_code": stop_code, "arrivals": arrivals}
287
-
288
- @app.get("/api/alerts")
289
- async def get_all_alerts():
290
- """
291
- Returns every active service alert for the entire TTC network.
292
- """
293
- data = await ttc_cache.get_data()
294
- return {
295
- "timestamp": datetime.now().timestamp(),
296
- "count": len(data["alerts"]),
297
- "alerts": data["alerts"]
298
- }
299
-
300
- @app.get("/api/alerts/{route_id}")
301
- async def get_alerts_for_route(route_id: str):
302
- data = await ttc_cache.get_data()
303
- alerts = data.get("alerts", {})
304
- route_alerts = alerts.get(route_id, [])
305
-
306
- if not route_alerts:
307
- return {
308
- "route_id": route_id,
309
- "count": 0,
310
- "alerts": "No alerts"
311
- }
312
-
313
- return {
314
- "route_id": route_id,
315
- "count": len(route_alerts),
316
- "alerts": route_alerts
317
- }
318
-
319
- if __name__ == "__main__":
320
- import uvicorn # type: ignore
321
- # Start the server
322
- uvicorn.run(app, host="0.0.0.0", port=7860)