Add Flask API endpoints and testing scripts for safety analysis
- Created requirements.txt for Flask and related libraries. - Implemented test_api.py to validate API endpoints including health check, weather data retrieval, crash analysis, route finding, and single route fetching. - Developed test_crash_endpoint.py for focused testing on crash analysis endpoint. - Added test_flask_endpoints.py for lightweight tests using Flask's test client with mocked dependencies. - Introduced SafetyAnalysisModal component in the frontend for displaying detailed safety analysis results. - Implemented flaskApi.ts to handle API requests for weather data and crash analysis, including data transformation to match frontend interfaces.
This commit is contained in:
285
llm/test_flask_endpoints.py
Normal file
285
llm/test_flask_endpoints.py
Normal file
@@ -0,0 +1,285 @@
|
||||
"""
|
||||
Lightweight tests for llm/flask_server.py endpoints.
|
||||
This script injects fake local modules to avoid external network/DB calls,
|
||||
imports the flask app, and uses Flask's test_client to call endpoints.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import types
|
||||
import json
|
||||
import traceback
|
||||
|
||||
# Prepare fake modules to avoid external dependencies (MongoDB, external APIs, LLMs)
|
||||
fake_mongo = types.ModuleType("gemini_mongo_mateo")
|
||||
|
||||
def fake_connect_to_mongodb():
|
||||
# Return a simple truthy object representing a connection/collection
|
||||
return {"fake": "collection"}
|
||||
|
||||
def fake_get_crashes_within_radius_mongodb(collection, lat, lon, radius_km):
|
||||
return []
|
||||
|
||||
def fake_analyze_mongodb_crash_patterns(crashes, lat, lon, radius_km, weather_summary=None):
|
||||
return "No crash data available (fake)"
|
||||
|
||||
def fake_get_current_weather(lat, lon):
|
||||
return ({"temp": 20, "weather_code": 0}, "Clear sky")
|
||||
|
||||
fake_mongo.connect_to_mongodb = fake_connect_to_mongodb
|
||||
fake_mongo.get_crashes_within_radius_mongodb = fake_get_crashes_within_radius_mongodb
|
||||
fake_mongo.analyze_mongodb_crash_patterns = fake_analyze_mongodb_crash_patterns
|
||||
fake_mongo.get_current_weather = fake_get_current_weather
|
||||
fake_mongo.MONGO_URI = "mongodb://fake"
|
||||
|
||||
# Fake reroute module
|
||||
fake_reroute = types.ModuleType("gemini_reroute_mateo")
|
||||
|
||||
class FakeSafeRouteAnalyzer:
|
||||
def __init__(self, uri):
|
||||
self.collection = {"fake": "collection"}
|
||||
|
||||
def find_safer_route(self, start_lat, start_lon, end_lat, end_lon):
|
||||
return {
|
||||
'recommended_route': {
|
||||
'route_data': {
|
||||
'coordinates': [[start_lat, start_lon], [end_lat, end_lon]],
|
||||
'distance_km': 1.23,
|
||||
'duration_min': 4.5,
|
||||
'geometry': None
|
||||
},
|
||||
'safety_analysis': {
|
||||
'average_safety_score': 0.0,
|
||||
'total_crashes_near_route': 0,
|
||||
'max_danger_score': 0.0
|
||||
}
|
||||
},
|
||||
'safety_report': 'All good (fake)',
|
||||
'alternative_routes': []
|
||||
}
|
||||
|
||||
def get_route_from_mapbox(self, start_lat, start_lon, end_lat, end_lon, profile='driving'):
|
||||
return {
|
||||
'success': True,
|
||||
'coordinates': [[start_lat, start_lon], [end_lat, end_lon]],
|
||||
'distance_km': 1.0,
|
||||
'duration_min': 3.0,
|
||||
'geometry': None
|
||||
}
|
||||
|
||||
def analyze_route_safety(self, coordinates):
|
||||
return {
|
||||
'total_crashes_near_route': 0,
|
||||
'average_safety_score': 0.0,
|
||||
'max_danger_score': 0.0,
|
||||
'safety_points': [],
|
||||
'crashes_data': [],
|
||||
'route_length_points': len(coordinates)
|
||||
}
|
||||
|
||||
def get_current_weather(self, lat, lon):
|
||||
return fake_get_current_weather(lat, lon)
|
||||
|
||||
def generate_safety_report_with_llm(self, safety_data, route_info, weather_summary=None):
|
||||
return "Fake LLM report"
|
||||
|
||||
fake_reroute.SafeRouteAnalyzer = FakeSafeRouteAnalyzer
|
||||
fake_reroute.MONGO_URI = "mongodb://fake"
|
||||
|
||||
# Insert fake modules into sys.modules so importing flask_server uses them
|
||||
sys.modules['gemini_mongo_mateo'] = fake_mongo
|
||||
sys.modules['gemini_reroute_mateo'] = fake_reroute
|
||||
|
||||
# Also provide package-style names in case flask_server tries them
|
||||
sys.modules['llm.gemini_mongo_mateo'] = fake_mongo
|
||||
sys.modules['llm.gemini_reroute_mateo'] = fake_reroute
|
||||
|
||||
# If Flask isn't installed in the environment, provide a minimal fake
|
||||
# implementation sufficient for this test: Flask, request, jsonify and a
|
||||
# test_client that can call registered route handlers.
|
||||
if 'flask' not in sys.modules:
|
||||
import types
|
||||
|
||||
flask_mod = types.ModuleType('flask')
|
||||
|
||||
class FakeRequest:
|
||||
def __init__(self):
|
||||
self.args = {}
|
||||
self._json = None
|
||||
|
||||
def get_json(self, force=False):
|
||||
return self._json
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, data, status_code=200):
|
||||
self.data = data
|
||||
self.status_code = status_code
|
||||
|
||||
def get_json(self):
|
||||
# If data already a dict, return it; if string, try parse
|
||||
if isinstance(self.data, dict):
|
||||
return self.data
|
||||
try:
|
||||
return json.loads(self.data)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
class FakeApp:
|
||||
def __init__(self, name=None):
|
||||
self._routes = {}
|
||||
self.request = FakeRequest()
|
||||
|
||||
def route(self, path, methods=None):
|
||||
methods = methods or ['GET']
|
||||
def decorator(fn):
|
||||
self._routes[(path, tuple(sorted(methods)))] = fn
|
||||
# Also store by path for simpler lookup
|
||||
self._routes[path] = fn
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
def test_client(self):
|
||||
app = self
|
||||
class Client:
|
||||
def get(self, path):
|
||||
# parse querystring
|
||||
if '?' in path:
|
||||
route, qs = path.split('?', 1)
|
||||
params = {}
|
||||
for pair in qs.split('&'):
|
||||
if '=' in pair:
|
||||
k, v = pair.split('=', 1)
|
||||
params[k] = v
|
||||
else:
|
||||
route = path
|
||||
params = {}
|
||||
app.request.args = params
|
||||
handler = app._routes.get(route)
|
||||
if handler is None:
|
||||
return FakeResponse({'error': 'not found'}, status_code=404)
|
||||
try:
|
||||
result = handler()
|
||||
if isinstance(result, tuple):
|
||||
body, code = result
|
||||
return FakeResponse(body, status_code=code)
|
||||
return FakeResponse(result, status_code=200)
|
||||
except Exception as e:
|
||||
return FakeResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
def post(self, path, json=None):
|
||||
app.request._json = json
|
||||
handler = app._routes.get(path)
|
||||
if handler is None:
|
||||
return FakeResponse({'error': 'not found'}, status_code=404)
|
||||
try:
|
||||
result = handler()
|
||||
if isinstance(result, tuple):
|
||||
body, code = result
|
||||
return FakeResponse(body, status_code=code)
|
||||
return FakeResponse(result, status_code=200)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return FakeResponse({'error': str(e)}, status_code=500)
|
||||
|
||||
return Client()
|
||||
|
||||
def errorhandler(self, code):
|
||||
def decorator(fn):
|
||||
# store error handlers by code
|
||||
if not hasattr(self, '_error_handlers'):
|
||||
self._error_handlers = {}
|
||||
self._error_handlers[code] = fn
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
def fake_jsonify(obj):
|
||||
return obj
|
||||
|
||||
# Populate module
|
||||
flask_mod.Flask = FakeApp
|
||||
flask_mod.request = FakeRequest()
|
||||
flask_mod.jsonify = fake_jsonify
|
||||
|
||||
sys.modules['flask'] = flask_mod
|
||||
|
||||
# Minimal flask_cors shim
|
||||
if 'flask_cors' not in sys.modules:
|
||||
fc = types.ModuleType('flask_cors')
|
||||
def fake_CORS(app):
|
||||
return None
|
||||
fc.CORS = fake_CORS
|
||||
sys.modules['flask_cors'] = fc
|
||||
|
||||
# Load flask_server module from file without executing its __main__ block
|
||||
import importlib.util
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
flask_file = os.path.join(this_dir, 'flask_server.py')
|
||||
|
||||
spec = importlib.util.spec_from_file_location('flask_server', flask_file)
|
||||
flask_server = importlib.util.module_from_spec(spec)
|
||||
# Ensure the module sees the fake modules we inserted
|
||||
sys.modules['flask_server'] = flask_server
|
||||
try:
|
||||
spec.loader.exec_module(flask_server)
|
||||
except Exception as e:
|
||||
print('Failed to import flask_server:', e)
|
||||
traceback.print_exc()
|
||||
sys.exit(2)
|
||||
|
||||
app = getattr(flask_server, 'app', None)
|
||||
if app is None:
|
||||
print('flask_server.app not found')
|
||||
sys.exit(3)
|
||||
|
||||
client = app.test_client()
|
||||
|
||||
results = {}
|
||||
|
||||
# 1) Health
|
||||
r = client.get('/api/health')
|
||||
try:
|
||||
results['health'] = {'status_code': r.status_code, 'json': r.get_json()}
|
||||
except Exception:
|
||||
results['health'] = {'status_code': r.status_code, 'data': r.data.decode('utf-8')}
|
||||
|
||||
# 2) Weather (valid coords)
|
||||
r = client.get('/api/weather?lat=38.9072&lon=-77.0369')
|
||||
results['weather_ok'] = {'status_code': r.status_code, 'json': r.get_json()}
|
||||
|
||||
# 3) Weather (invalid coords)
|
||||
r = client.get('/api/weather')
|
||||
results['weather_invalid'] = {'status_code': r.status_code, 'json': r.get_json()}
|
||||
|
||||
# 4) Analyze crashes (POST)
|
||||
payload = {'lat': 38.9072, 'lon': -77.0369, 'radius': 1.0}
|
||||
r = client.post('/api/analyze-crashes', json=payload)
|
||||
results['analyze_crashes'] = {'status_code': r.status_code, 'json': r.get_json()}
|
||||
|
||||
# 5) Find safe route
|
||||
route_payload = {'start_lat': 38.9, 'start_lon': -77.0, 'end_lat': 38.95, 'end_lon': -77.04}
|
||||
r = client.post('/api/find-safe-route', json=route_payload)
|
||||
results['find_safe_route'] = {'status_code': r.status_code, 'json': r.get_json()}
|
||||
|
||||
# 6) Get single route
|
||||
single_payload = {'start_lat': 38.9, 'start_lon': -77.0, 'end_lat': 38.95, 'end_lon': -77.04}
|
||||
r = client.post('/api/get-single-route', json=single_payload)
|
||||
results['get_single_route'] = {'status_code': r.status_code, 'json': r.get_json()}
|
||||
|
||||
print('\n=== Endpoint test results ===')
|
||||
print(json.dumps(results, indent=2, default=str))
|
||||
|
||||
# Summarize endpoints
|
||||
summary = {
|
||||
'/api/health': 'GET - returns service + dependency health',
|
||||
'/api/weather': 'GET - requires lat & lon query params; returns current weather from LLM module',
|
||||
'/api/analyze-crashes': 'POST - requires JSON {lat, lon, radius}; returns crash summary, weather, LLM safety analysis',
|
||||
'/api/find-safe-route': 'POST - requires JSON start_lat,start_lon,end_lat,end_lon; returns recommended route with safety analysis',
|
||||
'/api/get-single-route': 'POST - similar to find-safe-route but returns single route + LLM safety report'
|
||||
}
|
||||
|
||||
print('\n=== Endpoint summary ===')
|
||||
for path, desc in summary.items():
|
||||
print(f"{path}: {desc}")
|
||||
|
||||
# Exit code 0
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user