feat(backend): initialisation du projet FastAPI

Structure en couches api / services / repositories / models, sens de
dependance unique, une session SQLAlchemy async injectee par dependance.

- Python 3.14, dependances gerees par uv et verrouillees dans uv.lock
- FastAPI expose par une factory : aucune configuration lue a l'import,
  ce qui rend tests et migrations independants de l'environnement
- Settings Pydantic, APP_SECRET_KEY et DATABASE_URL sans valeur par defaut
- Sondes /health/live et /health/ready, metriques Prometheus sur /metrics
- Lint et format ruff, mypy strict, pytest avec couverture
- Alembic branche sur DATABASE_URL et non sur alembic.ini
- Image Docker multi-stage, utilisateur non root, sonde de sante integree
This commit is contained in:
Johan LEROY
2026-09-14 12:26:20 +02:00
parent 26704831e4
commit 6161a432c3
36 changed files with 1795 additions and 0 deletions
View File
View File
+51
View File
@@ -0,0 +1,51 @@
from collections.abc import AsyncIterator
import pytest
from fastapi import FastAPI
from httpx import AsyncClient
from sqlalchemy.exc import OperationalError
from app.db.session import get_session
async def test_liveness_exposes_service_metadata(client: AsyncClient) -> None:
response = await client.get("/api/v1/health/live")
assert response.status_code == 200
assert response.json() == {
"status": "ok",
"service": "EnerVision API",
"version": "0.1.0",
"environment": "local",
}
@pytest.mark.parametrize(
"failure",
[
OperationalError("SELECT 1", {}, Exception("connexion refusee")),
ConnectionRefusedError(111, "Connection refused"),
],
ids=["erreur_sqlalchemy", "erreur_reseau_asyncpg"],
)
async def test_readiness_returns_503_when_database_is_unreachable(
app: FastAPI, client: AsyncClient, failure: Exception
) -> None:
class UnreachableSession:
async def execute(self, *_: object, **__: object) -> None:
raise failure
async def override() -> AsyncIterator[UnreachableSession]:
yield UnreachableSession()
app.dependency_overrides[get_session] = override
response = await client.get("/api/v1/health/ready")
assert response.status_code == 503
assert response.json()["detail"] == "Base de donnees injoignable"
@pytest.mark.parametrize("path", ["/openapi.json", "/metrics"])
async def test_technical_endpoints_are_served(client: AsyncClient, path: str) -> None:
assert (await client.get(path)).status_code == 200
+32
View File
@@ -0,0 +1,32 @@
import os
from collections.abc import AsyncIterator, Iterator
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from app.core.config import get_settings
from app.main import create_app
@pytest.fixture(autouse=True, scope="session")
def environment() -> Iterator[None]:
os.environ.setdefault("APP_SECRET_KEY", "secret-de-test")
os.environ.setdefault(
"DATABASE_URL", "postgresql+asyncpg://enervision:enervision@localhost:5432/enervision_test"
)
get_settings.cache_clear()
yield
get_settings.cache_clear()
@pytest.fixture
def app() -> FastAPI:
return create_app()
@pytest.fixture
async def client(app: FastAPI) -> AsyncIterator[AsyncClient]:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as async_client:
yield async_client