Files
lifetrack/apps/api/app/core/timeutils.py
T
MeeJayandClaude Opus 5 93f0689c1e Initial import: LifeTrack v1 (santé, vape, finances)
Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de
pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et
finances personnelles avec import de relevés bancaires.

Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS +
Vite + Tailwind + ECharts, déploiement Docker Compose. Modules
auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework
de connecteurs à deux voies (importeurs de fichiers + ingestion JSON)
pour brancher de nouvelles sources sans toucher au noyau.

Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié
contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16
(28 tables, SPA servie par nginx, wizard de premier démarrage).

Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les
documents de conception et de recherche dans docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:48:57 +02:00

58 lines
2.0 KiB
Python

from datetime import UTC, date, datetime
from typing import Annotated
from zoneinfo import ZoneInfo
from pydantic import AfterValidator
from app.core.config import get_settings
from app.core.errors import DomainValidationError
def utcnow() -> datetime:
return datetime.now(UTC)
def resolve_tz(tz: str | None) -> ZoneInfo:
name = tz or get_settings().timezone
try:
return ZoneInfo(name)
except Exception as exc:
raise DomainValidationError(f"Fuseau horaire inconnu : {name}") from exc
def local_day(dt_utc: datetime, tz: ZoneInfo) -> date:
return dt_utc.astimezone(tz).date()
def as_utc(value: datetime) -> datetime:
"""Normalise a *stored* instant to UTC-aware.
Every instant is persisted in UTC (CONVENTIONS C2.3), but the driver decides
what comes back: PostgreSQL `TIMESTAMPTZ` yields an aware datetime while
SQLite yields a naive one. Stamping UTC on naive values keeps responses
compliant with the `Z` suffix required by architecture §8.4 on both engines.
"""
return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
def require_utc(value: datetime) -> datetime:
"""Validate an *inbound* instant: naive is refused, aware is converted to UTC.
Architecture §8.4: "entrée avec offset acceptée et convertie en UTC ; entrée
naïve refusée (422)". Silently reading a naive datetime as UTC would record a
local wall-clock time off by the client's offset.
"""
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError(
"Horodatage sans fuseau horaire : précisez le décalage "
"(par exemple « 2026-08-13T06:31:00Z »)."
)
return value.astimezone(UTC)
# Use in request schemas (XxxCreate/XxxUpdate) for any client-supplied instant.
UtcDatetime = Annotated[datetime, AfterValidator(require_utc)]
# Use in response schemas (XxxRead) for any instant read back from the database.
StoredUtcDatetime = Annotated[datetime, AfterValidator(as_utc)]