"""МИС / ЕГИСЗ stand-in: the patient's prescriptions (SPEC §9). Source order: the clinical DB table `prescriptions` (written when the doctor connects a patient or by the demo seed), then the synthetic catalogue `data/meds/mis_prescriptions.json` keyed by token. In prod this is a regional МИС query; the Protocol stays the same. """ import json import logging from datetime import date from pathlib import Path from typing import Any from pydantic import BaseModel, Field, ValidationError from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError from app.config import get_settings from app.integrations.base import Prescription log = logging.getLogger(__name__) class _CatalogPatient(BaseModel): persona: str tokens: list[str] = Field(default_factory=list) prescriptions: list[Prescription] class _Catalog(BaseModel): patients: list[_CatalogPatient] def _from_row(drug: str, dose: str, schedule: dict[str, Any], start: date | None, end: date | None) -> Prescription: # The table has no instructions column; the doctor's note («утром после еды») may ride in the schedule JSON. times = {k: [str(x) for x in v] for k, v in schedule.items() if isinstance(v, list)} return Prescription( drug=drug, dose=dose, schedule=times or {"times": ["08:00"]}, instructions=str(schedule.get("instructions") or ""), start_date=start, end_date=end, ) def default_catalog_path() -> Path: return get_settings().data_dir / "meds" / "mis_prescriptions.json" class MisMock: """`PrescriptionSource`: clinical DB first, synthetic JSON catalogue as the fallback.""" def __init__(self, catalog_path: Path | None = None, *, use_db: bool = True) -> None: self._path = catalog_path self._use_db = use_db self._by_token: dict[str, list[Prescription]] | None = None def _catalog(self) -> dict[str, list[Prescription]]: # Lazy: the package is imported before startup, a missing/broken file must not break the import. if self._by_token is None: path = self._path or default_catalog_path() index: dict[str, list[Prescription]] = {} try: catalog = _Catalog.model_validate(json.loads(path.read_text(encoding="utf-8"))) except (OSError, ValueError, ValidationError) as exc: log.warning("mis_mock catalogue not loaded error=%s", type(exc).__name__) catalog = _Catalog(patients=[]) for patient in catalog.patients: for token in patient.tokens: index[token] = patient.prescriptions self._by_token = index return self._by_token async def _from_db(self, token: str) -> list[Prescription]: from app.db.clinical import Prescription as PrescriptionRow from app.db.session import get_clinical_session async with get_clinical_session() as s: rows = ( await s.scalars( select(PrescriptionRow).where(PrescriptionRow.token == token).order_by(PrescriptionRow.id) ) ).all() return [_from_row(r.drug, r.dose, r.schedule or {}, r.start_date, r.end_date) for r in rows] async def get_prescriptions(self, token: str) -> list[Prescription]: if self._use_db: try: found = await self._from_db(token) except (OSError, SQLAlchemyError) as exc: # DB down or not migrated: the demo still shows the catalogue prescription. log.warning("mis_mock db lookup failed error=%s", type(exc).__name__) found = [] if found: return found return [p.model_copy(deep=True) for p in self._catalog().get(token, [])]