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
+41
View File
@@ -0,0 +1,41 @@
from functools import lru_cache
from typing import Literal
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
Environment = Literal["local", "dev", "staging", "prod"]
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_prefix="APP_",
env_file_encoding="utf-8",
extra="ignore",
)
name: str = "EnerVision API"
version: str = "0.1.0"
env: Environment = "local"
debug: bool = False
log_level: str = "INFO"
api_prefix: str = "/api/v1"
secret_key: SecretStr
cors_origins: str = ""
database_url: str = Field(validation_alias="DATABASE_URL")
database_pool_size: int = 5
database_max_overflow: int = 10
@property
def allowed_origins(self) -> list[str]:
return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
@property
def is_production(self) -> bool:
return self.env == "prod"
@lru_cache
def get_settings() -> Settings:
return Settings()
+48
View File
@@ -0,0 +1,48 @@
import logging
from logging.config import dictConfig
from app.core.config import Settings
def configure_logging(settings: Settings) -> None:
formatter = "json" if settings.is_production else "console"
dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {
"format": "%(asctime)s %(levelname)-8s %(name)s %(message)s",
},
"json": {
"()": "pythonjsonlogger.json.JsonFormatter",
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
},
},
"handlers": {
"default": {
"class": "logging.StreamHandler",
"formatter": formatter,
"stream": "ext://sys.stdout",
},
},
"root": {"handlers": ["default"], "level": settings.log_level},
"loggers": {
"uvicorn": {
"handlers": ["default"],
"level": settings.log_level,
"propagate": False,
},
"uvicorn.access": {
"handlers": ["default"],
"level": settings.log_level,
"propagate": False,
},
"sqlalchemy.engine": {"level": "WARNING"},
},
}
)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)