Files

286 lines
10 KiB
Python

from fastapi import FastAPI, Depends, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from apscheduler.schedulers.background import BackgroundScheduler
from database import engine, get_db
from typing import Dict, Set, Optional
import asyncio
import models, schemas, auth, simulator
models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="HackOnLinces 2026 - Recolección de Residuos")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"])
alertas_activas: Dict[str, dict] = {}
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, Set[WebSocket]] = {}
async def connect(self, websocket: WebSocket, route_id: str):
await websocket.accept()
if route_id not in self.active_connections:
self.active_connections[route_id] = set()
self.active_connections[route_id].add(websocket)
def disconnect(self, websocket: WebSocket, route_id: str):
if route_id in self.active_connections:
self.active_connections[route_id].discard(websocket)
async def broadcast_to_route(self, route_id: str, message: dict):
if route_id in self.active_connections:
dead = set()
for ws in self.active_connections[route_id]:
try:
await ws.send_json(message)
except:
dead.add(ws)
self.active_connections[route_id] -= dead
manager = ConnectionManager()
scheduler = BackgroundScheduler()
scheduler.add_job(simulator.avanzar_rutas, "interval", minutes=2)
scheduler.start()
@app.on_event("startup")
def startup():
db = next(get_db())
simulator.init_rutas(db)
@app.get("/")
def root():
return {"message": "API HackOnLinces 2026 funcionando"}
@app.post("/auth/register", response_model=schemas.Token)
def register(user: schemas.UsuarioCreate, db: Session = Depends(get_db)):
if db.query(models.Usuario).filter_by(email=user.email).first():
raise HTTPException(status_code=400, detail="Email ya registrado")
nuevo = models.Usuario(email=user.email,
hashed_password=auth.hash_password(user.password))
db.add(nuevo)
db.commit()
db.refresh(nuevo)
token = auth.create_token({"sub": nuevo.email})
return {"access_token": token, "token_type": "bearer"}
@app.post("/auth/login", response_model=schemas.Token)
def login(user: schemas.UsuarioLogin, db: Session = Depends(get_db)):
db_user = db.query(models.Usuario).filter_by(email=user.email).first()
if not db_user or not auth.verify_password(user.password, db_user.hashed_password):
raise HTTPException(status_code=401, detail="Credenciales incorrectas")
token = auth.create_token({"sub": db_user.email})
return {"access_token": token, "token_type": "bearer"}
@app.post("/domicilios", response_model=schemas.DomicilioResponse)
def crear_domicilio(data: schemas.DomicilioCreate,
current_user=Depends(auth.get_current_user),
db: Session = Depends(get_db)):
colonia_key = data.colonia.lower()
# 1. Buscar en colonias completas (220 colonias reales)
colonia_info = simulator.COLONIAS_COMPLETAS.get(colonia_key)
# 2. Buscar en colonias originales del dataset
if not colonia_info:
colonia_info = simulator.COLONIAS.get(colonia_key)
# 3. Buscar por similitud parcial en colonias completas
if not colonia_info:
for key, value in simulator.COLONIAS_COMPLETAS.items():
if key in colonia_key or colonia_key in key:
colonia_info = value
break
# 4. Fallback por coordenadas GPS
if not colonia_info:
ruta, _ = simulator.encontrar_ruta_por_coordenadas(data.lat, data.lng)
if ruta:
colonia_info = next(
(v for v in simulator.COLONIAS_COMPLETAS.values() if v["routeId"] == ruta["routeId"]),
None
)
if not colonia_info:
raise HTTPException(status_code=404, detail="Colonia no encontrada en el sistema")
dom = models.Domicilio(direccion=data.direccion, colonia=data.colonia,
lat=data.lat, lng=data.lng,
route_id=colonia_info["routeId"],
usuario_id=current_user.id)
db.add(dom)
db.commit()
db.refresh(dom)
return dom
return dom
@app.get("/domicilios")
def listar_domicilios(
current_user=Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
domicilios = db.query(models.Domicilio).filter_by(usuario_id=current_user.id).all()
return [{"id": d.id, "direccion": d.direccion, "colonia": d.colonia, "route_id": d.route_id} for d in domicilios]
@app.get("/eta/{domicilio_id}", response_model=schemas.ETAResponse)
def get_eta(domicilio_id: int,
current_user=Depends(auth.get_current_user),
db: Session = Depends(get_db)):
dom = db.query(models.Domicilio).filter_by(id=domicilio_id).first()
if not dom:
raise HTTPException(status_code=404, detail="Domicilio no encontrado")
if dom.usuario_id != current_user.id:
raise HTTPException(status_code=403, detail="No tienes acceso a este domicilio")
eta = simulator.get_eta(dom.route_id, db)
if not eta:
raise HTTPException(status_code=404, detail="Ruta no encontrada")
return {**eta, "route_id": dom.route_id, "colonia": dom.colonia}
@app.get("/colonias-por-cp")
def colonias_por_cp(
codigo_postal: str,
current_user=Depends(auth.get_current_user)
):
colonias = [
v["colonia"] for v in simulator.COLONIAS_COMPLETAS.values()
if v.get("cp") == codigo_postal
]
return {
"colonias": sorted(set(colonias)),
"encontrado": len(colonias) > 0
}
@app.get("/admin/rutas/estado")
def admin_estado_rutas(db: Session = Depends(get_db)):
resultado = []
for ruta in simulator.RUTAS:
eta = simulator.get_eta(ruta["routeId"], db)
resultado.append({
"route_id": ruta["routeId"],
"name": ruta["name"],
"evento": eta["evento"] if eta else "DESCONOCIDO",
"current_position": eta.get("current_position", 0) if eta else 0,
"ventana_inicio": eta["ventana_inicio"] if eta else "--",
"ventana_fin": eta["ventana_fin"] if eta else "--",
})
return resultado
@app.post("/reportes")
def crear_reporte(
domicilio_id: int,
tipo: str,
descripcion: str,
current_user=Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
dom = db.query(models.Domicilio).filter_by(id=domicilio_id).first()
if not dom or dom.usuario_id != current_user.id:
raise HTTPException(status_code=403, detail="No tienes acceso a este domicilio")
return {
"mensaje": "Reporte recibido correctamente",
"tipo": tipo,
"domicilio_id": domicilio_id,
"descripcion": descripcion,
"estado": "PENDIENTE"
}
@app.post("/alertas/operativa")
def crear_alerta_operativa(
route_id: str,
tipo: str,
mensaje: str,
db: Session = Depends(get_db)
):
estado = db.query(models.EstadoRuta).filter_by(route_id=route_id).first()
if not estado:
raise HTTPException(status_code=404, detail="Ruta no encontrada")
alertas_activas[route_id] = {
"route_id": route_id,
"tipo": tipo,
"mensaje": mensaje,
"evento": "ALERTA_OPERATIVA",
"activa": True
}
return alertas_activas[route_id]
@app.delete("/alertas/operativa/{route_id}")
def eliminar_alerta_operativa(route_id: str):
if route_id in alertas_activas:
del alertas_activas[route_id]
return {"mensaje": "Alerta eliminada"}
@app.get("/alertas/operativa/activa")
def get_alerta_activa(
route_id: str,
current_user=Depends(auth.get_current_user),
db: Session = Depends(get_db)
):
dom = db.query(models.Domicilio).filter_by(
usuario_id=current_user.id, route_id=route_id
).first()
if not dom:
raise HTTPException(status_code=403, detail="Sin acceso")
alerta = alertas_activas.get(route_id)
if alerta:
return alerta
return {"activa": False, "mensaje": None}
@app.get("/domicilios/ruta-por-coordenadas")
def ruta_por_coordenadas(
lat: float,
lng: float,
current_user=Depends(auth.get_current_user)
):
ruta, distancia = simulator.encontrar_ruta_por_coordenadas(lat, lng)
if not ruta:
raise HTTPException(status_code=404, detail="No hay cobertura en esta ubicación")
colonia_info = next(
(c for c in simulator.COLONIAS.values() if c["routeId"] == ruta["routeId"]),
None
)
return {
"route_id": ruta["routeId"],
"nombre_ruta": ruta["name"],
"colonia_sugerida": colonia_info["colonia"] if colonia_info else ruta["name"],
"distancia_metros": round(distancia),
"cobertura": True
}
@app.websocket("/ws/eta/{domicilio_id}")
async def websocket_eta(websocket: WebSocket, domicilio_id: int,
token: str, db: Session = Depends(get_db)):
try:
payload = auth.jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM])
email = payload.get("sub")
user = db.query(models.Usuario).filter_by(email=email).first()
if not user:
await websocket.close(code=1008)
return
dom = db.query(models.Domicilio).filter_by(id=domicilio_id).first()
if not dom or dom.usuario_id != user.id:
await websocket.close(code=1008)
return
except:
await websocket.close(code=1008)
return
await manager.connect(websocket, dom.route_id)
try:
eta = simulator.get_eta(dom.route_id, db)
if eta:
await websocket.send_json({**eta, "route_id": dom.route_id, "colonia": dom.colonia})
while True:
await asyncio.sleep(30)
eta = simulator.get_eta(dom.route_id, db)
if eta:
payload = {**eta, "route_id": dom.route_id, "colonia": dom.colonia}
alerta = alertas_activas.get(dom.route_id)
if alerta:
payload["alerta_operativa"] = alerta
await websocket.send_json(payload)
except WebSocketDisconnect:
manager.disconnect(websocket, dom.route_id)