| """ |
| 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.", |
| } |
| } |
| } |
|
|