LIBRE / src /domain /exceptions /domain_exceptions.py
RyZ
feat: adding full working local ETL Pipeline
e391a84
Raw
History Blame Contribute Delete
5.15 kB
"""
domain/exceptions/domain_exceptions.py
───────────────────────────────────────
Domain-specific exception hierarchy.
All exceptions here represent business-rule violations β€” they are completely
framework-agnostic (no HTTP codes, no FastAPI imports).
Hierarchy:
DomainException (base)
β”œβ”€β”€ InvalidSignalError β€” PPG data fails domain validation β†’ 400
β”œβ”€β”€ PredictionOutOfRangeError β€” Predicted BP is physiologically impossible β†’ 422
β”œβ”€β”€ EntityNotFoundError β€” Entity lookup returned nothing β†’ 404
β”œβ”€β”€ ConflictError β€” Duplicate/unique constraint violation β†’ 409
└── DatabaseError β€” DB unavailable or unrecoverable query err β†’ 503
"""
from __future__ import annotations
class DomainException(Exception):
"""
Base class for all domain-layer exceptions.
Catch this if you want to handle any business-rule violation without
knowing the specific type.
"""
def __init__(self, message: str, context: dict | None = None) -> None:
super().__init__(message)
self.message = message
self.context: dict = context or {}
def __repr__(self) -> str:
return f"{self.__class__.__name__}(message={self.message!r}, context={self.context})"
class InvalidSignalError(DomainException):
"""
Raised when a PPG signal fails domain validation.
Examples:
- Sampling rate is outside the accepted range
- Signal is too short / too long
- ``ppg_values`` list is empty
- ``duration_seconds`` does not match the number of samples
HTTP Status: 400 Bad Request
"""
def __init__(self, field: str, value: object, reason: str) -> None:
message = f"Invalid PPG signal β€” field '{field}': {reason} (got {value!r})"
super().__init__(message, context={"field": field, "value": value, "reason": reason})
self.field = field
self.value = value
self.reason = reason
class PredictionOutOfRangeError(DomainException):
"""
Raised when a model's prediction is physiologically implausible.
Examples:
- Predicted SBP < 60 mmHg or > 260 mmHg
- Predicted DBP < 30 mmHg or > 160 mmHg
- SBP < DBP (pulse pressure is negative)
HTTP Status: 422 Unprocessable Entity
"""
def __init__(self, predicted_sbp: float, predicted_dbp: float, reason: str) -> None:
message = (
f"Prediction out of physiological range β€” "
f"SBP={predicted_sbp} mmHg, DBP={predicted_dbp} mmHg: {reason}"
)
super().__init__(
message,
context={
"predicted_sbp": predicted_sbp,
"predicted_dbp": predicted_dbp,
"reason": reason,
},
)
self.predicted_sbp = predicted_sbp
self.predicted_dbp = predicted_dbp
self.reason = reason
class EntityNotFoundError(DomainException):
"""
Raised when a repository lookup fails to find the requested entity.
HTTP Status: 404 Not Found
Args:
entity_type: Class name of the entity (e.g. ``"PPGSignal"``).
entity_id: The ID that was looked up.
"""
def __init__(self, entity_type: str, entity_id: str) -> None:
message = f"{entity_type} with id='{entity_id}' not found."
super().__init__(message, context={"entity_type": entity_type, "entity_id": entity_id})
self.entity_type = entity_type
self.entity_id = entity_id
class ConflictError(DomainException):
"""
Raised when an insert/update violates a unique constraint in the database.
Typical causes:
- Duplicate PPG signal ID (re-submission of the same record)
- Unique index violation on (user_id, timestamp) composite key
HTTP Status: 409 Conflict
Args:
entity_type: Class name of the conflicting entity (e.g. ``"PPGSignal"``).
detail: Human-readable description of the conflict.
"""
def __init__(self, entity_type: str, detail: str) -> None:
message = f"Conflict on {entity_type}: {detail}"
super().__init__(message, context={"entity_type": entity_type, "detail": detail})
self.entity_type = entity_type
self.detail = detail
class DatabaseError(DomainException):
"""
Raised when the database is unreachable or an unrecoverable query error occurs.
Wraps SQLAlchemy/asyncpg low-level errors so that domain and application
layers never import SQLAlchemy directly.
HTTP Status: 503 Service Unavailable
Args:
operation: The repository operation that failed (e.g. ``"add"``, ``"get_by_id"``).
reason: Human-readable description of what went wrong.
"""
def __init__(self, operation: str, reason: str) -> None:
message = f"Database error during '{operation}': {reason}"
super().__init__(message, context={"operation": operation, "reason": reason})
self.operation = operation
self.reason = reason