| """ |
| interface/api/routes/prediction_routes.py |
| βββββββββββββββββββββββββββββββββββββββββββ |
| REST endpoints for querying BP prediction history. |
| |
| Endpoints: |
| GET /api/v1/predictions/{user_id} β Latest N predictions for a user |
| GET /api/v1/predictions/{user_id}/signal/{signal_id} β Prediction for a specific signal |
| |
| Error handling: |
| Domain exceptions bubble up to the global handlers in error_handlers.py. |
| EntityNotFoundError is raised explicitly when a lookup returns None. |
| """ |
| from __future__ import annotations |
|
|
| from datetime import datetime |
| from typing import Annotated, Optional |
|
|
| from fastapi import APIRouter, Depends, Query, status |
|
|
| from src.application.dto.prediction_dto import PredictionHistoryResponse, PredictionResponse |
| from src.application.use_cases.get_prediction_history import GetPredictionHistoryUseCase |
| from src.domain.exceptions.domain_exceptions import EntityNotFoundError |
| from src.domain.interfaces.repositories.prediction_repository import PredictionRepository |
| from src.interface.api.dependencies import ( |
| get_prediction_history_use_case, |
| get_prediction_repository, |
| ) |
| from src.shared.constants import DEFAULT_PAGINATION_LIMIT |
| from src.shared.logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
| router = APIRouter( |
| prefix="/predictions", |
| tags=["Blood Pressure Predictions"], |
| ) |
|
|
| |
| _NOT_FOUND_RESPONSE = { |
| "description": "Resource not found.", |
| "content": { |
| "application/json": { |
| "example": { |
| "error": "not_found", |
| "message": "BPPrediction with id='...' not found.", |
| "context": {"entity_type": "BPPrediction", "entity_id": "..."}, |
| "timestamp": "2026-05-31T09:00:00+00:00", |
| } |
| } |
| }, |
| } |
|
|
| _DB_ERROR_RESPONSE = { |
| "description": "Database temporarily unavailable.", |
| "content": { |
| "application/json": { |
| "example": { |
| "error": "database_unavailable", |
| "message": "The database is temporarily unavailable. Please try again later.", |
| "context": {}, |
| "timestamp": "2026-05-31T09:00:00+00:00", |
| } |
| } |
| }, |
| } |
|
|
|
|
| @router.get( |
| "/{user_id}", |
| response_model=PredictionHistoryResponse, |
| status_code=status.HTTP_200_OK, |
| summary="Get Prediction History for a User", |
| description=( |
| "Retrieve paginated BP prediction history for a patient. " |
| "Optionally filter by date range (ISO 8601 UTC timestamps)." |
| ), |
| responses={ |
| 200: {"description": "Prediction history retrieved successfully."}, |
| 503: _DB_ERROR_RESPONSE, |
| }, |
| ) |
| async def get_prediction_history( |
| user_id: str, |
| use_case: Annotated[GetPredictionHistoryUseCase, Depends(get_prediction_history_use_case)], |
| limit: int = Query(default=DEFAULT_PAGINATION_LIMIT, ge=1, le=500), |
| start: Optional[datetime] = Query(default=None, description="Start of date range (UTC ISO8601)"), |
| end: Optional[datetime] = Query(default=None, description="End of date range (UTC ISO8601)"), |
| ) -> PredictionHistoryResponse: |
| """ |
| Retrieve BP prediction history for a given user. |
| |
| - Without ``start``/``end``: returns the latest N predictions (``limit``). |
| - With ``start`` and ``end``: returns predictions within the date range. |
| |
| **Possible errors:** |
| - `503 database_unavailable` β Supabase is temporarily unreachable. |
| """ |
| logger.info( |
| "GET /predictions/%s β limit=%d, start=%s, end=%s", |
| user_id, limit, start, end, |
| ) |
| return await use_case.execute( |
| user_id=user_id, |
| limit=limit, |
| start=start, |
| end=end, |
| ) |
|
|
|
|
| @router.get( |
| "/{user_id}/signal/{signal_id}", |
| response_model=PredictionResponse, |
| status_code=status.HTTP_200_OK, |
| summary="Get Prediction for a Specific PPG Signal", |
| description=( |
| "Retrieve the AI-generated blood pressure prediction associated " |
| "with a specific PPG signal ID." |
| ), |
| responses={ |
| 200: {"description": "Prediction retrieved successfully."}, |
| 404: _NOT_FOUND_RESPONSE, |
| 503: _DB_ERROR_RESPONSE, |
| }, |
| ) |
| async def get_prediction_by_signal( |
| user_id: str, |
| signal_id: str, |
| prediction_repo: Annotated[PredictionRepository, Depends(get_prediction_repository)], |
| ) -> PredictionResponse: |
| """ |
| Retrieve the BP prediction associated with a specific PPG signal. |
| |
| **Possible errors:** |
| - `404 not_found` β No prediction exists for the given ``signal_id``. |
| - `503 database_unavailable` β Supabase is temporarily unreachable. |
| """ |
| logger.info("GET /predictions/%s/signal/%s", user_id, signal_id) |
|
|
| prediction = await prediction_repo.get_by_signal_id(signal_id) |
|
|
| if prediction is None: |
| raise EntityNotFoundError(entity_type="BPPrediction", entity_id=signal_id) |
|
|
| return PredictionResponse( |
| id=prediction.id, |
| ppg_signal_id=prediction.ppg_signal_id, |
| predicted_sbp=prediction.predicted_sbp, |
| predicted_dbp=prediction.predicted_dbp, |
| predicted_ecg=prediction.predicted_ecg, |
| mean_arterial_pressure=prediction.mean_arterial_pressure, |
| pulse_pressure=prediction.pulse_pressure, |
| hypertension_stage=prediction.hypertension_stage, |
| model_version=prediction.model_version, |
| inference_time_ms=prediction.inference_time_ms, |
| sa_log=prediction.sa_log, |
| created_at=prediction.created_at, |
| ) |
|
|