Spaces:
Sleeping
Sleeping
File size: 2,943 Bytes
70e641d 1339cdc 70e641d 1339cdc 70e641d 1339cdc 70e641d 1339cdc 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 | """Almacén en proceso de resultados de analizador, emparejados por ID de muestra.
Segmentado por TENANT: cada resultado lleva la clínica dueña y las lecturas la exigen. Antes era
un dict global y cualquier sesión autenticada podía leer la muestra de cualquier clínica.
Deliberadamente NO es SQLite: HF Spaces tiene disco efímero, un único worker de uvicorn, y
los resultados son de vida corta (se emparejan con el formulario en minutos). Un dict con
TTL + tope LRU es la primitiva correcta. Clave normalizada (trim + minúsculas) en lectura y
escritura. Si algún día se añaden workers, este almacén deja de ser correcto y hay que
moverlo a SQLite/caché compartida (ver `lab_persistir` en config).
"""
from __future__ import annotations
import threading
import time
from collections import OrderedDict
from ..schemas_lab import ResultadoMapeado
TTL_SEGUNDOS = 24 * 3600
MAX_ENTRADAS = 500
def _clave(muestra_id: str) -> str:
return muestra_id.strip().lower()
class AlmacenResultados:
def __init__(self, ttl: int = TTL_SEGUNDOS, max_entradas: int = MAX_ENTRADAS) -> None:
self._ttl = ttl
self._max = max_entradas
self._lock = threading.Lock()
# clave → (instante_monotónico, resultado). OrderedDict para desalojo LRU.
self._datos: OrderedDict[str, tuple[float, ResultadoMapeado]] = OrderedDict()
def guardar(self, res: ResultadoMapeado) -> None:
with self._lock:
k = _clave(res.muestra_id)
self._datos[k] = (time.monotonic(), res) # último gana
self._datos.move_to_end(k)
self._barrer_locked()
while len(self._datos) > self._max:
self._datos.popitem(last=False) # desaloja el más antiguo
def obtener(self, muestra_id: str, tenant: str) -> ResultadoMapeado | None:
"""Resultado de ESE tenant, o None. Un ID de otra clínica se comporta como inexistente.
El tenant es obligatorio a propósito: si fuera opcional, olvidarlo en una llamada nueva
devolvería datos de todas las clínicas en silencio, que es justo el fallo que esto cierra.
"""
with self._lock:
self._barrer_locked()
item = self._datos.get(_clave(muestra_id))
if item is None or item[1].tenant != tenant:
return None
return item[1]
def pendientes(self, tenant: str) -> list[ResultadoMapeado]:
"""Cola de ESE tenant, más recientes primero."""
with self._lock:
self._barrer_locked()
return [r for (_, r) in reversed(self._datos.values()) if r.tenant == tenant]
def _barrer_locked(self) -> None:
ahora = time.monotonic()
expiradas = [k for k, (t, _) in self._datos.items() if ahora - t > self._ttl]
for k in expiradas:
del self._datos[k]
# Singleton de módulo importado por routers/lab.py.
almacen = AlmacenResultados()
|