LIBRE / src /interface /api /routes /ppg_routes.py
RyZ
feat: adding full working local ETL Pipeline
e391a84
Raw
History Blame Contribute Delete
4.17 kB
"""
interface/api/routes/ppg_routes.py
────────────────────────────────────
REST endpoints for PPG signal ingestion (ETL #1).
Endpoints:
POST /api/v1/ppg/ingest β€” Receive PPG from mobile app, store + queue
GET /api/v1/ppg/{user_id} β€” Query stored PPG signals for a user
Error handling:
All domain exceptions (InvalidSignalError, ConflictError, DatabaseError,
BrokerError, etc.) bubble up to the global handlers registered in
error_handlers.py. Route handlers are intentionally kept exception-free
for readability.
"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, status
from src.application.dto.ppg_dto import PPGIngestRequest, PPGIngestResponse
from src.application.use_cases.ingest_ppg import IngestPPGUseCase
from src.interface.api.dependencies import get_ingest_ppg_use_case
from src.shared.logger import get_logger
logger = get_logger(__name__)
router = APIRouter(
prefix="/ppg",
tags=["PPG Ingestion"],
)
@router.post(
"/ingest",
response_model=PPGIngestResponse,
status_code=status.HTTP_201_CREATED,
summary="Ingest a PPG Signal",
description=(
"Receive a raw PPG signal from the mobile app (IoT sensor), validate it, "
"persist it to the database, and publish it to the message queue for "
"downstream AI processing (ETL #1)."
),
responses={
201: {"description": "Signal ingested and queued successfully."},
400: {
"description": "Invalid signal β€” domain validation failed.",
"content": {
"application/json": {
"example": {
"error": "invalid_signal",
"message": "Invalid PPG signal β€” field 'sampling_rate': ...",
"context": {"field": "sampling_rate", "value": -1, "reason": "..."},
"timestamp": "2026-05-31T09:00:00+00:00",
}
}
},
},
409: {
"description": "Conflict β€” duplicate signal record.",
"content": {
"application/json": {
"example": {
"error": "conflict",
"message": "Conflict on PPGModel: A record with the same unique key already exists.",
"context": {},
"timestamp": "2026-05-31T09:00:00+00:00",
}
}
},
},
422: {"description": "Unprocessable β€” Pydantic request validation failed."},
503: {
"description": "Service unavailable β€” database or broker is down.",
"content": {
"application/json": {
"example": {
"error": "database_unavailable",
"message": "The database is temporarily unavailable.",
"context": {},
"timestamp": "2026-05-31T09:00:00+00:00",
}
}
},
},
},
)
async def ingest_ppg(
request: PPGIngestRequest,
use_case: Annotated[IngestPPGUseCase, Depends(get_ingest_ppg_use_case)],
) -> PPGIngestResponse:
"""
**ETL #1 β€” Entry Point**
Accepts a JSON payload from the smartphone, validates it against domain
rules, stores the raw PPG in PostgreSQL (Supabase), and publishes an event
to RabbitMQ for the AI processing consumer (ETL #2).
Returns the stored signal ID and queue confirmation.
**Possible errors:**
- `400 invalid_signal` β€” sampling rate out of range, signal too short, etc.
- `409 conflict` β€” duplicate signal ID submitted.
- `503 database_unavailable` β€” Supabase is temporarily unreachable.
- `503 broker_unavailable` β€” RabbitMQ/CloudAMQP is temporarily unreachable.
"""
logger.info(
"POST /ppg/ingest β€” device=%s user=%s",
request.device_id,
request.user_id,
)
return await use_case.execute(request)