LIBRE / src /application /use_cases /ingest_ppg.py
RyZ
feat: adding full working local ETL Pipeline
e391a84
Raw
History Blame Contribute Delete
4.69 kB
"""
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),
)
# ── Step 1: EXTRACT β€” Build domain entity ─────────────────────────────
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,
)
# ── Step 2: TRANSFORM β€” Domain validation ────────────────────────────
signal.validate()
logger.debug("Signal validated: %s", signal)
# ── Step 3a: LOAD β€” Persist to database ──────────────────────────────
persisted_signal = await self._ppg_repo.add(signal)
logger.info("Signal persisted with id=%s", persisted_signal.id)
# ── Step 3b: LOAD β€” Publish to message queue ─────────────────────────
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:
# Publishing failure is non-fatal: signal is already persisted.
# Log the error so the ops team can replay the message if needed.
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,
)