Files
ENI-projet-piscine/apps/backend/app/services/reading.py
T
Johan LEROYandClaude Sonnet 5 5d921a9b1e feat: vue détail d'un site (#51)
Ajoute la page de détail d'un site (fiche, mesure instantanée, jauge de
consommation, historique) en remplacement du placeholder. Les champs
null sont affichés explicitement avec leur raison plutôt que masqués.

Ajoute l'endpoint GET /sites/{id}/current côté backend, qui retourne la
dernière mesure connue d'un site sans filtre temporel, conformément au
contrat de l'issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012i5NteMLRZgfTAKB5GD37X
2026-09-17 16:43:15 +02:00

63 lines
2.0 KiB
Python

from collections.abc import Sequence
from datetime import UTC, datetime, timedelta
from app.models.energy import Reading
from app.repositories.reading import ReadingRepository
FENETRE_PAR_DEFAUT = timedelta(hours=24)
FENETRE_MAXIMALE = timedelta(days=90)
class FenetreInverseeError(Exception):
"""`start` est postérieur ou égal à `end`."""
class FenetreTropLargeError(Exception):
"""L'écart entre `start` et `end` dépasse `FENETRE_MAXIMALE`."""
class ReadingService:
def __init__(self, *, readings: ReadingRepository) -> None:
self._readings = readings
async def get_latest(self, site_id: str) -> Reading | None:
return await self._readings.latest_for_site(site_id)
async def list_history(
self,
*,
site_id: str | None = None,
start: datetime | None = None,
end: datetime | None = None,
limit: int,
offset: int,
) -> Sequence[Reading]:
debut, fin = self._resoudre_fenetre(start, end)
return await self._readings.list_history(
site_id=site_id, start=debut, end=fin, limit=limit, offset=offset
)
@staticmethod
def _resoudre_fenetre(
start: datetime | None, end: datetime | None
) -> tuple[datetime, datetime]:
# Piège : un datetime naïf (sans fuseau dans la chaîne ISO reçue) fait échouer la
# comparaison à `reading.timestamp` (`timestamptz`) au niveau du pilote, en 500 plutôt
# qu'un refus propre. On le traite comme de l'UTC plutôt que de le rejeter.
debut = _vers_utc(start)
fin = _vers_utc(end) or datetime.now(UTC)
if debut is None:
debut = fin - FENETRE_PAR_DEFAUT
if debut >= fin:
raise FenetreInverseeError
if fin - debut > FENETRE_MAXIMALE:
raise FenetreTropLargeError
return debut, fin
def _vers_utc(instant: datetime | None) -> datetime | None:
if instant is None:
return None
return instant if instant.tzinfo is not None else instant.replace(tzinfo=UTC)