File size: 3,821 Bytes
70e641d
 
 
 
 
 
 
 
 
 
 
 
e26316b
 
70e641d
 
 
 
 
 
 
 
 
 
 
 
1339cdc
70e641d
 
 
 
 
 
 
 
 
 
 
1339cdc
70e641d
 
1339cdc
70e641d
 
e26316b
 
 
 
70e641d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1339cdc
70e641d
1339cdc
70e641d
 
 
 
 
 
 
 
 
 
 
 
1339cdc
70e641d
61cd0db
 
 
 
 
 
 
 
 
 
70e641d
 
 
 
 
 
 
 
1339cdc
70e641d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""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()


@router.post("/lab/ingesta", response_model=RespuestaIngesta)
@limiter.limit(obtener_config().limite_lab_ingesta)
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,
    )


@router.get("/lab/resultados", response_model=ResultadoMapeado)
@limiter.limit(obtener_config().limite_lab_consulta)
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


@router.get("/lab/pendientes", response_model=list[ResumenPendiente])
@limiter.limit(obtener_config().limite_lab_consulta)
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))
    ]