| """ |
| application/use_cases/ingest_ppg.py |
| βββββββββββββββββββββββββββββββββββββ |
| IngestPPGUseCase β ETL #1 orchestrator. |
| |
| Responsibility: |
| Accept a raw PPG payload from the API, validate it, persist it to the |
| database, and publish a message to the queue for downstream processing. |
| |
| Dependencies (injected via constructor β Dependency Inversion Principle): |
| β’ PPGRepository β stores the signal |
| β’ MessageBroker β publishes the event |
| |
| This use case knows nothing about FastAPI, SQLAlchemy, or RabbitMQ. |
| """ |
| from __future__ import annotations |
|
|
| import json |
|
|
| from src.application.dto.ppg_dto import PPGIngestRequest, PPGIngestResponse |
| from src.domain.entities.ppg_signal import PPGSignal |
| from src.domain.interfaces.repositories.ppg_repository import PPGRepository |
| from src.domain.interfaces.services.message_broker import MessageBroker |
| from src.shared.constants import PPG_QUEUE_NAME |
| from src.shared.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| class IngestPPGUseCase: |
| """ |
| ETL #1: Extract β Transform β Load for incoming PPG signals. |
| |
| Steps: |
| 1. Extract β build a validated PPGSignal entity from the request DTO. |
| 2. Transform β call ``entity.validate()`` to enforce domain rules. |
| 3. Load β persist to database (``ppg_repo.add()``) |
| publish to queue (``broker.publish()``). |
| |
| Usage:: |
| |
| use_case = IngestPPGUseCase(ppg_repo=..., broker=...) |
| response = await use_case.execute(request) |
| """ |
|
|
| def __init__( |
| self, |
| ppg_repo: PPGRepository, |
| broker: MessageBroker, |
| ) -> None: |
| """ |
| Args: |
| ppg_repo: Any concrete PPGRepository (SQLAlchemy, in-memory, β¦). |
| broker: Any concrete MessageBroker (RabbitMQ, in-memory, β¦). |
| """ |
| self._ppg_repo = ppg_repo |
| self._broker = broker |
|
|
| async def execute(self, request: PPGIngestRequest) -> PPGIngestResponse: |
| """ |
| Run the ingestion pipeline for a single PPG signal. |
| |
| Args: |
| request: Validated Pydantic DTO from the API route. |
| |
| Returns: |
| PPGIngestResponse with the stored signal ID and queue status. |
| |
| Raises: |
| InvalidSignalError: If the signal fails domain validation. |
| """ |
| logger.info( |
| "IngestPPGUseCase.execute β device=%s user=%s samples=%d", |
| request.device_id, |
| request.user_id, |
| len(request.ppg_values), |
| ) |
|
|
| |
| signal = PPGSignal( |
| device_id=request.device_id, |
| user_id=request.user_id, |
| sampling_rate=request.sampling_rate, |
| ppg_values=request.ppg_values, |
| duration_seconds=request.duration_seconds, |
| timestamp=request.timestamp, |
| ) |
|
|
| |
| signal.validate() |
| logger.debug("Signal validated: %s", signal) |
|
|
| |
| persisted_signal = await self._ppg_repo.add(signal) |
| logger.info("Signal persisted with id=%s", persisted_signal.id) |
|
|
| |
| queued = False |
| try: |
| await self._broker.publish( |
| queue_name=PPG_QUEUE_NAME, |
| message=persisted_signal.to_dict(), |
| ) |
| queued = True |
| logger.info("Signal %s published to queue '%s'", persisted_signal.id, PPG_QUEUE_NAME) |
| except Exception as exc: |
| |
| |
| logger.error( |
| "Failed to publish signal %s to queue: %s", |
| persisted_signal.id, |
| exc, |
| exc_info=True, |
| ) |
|
|
| return PPGIngestResponse( |
| signal_id=persisted_signal.id, |
| user_id=persisted_signal.user_id, |
| device_id=persisted_signal.device_id, |
| num_samples=persisted_signal.num_samples, |
| duration_seconds=persisted_signal.duration_seconds, |
| queued=queued, |
| ) |
|
|