chore(core): stage 1 contracts — dialog, API schemas + fixtures, integrations, monitoring, modules
CI / ruff · pytest (pull_request) Successful in 22s
CI / ruff · pytest (pull_request) Successful in 22s
Freeze interfaces between lanes A (bot), B (backend), C (dashboard) so three developers work in parallel without touching each other's files: dialog Event/Reply + engine registry with echo stub, integration protocols with in-memory defaults, MonitoringService with noop stub, /api/v1 schemas with fixture-backed routes, frozen main.py wiring, config/modules.yaml + due_scales(), DAR-5, tzdata, Andrey trajectory, lanes in WORKFLOW, ADR-010. Refs #34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Integration protocols (frozen, #34). Demo uses mocks; prod swaps implementations (SPEC §9).
|
||||
|
||||
Lane B implements the real mocks in `app/integrations/*` (#14) and registers them via `set_*`.
|
||||
The in-memory defaults below keep every lane unblocked and testable before #14 lands.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from typing import Literal, Protocol
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.dialog.contracts import Reply
|
||||
|
||||
AlertLevelName = Literal["info", "moderate", "high", "acute"]
|
||||
|
||||
|
||||
class Prescription(BaseModel):
|
||||
drug: str # INN, e.g. "сертралин"
|
||||
dose: str # "50 мг"
|
||||
schedule: dict[str, list[str]] = Field(default_factory=lambda: {"times": ["08:00"]})
|
||||
instructions: str = "" # "утром после еды"
|
||||
start_date: date | None = None
|
||||
end_date: date | None = None
|
||||
|
||||
|
||||
class VerifiedPerson(BaseModel):
|
||||
token: str
|
||||
doctor_id: str
|
||||
|
||||
|
||||
class DrugSection(BaseModel):
|
||||
inn: str
|
||||
section: str # side_effects | onset | alcohol | discontinuation | ...
|
||||
text: str
|
||||
source: str # reference to the ГРЛС instruction
|
||||
|
||||
|
||||
class PrescriptionSource(Protocol): # МИС / ЕГИСЗ
|
||||
async def get_prescriptions(self, token: str) -> list[Prescription]: ...
|
||||
|
||||
|
||||
class IdentityProvider(Protocol): # ЕСИА; demo: connect code from the doctor
|
||||
async def verify(self, auth_code: str) -> VerifiedPerson | None: ...
|
||||
|
||||
|
||||
class Notifier(Protocol): # doctor notifications: dashboard feed + Telegram chat / MAX / МИС
|
||||
async def send(self, doctor_id: str, level: AlertLevelName, text: str, token: str | None = None) -> None: ...
|
||||
|
||||
|
||||
class DrugInfo(Protocol): # ГРЛС
|
||||
async def sections(self, inn: str) -> list[DrugSection]: ...
|
||||
|
||||
|
||||
class Outbox(Protocol):
|
||||
"""Delivers engine replies initiated by the system (reminders, check-ins) to the patient's channel."""
|
||||
|
||||
async def push(self, token: str, replies: list[Reply]) -> None: ...
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- in-memory defaults
|
||||
|
||||
|
||||
class MemoryOutbox:
|
||||
"""Default outbox: per-token queue polled by the web demo console (`drain`)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._q: dict[str, list[Reply]] = defaultdict(list)
|
||||
|
||||
async def push(self, token: str, replies: list[Reply]) -> None:
|
||||
self._q[token].extend(replies)
|
||||
|
||||
def drain(self, token: str) -> list[Reply]:
|
||||
out, self._q[token] = self._q[token], []
|
||||
return out
|
||||
|
||||
|
||||
class MemoryNotifier:
|
||||
"""Default notifier: keeps notifications in memory (no PII in `text` — reason codes only)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sent: list[tuple[str, str, str, str | None]] = []
|
||||
|
||||
async def send(self, doctor_id: str, level: AlertLevelName, text: str, token: str | None = None) -> None:
|
||||
self.sent.append((doctor_id, level, text, token))
|
||||
|
||||
|
||||
class EmptyPrescriptions:
|
||||
async def get_prescriptions(self, token: str) -> list[Prescription]:
|
||||
return []
|
||||
|
||||
|
||||
class NoDrugInfo:
|
||||
async def sections(self, inn: str) -> list[DrugSection]:
|
||||
return []
|
||||
|
||||
|
||||
class RejectIdentity:
|
||||
async def verify(self, auth_code: str) -> VerifiedPerson | None:
|
||||
return None
|
||||
|
||||
|
||||
class Integrations:
|
||||
"""Registry. Lane B replaces fields at startup (`app.integrations.setup()`, #14)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.outbox: Outbox = MemoryOutbox()
|
||||
self.notifier: Notifier = MemoryNotifier()
|
||||
self.prescriptions: PrescriptionSource = EmptyPrescriptions()
|
||||
self.drug_info: DrugInfo = NoDrugInfo()
|
||||
self.identity: IdentityProvider = RejectIdentity()
|
||||
|
||||
|
||||
integrations = Integrations()
|
||||
Reference in New Issue
Block a user