Spaces:
Sleeping
Sleeping
| """Endpoints de la integración de analizadores. | |
| - POST /api/lab/ingesta (puente local → backend, autenticado por API key de dispositivo) | |
| - GET /api/lab/resultados (navegador → backend, autenticado por sesión) — match por muestra | |
| Dos zonas de confianza: la ingesta NO usa cookie/CSRF (el puente es headless, la API key es | |
| la auth); la consulta usa la sesión existente. El mapeo código→analito ocurre aquí (backend), | |
| única fuente de verdad. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| from fastapi import APIRouter, Depends, HTTPException, Query, Request, status | |
| from .. import db | |
| from ..config import obtener_config | |
| from ..lab.almacen import almacen | |
| from ..lab.mapeo import mapear_resultado | |
| from ..schemas_lab import ( | |
| RespuestaIngesta, | |
| ResultadoAnalizador, | |
| ResultadoMapeado, | |
| ResumenPendiente, | |
| ) | |
| from ..security.authz import tenant_de_sesion, usuario_actual | |
| from ..security.device import verificar_dispositivo | |
| from ..security.rate_limit import limiter | |
| router = APIRouter() | |
| async def post_ingesta( | |
| request: Request, # requerido por slowapi | |
| cuerpo: ResultadoAnalizador, | |
| tenant: str = Depends(verificar_dispositivo), # 503 sin keys / 401 sin Bearer válido | |
| ) -> RespuestaIngesta: | |
| mapeado = mapear_resultado(cuerpo) | |
| mapeado.tenant = tenant # del servidor, a partir de la API key; nunca del cuerpo | |
| almacen.guardar(mapeado) | |
| if obtener_config().lab_persistir: | |
| # A un hilo como el resto de SQLite: el analizador manda en ráfaga | |
| # (`limite_lab_ingesta` = 120/minute) y esto corre por cada muestra. | |
| await asyncio.to_thread( | |
| db.guardar_resultado_lab, | |
| mapeado.muestra_id.strip().lower(), | |
| mapeado.momento.isoformat(), | |
| mapeado.model_dump_json(), | |
| ) | |
| return RespuestaIngesta( | |
| muestra_id=mapeado.muestra_id, | |
| analitos_mapeados=len(mapeado.analitos), | |
| no_mapeados=mapeado.no_mapeados, | |
| ) | |
| async def get_resultados( | |
| request: Request, | |
| muestra: str = Query(..., min_length=1, max_length=128), | |
| sesion: dict = Depends(usuario_actual), # 401 si no hay sesión | |
| ) -> ResultadoMapeado: | |
| res = almacen.obtener(muestra, tenant_de_sesion(sesion)) | |
| if res is None: | |
| raise HTTPException( | |
| status.HTTP_404_NOT_FOUND, | |
| "No hay resultados para esa muestra todavía.", | |
| ) | |
| return res | |
| async def get_pendientes( | |
| request: Request, | |
| sesion: dict = Depends(usuario_actual), | |
| ) -> list[ResumenPendiente]: | |
| """Cola de resultados recibidos (más recientes primero) para elegir sin teclear el ID. | |
| Desactivada por defecto (`MORPHOS_LAB_PENDIENTES_HABILITADO`): el almacén no está | |
| segmentado, así que esto enumera las muestras de TODAS las clínicas y cada ID abre el panel | |
| completo en `/lab/resultados`. 404 —y no 403— para que apagada sea indistinguible de no | |
| existir. Ver el comentario en `config.py`: apagarla reduce el volcado masivo, no sustituye | |
| al filtrado por tenant. | |
| """ | |
| if not obtener_config().lab_pendientes_habilitado: | |
| raise HTTPException(status.HTTP_404_NOT_FOUND, "No disponible.") | |
| return [ | |
| ResumenPendiente( | |
| muestra_id=r.muestra_id, | |
| instrumento_id=r.instrumento_id, | |
| momento=r.momento, | |
| analitos=len(r.analitos), | |
| no_mapeados=len(r.no_mapeados), | |
| ) | |
| for r in almacen.pendientes(tenant_de_sesion(sesion)) | |
| ] | |