Fusionne dev dans feat/endpoint-alertes-predictives

Resout les conflits additifs entre les routes alerts, recommendations
et stats mergees sur dev (PR #79, PR #82) pendant le developpement de
cette branche : deps.py, router.py, openapi.py, openapi.json,
test_openapi.py et 20-backend.md conservent desormais les trois routes.
This commit is contained in:
Johan LEROY
2026-09-16 14:25:49 +02:00
23 changed files with 1193 additions and 11 deletions
@@ -0,0 +1,72 @@
import uuid
from datetime import UTC, datetime, timedelta
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.energy import Reading, Site
from app.repositories.reading import ReadingRepository
pytestmark = pytest.mark.integration
def identifiant() -> str:
return f"SITE-{uuid.uuid4().hex[:8]}"
def lecture(site_id: str, *, timestamp: datetime, consumption_kw: float) -> Reading:
return Reading(
site_id=site_id,
timestamp=timestamp,
source="api_current",
consumption_kw=consumption_kw,
data_quality="good",
raw_data={},
)
async def test_latest_by_site_keeps_only_the_most_recent_reading(session: AsyncSession) -> None:
site_id = identifiant()
maintenant = datetime.now(UTC)
session.add(Site(site_id=site_id, site_name="Site", site_type="bureau", capacity_kw=100))
await session.flush()
session.add_all(
[
lecture(site_id, timestamp=maintenant - timedelta(hours=1), consumption_kw=10),
lecture(site_id, timestamp=maintenant, consumption_kw=42),
]
)
await session.flush()
depot = ReadingRepository(session)
resultats = await depot.latest_by_site()
consommations = [r.consumption_kw for r in resultats if r.site_id == site_id]
await session.rollback()
assert consommations == [42]
async def test_latest_by_site_returns_one_row_per_site(session: AsyncSession) -> None:
premier, second = identifiant(), identifiant()
maintenant = datetime.now(UTC)
session.add_all(
[
Site(site_id=premier, site_name="A", site_type="bureau", capacity_kw=100),
Site(site_id=second, site_name="B", site_type="bureau", capacity_kw=200),
]
)
await session.flush()
session.add_all(
[
lecture(premier, timestamp=maintenant, consumption_kw=10),
lecture(second, timestamp=maintenant, consumption_kw=20),
]
)
await session.flush()
depot = ReadingRepository(session)
resultats = await depot.latest_by_site()
identifiants = {r.site_id for r in resultats if r.site_id in (premier, second)}
await session.rollback()
assert identifiants == {premier, second}
@@ -0,0 +1,85 @@
import uuid
from datetime import UTC, datetime
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.energy import Alert, Recommendation, Site
from app.repositories.recommendation import RecommendationRepository
pytestmark = pytest.mark.integration
MOMENT = datetime(2024, 1, 1, tzinfo=UTC)
async def creer_site(session: AsyncSession) -> str:
site_id = f"TEST-{uuid.uuid4()}"
session.add(Site(site_id=site_id, site_name="Site de test", site_type="office"))
await session.flush()
return site_id
async def creer_alerte(session: AsyncSession) -> int:
site_id = await creer_site(session)
alerte = Alert(
source_alert_id=str(uuid.uuid4()),
site_id=site_id,
source="api_mock",
timestamp=MOMENT,
type="spike",
severity="high",
message="Test",
raw_data={},
)
session.add(alerte)
await session.flush()
return alerte.alert_id
async def creer(session: AsyncSession, **overrides: object) -> Recommendation:
recommendation = Recommendation(
alert_id=overrides.get("alert_id") or await creer_alerte(session),
action=overrides.get("action", "Vérifier la consommation"),
explanation=overrides.get("explanation", "Pic détecté"),
rule_reference=overrides.get("rule_reference", f"spike-{uuid.uuid4().hex[:8]}"),
)
session.add(recommendation)
await session.flush()
return recommendation
async def test_get_by_id_returns_the_matching_recommendation(session: AsyncSession) -> None:
depot = RecommendationRepository(session)
cree = await creer(session)
trouve = await depot.get_by_id(cree.recommendation_id)
action = trouve.action if trouve else None
await session.rollback()
assert action == "Vérifier la consommation"
async def test_get_by_id_returns_nothing_for_an_unknown_identifier(
session: AsyncSession,
) -> None:
trouve = await RecommendationRepository(session).get_by_id(0)
assert trouve is None
async def test_list_all_returns_the_recommendations_sorted_by_identifier(
session: AsyncSession,
) -> None:
depot = RecommendationRepository(session)
premiere = await creer(session)
seconde = await creer(session)
recommendations = await depot.list_all()
identifiants = [
r.recommendation_id
for r in recommendations
if r.recommendation_id in (premiere.recommendation_id, seconde.recommendation_id)
]
await session.rollback()
assert identifiants == sorted(identifiants)