""" application/dto/ppg_dto.py ─────────────────────────── Pydantic DTOs for PPG ingestion — the "shape" of data crossing the API boundary into the Application layer. Why separate from domain entity? • PPGSignal entity enforces domain rules (validate()). • PPGIngestRequest handles HTTP deserialization & input validation. • Keeps domain entity clean — no Pydantic Field annotations, no JSON aliases. """ from __future__ import annotations from datetime import datetime, timezone from typing import Optional from pydantic import BaseModel, Field, field_validator class PPGIngestRequest(BaseModel): """ Incoming payload from the mobile app (POST /api/v1/ppg/ingest). Sent by the smartphone after acquiring a PPG signal from the IoT sensor. """ device_id: str = Field( ..., min_length=1, max_length=128, description="Unique identifier of the IoT sensor/device.", examples=["sensor-001", "ppg-wristband-A3F2"], ) user_id: str = Field( ..., min_length=1, max_length=128, description="Unique identifier of the patient/user.", examples=["user-123", "patient-456"], ) sampling_rate: float = Field( ..., gt=0, le=1000, description="Signal sampling rate in Hz.", examples=[125.0, 250.0], ) ppg_values: list[float] = Field( ..., min_length=1, description="Ordered list of raw PPG amplitude values.", ) duration_seconds: float = Field( ..., gt=0, description="Duration of the recording in seconds.", examples=[10.0, 30.0, 60.0], ) timestamp: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), description="UTC timestamp of capture. Defaults to server time if omitted.", ) @field_validator("ppg_values") @classmethod def validate_ppg_values_not_empty(cls, v: list[float]) -> list[float]: if len(v) == 0: raise ValueError("ppg_values must contain at least one sample") return v model_config = { "json_schema_extra": { "example": { "device_id": "sensor-001", "user_id": "user-123", "sampling_rate": 125.0, "ppg_values": [0.10, 0.15, 0.40, 0.85, 0.92, 0.75, 0.42, 0.18, 0.12], "duration_seconds": 10.0, } } } class PPGIngestResponse(BaseModel): """ Response returned after a successful PPG ingestion (ETL #1). """ signal_id: str = Field(description="UUID of the stored PPGSignal record.") user_id: str device_id: str num_samples: int = Field(description="Number of PPG samples stored.") duration_seconds: float queued: bool = Field( description="Whether the signal was successfully published to the message queue." ) message: str = Field(default="PPG signal ingested successfully.") model_config = { "json_schema_extra": { "example": { "signal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "user_id": "user-123", "device_id": "sensor-001", "num_samples": 1250, "duration_seconds": 10.0, "queued": True, "message": "PPG signal ingested successfully.", } } }