Skip to content

Référence des modules du framework

API publique du package nene2.


nene2.http

PaginationQueryParser

Analyse les paramètres de requête limit et offset.

FastAPI Depends (recommandé) :

python
from typing import Annotated
from fastapi import Depends
from nene2.http import PaginationQueryParser

@router.get("/items")
def list_items(pagination: Annotated[PaginationQueryParser, Depends()]) -> JSONResponse:
    result = use_case.execute(pagination.limit, pagination.offset)

Héritage (basé sur Request) :

python
from nene2.http import PaginationQueryParser

pagination = PaginationQueryParser.parse(request)
# pagination.limit  → int (max 100, défaut 20)
# pagination.offset → int (défaut 0)

PaginationResponse

Encapsule un ensemble de résultats paginés.

python
from nene2.http import PaginationResponse

body = PaginationResponse(items=[...], limit=20, offset=0, total=42).to_dict()
# → {"items": [...], "limit": 20, "offset": 0, "total": 42}

problem_details_response()

Génère une réponse Problem Details RFC 9457.

python
from nene2.http import problem_details_response

return problem_details_response("not-found", "Not Found", 404, "Note 42 not found.")

PaginationQuery

Dataclass retourné par PaginationQueryParser.parse(). Contient limit: int et offset: int.

HealthCheckProtocol / HealthStatus

Contrat et type de résultat pour les health checks de l'application.

python
from nene2.http import HealthCheckProtocol, HealthStatus

class MyHealthCheck:
    def check(self) -> HealthStatus:
        return HealthStatus(status="ok")

Champs de HealthStatus : status: str ("ok" ou "error"), checks: dict[str, str]. La propriété is_healthy retourne True quand status == "ok".

ETag et requêtes conditionnelles

python
from nene2.http import check_not_modified, check_precondition, generate_etag

etag = generate_etag({"id": 1, "title": "Hello"})
# Retourne 304 quand If-None-Match correspond (GET)
check_not_modified(request, etag)
# Retourne 412 quand If-Match ne correspond pas (PUT/PATCH/DELETE)
check_precondition(request, etag)

Helpers de paramètres de requête

Analyseurs typés pour les schémas de requête courants (lèvent ValidationException sur entrée invalide) :

python
from nene2.http import query_array, query_bool, query_comma_separated, query_int, query_string

limit = query_int(request, "limit", default=20, minimum=1, maximum=100)
tags = query_comma_separated(request, "tags", max_items=10)

RequestScopedContext[T]

Conteneur de valeurs à portée de requête pour l'injection de dépendances (voir lifespan-and-app-state).

PaginationDep

Alias Depends() FastAPI pour PaginationQueryParser — préféré à l'analyse manuelle.


nene2.use_case

UseCaseProtocol[I, O]

Contrat structurel pour les UseCases synchrones.

python
from nene2.use_case import UseCaseProtocol

class MyUseCase:
    def execute(self, input_: MyInput) -> MyOutput: ...

assert isinstance(MyUseCase(), UseCaseProtocol)

AsyncUseCaseProtocol[I, O]

Contrat structurel pour les UseCases async.

python
from nene2.use_case import AsyncUseCaseProtocol

class MyAsyncUseCase:
    async def execute(self, input_: MyInput) -> MyOutput: ...

assert isinstance(MyAsyncUseCase(), AsyncUseCaseProtocol)

Note : les vérifications isinstance vérifient uniquement la présence de l'attribut. La distinction async/sync est appliquée statiquement par mypy --strict.


nene2.config

AppSettings

Classe Pydantic Settings — lit depuis les variables d'environnement et .env.

python
from nene2.config import AppSettings

cfg = AppSettings()                                   # depuis l'environnement
cfg_test = AppSettings(throttle_enabled=False)        # surcharge pour les tests

Voir la référence de configuration pour tous les champs.


nene2.middleware

ErrorHandlerMiddleware

Capture toutes les exceptions non gérées et les convertit en réponses Problem Details. Enregistrez les handlers d'exceptions de domaine via DomainExceptionHandlerProtocol.

python
from starlette.responses import Response
from nene2.http import problem_details_response
from nene2.middleware import ErrorHandlerMiddleware
from nene2.middleware.domain_exception import DomainExceptionHandlerProtocol

class NoteNotFoundExceptionHandler:
    def handles(self, exc: Exception) -> bool:
        return isinstance(exc, NoteNotFoundException)

    def handle(self, exc: Exception) -> Response:
        assert isinstance(exc, NoteNotFoundException)
        return problem_details_response("not-found", "Not Found", 404, str(exc))

# Enregistrement — passer comme liste domain_handlers :
app.add_middleware(
    ErrorHandlerMiddleware,
    debug=settings.app_debug,
    domain_handlers=[NoteNotFoundExceptionHandler()],
)

DomainExceptionHandlerProtocol nécessite deux méthodes :

MéthodeSignatureObjectif
handles(exc: Exception) -> boolRetourner True si ce handler possède l'exception
handle(exc: Exception) -> ResponseConvertir l'exception en réponse HTTP

Autres middleware

ClasseModuleRôle
SecurityHeadersMiddlewarenene2.middleware.security_headersAjouter les en-têtes de sécurité dans les réponses
RequestIdMiddlewarenene2.middleware.request_idGénérer / propager X-Request-ID
RequestLoggingMiddlewarenene2.middleware.request_loggingJournalisation structurée des requêtes / réponses
RequestSizeLimitMiddlewarenene2.middleware.request_size_limitRejeter les corps de requête trop volumineux
ThrottleMiddlewarenene2.middleware.throttleLimitation de débit par fenêtre fixe par IP

Arguments add_middleware

Starlette applique le middleware dans l'ordre inverse d'enregistrement — le dernier enregistré devient la couche la plus externe. Enregistrez ErrorHandlerMiddleware en premier pour qu'il capture toutes les exceptions de chaque autre middleware.

MiddlewareArguments nommésDéfaut
ErrorHandlerMiddlewaredebug: bool, domain_handlers: list[DomainExceptionHandlerProtocol] | NoneFalse, None
SecurityHeadersMiddleware(aucun)
RequestIdMiddleware(aucun)
RequestLoggingMiddleware(aucun)
RequestSizeLimitMiddlewaremax_bytes: int1_048_576 (1 Mio)
ThrottleMiddlewarelimit: int, window: int60, 60

ThrottleMiddleware n'a pas de flag enabled — encapsulez avec if settings.throttle_enabled: pour le désactiver.

Note — spoofing de X-Forwarded-For : La clé de limitation de débit est dérivée du premier élément de l'en-tête X-Forwarded-For, que les clients peuvent falsifier. En production, placez toujours l'application derrière un reverse proxy de confiance (nginx, Caddy, AWS ALB, etc.) qui réécrit X-Forwarded-For avant que la requête n'atteigne l'application. Voir ADR-0006 pour les détails.

Ordre d'enregistrement complet avec middleware optionnel

python
# Ordre d'enregistrement : le plus interne en premier, le plus externe en dernier.
# Starlette s'exécute en inverse — le dernier enregistré englobe tous les autres.
app.add_middleware(ErrorHandlerMiddleware, debug=settings.app_debug, domain_handlers=[...])
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(RequestIdMiddleware)
app.add_middleware(RequestLoggingMiddleware)
app.add_middleware(RequestSizeLimitMiddleware, max_bytes=settings.max_body_size)
if settings.throttle_enabled:
    app.add_middleware(ThrottleMiddleware, limit=settings.throttle_limit, window=settings.throttle_window)
# Middleware d'auth — enregistré avant CORS pour être à l'intérieur de la couche CORS
if settings.bearer_token_enabled:
    app.add_middleware(BearerTokenMiddleware, verifier=LocalTokenVerifier(settings.bearer_tokens))
if settings.api_key_enabled:
    app.add_middleware(ApiKeyAuthMiddleware, verifier=LocalTokenVerifier(settings.api_keys))
# CORS doit être la couche la plus externe — enregistrez-le en dernier.
# Les requêtes OPTIONS preflight doivent atteindre CORSMiddleware avant tout contrôle d'auth.
# Si CORSMiddleware est enregistré avant les middlewares d'auth, la couche auth devient
# la plus externe et retourne 401 sur les preflights, cassant CORS pour tous les navigateurs.
if settings.cors_enabled:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.cors_origins,
        allow_credentials=settings.cors_allow_credentials,
        allow_methods=settings.cors_allow_methods,
        allow_headers=settings.cors_allow_headers,
    )

Règle CORS + Auth : Enregistrez toujours CORSMiddleware après les middlewares d'auth. Dans l'ordre inverse de Starlette, "dernier enregistré = le plus externe" signifie que CORS englobe l'auth, de sorte que les requêtes preflight de navigateur (OPTIONS) sont traitées avant l'authentification.

setup_middlewares()

Enregistre la pile middleware complète de nene2 dans le bon ordre LIFO (y compris CORS optionnel). Préférez ceci aux appels add_middleware manuels quand vous n'avez pas besoin de middleware personnalisé.

python
from nene2.middleware import setup_middlewares

setup_middlewares(
    app,
    debug=settings.app_debug,
    domain_handlers=[NoteNotFoundExceptionHandler()],
    throttle_limit=settings.throttle_limit if settings.throttle_enabled else None,
    max_request_bytes=settings.max_body_size,
    cors_allowed_origins=settings.cors_origins if settings.cors_enabled else None,
)

Voir le guide how-to middleware-stack.

SimpleDomainHandler

Helper pour construire DomainExceptionHandlerProtocol à partir d'un type d'exception et d'un code statut.

Stockage de limitation de débit

SymboleRôle
RateLimitStorageProtocolStockage enfichable pour les compteurs de throttle
InMemoryRateLimitStorageImplémentation en processus par défaut
ThrottleMiddlewareAccepte un storage= optionnel pour les backends personnalisés

nene2.auth

LocalTokenVerifier

Vérifie les tokens contre une liste statique en utilisant secrets.compare_digest.

python
from nene2.auth import LocalTokenVerifier

verifier = LocalTokenVerifier(["token-a", "token-b"])
verifier.verify("token-a")  # True
verifier.verify("wrong")    # False

TokenVerifierProtocol / TokenIssuerProtocol

Contrats structurels pour les vérificateurs et émetteurs personnalisés (p. ex. JWT).

TokenVerificationException

Levez ceci depuis un vérificateur pour signaler un token invalide. BearerTokenMiddleware le mappe vers 401 Unauthorized.

CompositeAuthMiddleware

Règles par préfixe de chemin pour l'auth mixte (p. ex. Bearer sur /api/*, clé API sur /internal/*).

python
from nene2.auth import CompositeAuthMiddleware, CompositeAuthRule, bearer_check, api_key_check

app.add_middleware(
    CompositeAuthMiddleware,
    rules=[
        CompositeAuthRule(prefix="/api", check=bearer_check(verifier)),
        CompositeAuthRule(prefix="/internal", check=api_key_check(verifier)),
    ],
)

LocalTokenIssuer / LocalBearerJwtVerifier

Helpers de développement pour les tokens bearer signés HMAC (voir les routes protégées dans src/example/).

make_require_auth()

Factory Depends() FastAPI qui retourne 401 Problem Details quand les en-têtes d'auth sont manquants.


nene2.database

SqlAlchemyQueryExecutor

Exécute du SQL paramétré via SQLAlchemy Core.

python
from nene2.database import SqlAlchemyQueryExecutor

executor = SqlAlchemyQueryExecutor(engine)
rows = executor.fetch_all("SELECT * FROM notes WHERE id = :id", {"id": 1})
executor.write("INSERT INTO notes (title, body) VALUES (:t, :b)", {"t": "t", "b": "b"})

Valeur de retour de write()

write() retourne un int dont la signification dépend de l'opération SQL :

OpérationValeur de retour
INSERT avec AUTOINCREMENT / SERIALlastrowid — la clé primaire de la nouvelle ligne (toujours > 0)
INSERT sans auto-PK, ou INSERT multi-lignesrowcount — nombre de lignes insérées
UPDATE / DELETErowcount — lignes affectées (0 si rien ne correspondait)

Utilisez lastrowid pour reconstruire l'entité après un INSERT d'une seule ligne :

python
new_id = executor.write("INSERT INTO notes (title) VALUES (:title)", {"title": "Hello"})
return Note(id=new_id, title="Hello")

Utilisez rowcount pour détecter une ligne manquante sur UPDATE / DELETE :

python
affected = executor.write("UPDATE notes SET title=:title WHERE id=:id", {"title": t, "id": pk})
if affected == 0:
    raise NoteNotFoundException(pk)

SqlAlchemyTransactionManager

Gère les transactions. Préférez transactional() aux appels manuels begin/commit/rollback.

python
from nene2.database import SqlAlchemyTransactionManager

mgr = SqlAlchemyTransactionManager(engine)

result = mgr.transactional(
    lambda ex: ex.fetch_one("SELECT COUNT(*) AS cnt FROM notes")
)

Combiner transactional() avec le schéma Repository

Quand un UseCase doit effectuer plusieurs écritures atomiques, définissez des variantes _in_tx sur l'interface du repository qui acceptent un executor explicite. Le UseCase passe l'executor lié à la transaction depuis le callback vers chaque méthode _in_tx.

Interface du repository :

python
from nene2.database import DatabaseQueryExecutorInterface
from abc import ABC, abstractmethod

class AccountRepositoryInterface(ABC):
    # Méthodes standard — utilise self._executor (auto-commit)
    @abstractmethod
    def find_by_id(self, account_id: int) -> Account | None: ...

    # Variantes _in_tx — appeler uniquement dans un callback transactional()
    @abstractmethod
    def find_by_id_in_tx(
        self, executor: DatabaseQueryExecutorInterface, account_id: int
    ) -> Account | None: ...

    @abstractmethod
    def update_balance_in_tx(
        self, executor: DatabaseQueryExecutorInterface, account_id: int, delta: int
    ) -> None: ...

UseCase (exemple de virement atomique) :

python
from nene2.database import DatabaseQueryExecutorInterface, DatabaseTransactionManagerInterface

class TransferUseCase:
    def __init__(
        self,
        transaction_manager: DatabaseTransactionManagerInterface,
        account_repo: AccountRepositoryInterface,
        transfer_repo: TransferRepositoryInterface,
    ) -> None:
        self._tx = transaction_manager
        self._accounts = account_repo
        self._transfers = transfer_repo

    def execute(self, input_: TransferInput) -> Transfer:
        def _run(executor: DatabaseQueryExecutorInterface) -> Transfer:
            source = self._accounts.find_by_id_in_tx(executor, input_.from_account_id)
            if source is None:
                raise AccountNotFoundException(input_.from_account_id)
            if source.balance_cents < input_.amount_cents:
                raise InsufficientBalanceException(...)

            self._accounts.update_balance_in_tx(executor, input_.from_account_id, -input_.amount_cents)
            self._accounts.update_balance_in_tx(executor, input_.to_account_id, input_.amount_cents)
            return self._transfers.create_in_tx(executor, input_.from_account_id, input_.to_account_id, input_.amount_cents)

        return self._tx.transactional(_run)

transactional() utilise engine.begin() en interne — toute exception dans le callback déclenche un rollback automatique.

Tests avec InMemory : Implémentez DatabaseTransactionManagerInterface avec un executor no-op qui appelle le callback directement. Les méthodes _in_tx sur le repository InMemory ignorent l'executor et opèrent sur leur store en mémoire.

DatabaseHealthCheck

Implémente HealthCheckProtocol — vérifie la connexion à la base de données et retourne un HealthStatus.

python
from nene2.database import DatabaseHealthCheck
from nene2.http import HealthStatus

health = DatabaseHealthCheck(engine)
status: HealthStatus = health.check()
# status.status → "ok" ou "error"
# status.checks → {"db": "ok"} ou {"db": "error: <message>"}

DatabaseConnectionException

Levée par DatabaseHealthCheck ou les opérations du repository quand la base de données est inaccessible.


nene2.mcp

LocalMcpServer

Encapsule FastMCP — enregistre des fonctions UseCase comme outils MCP.

python
from nene2.mcp import LocalMcpServer

server = LocalMcpServer("my-server", instructions="...")

@server.tool("List all notes.")
def list_notes(limit: int = 20, offset: int = 0) -> list[dict]: ...

server.run(transport="stdio")

HttpxMcpClient

Client HTTP pour appeler une API nene2 depuis des handlers d'outils MCP.

python
from nene2.mcp import HttpxMcpClient

client = HttpxMcpClient("bearer-token")
response = client.get("http://localhost:8080", "/notes")
response.is_successful()   # True
response.body              # str — texte de réponse brut
response.status_code       # int
response.request_id()      # str | None — valeur de l'en-tête X-Request-ID

McpHttpResponse

Type de retour des méthodes HttpxMcpClient.

Champs : status_code: int, headers: dict[str, str], body: str (texte de réponse brut).

Méthodes :

  • is_successful() -> boolTrue quand 200 ≤ status_code < 300
  • request_id() -> str | None — retourne la valeur de l'en-tête X-Request-ID, ou None

McpHttpClientProtocol

Contrat structurel pour les clients HTTP MCP personnalisés. Implémentez get(), post(), put(), delete() retournant McpHttpResponse, et has_authentication() -> bool.


nene2.log

setup_logging()

Initialise structlog. Bascule entre ConsoleRenderer (local) et JSON (production).

python
from nene2.log import setup_logging

setup_logging(app_env="production")  # rendu JSON
setup_logging(app_env="local")       # rendu Console

nene2.validation

ValidationException / ValidationError

Levez ValidationException à la frontière HTTP pour retourner 422 Unprocessable Entity.

python
from nene2.validation.exceptions import ValidationError, ValidationException

errors = [ValidationError("body", "Body must not be empty.", "required")]
raise ValidationException(errors)

nene2.cache

TtlCache[V]

Cache en mémoire thread-safe avec expiration TTL par clé. Utilisez pour les clés d'idempotence, les lookups à courte durée de vie, ou les auxiliaires de limitation de débit.

python
from nene2.cache import TtlCache

cache: TtlCache[str] = TtlCache(ttl_seconds=60.0)
cache.set("key", "value")
cache.get("key")  # str | None

Voir le guide how-to lifespan-and-app-state pour le câblage app.state.


nene2.security

verify_hmac_signature()

Vérification HMAC protégée contre les attaques temporelles pour les endpoints webhook.

python
from nene2.security import verify_hmac_signature

if not verify_hmac_signature(body, signature_header, secret.get_secret_value()):
    return problem_details_response("unauthorized", "Unauthorized", 401, "Invalid signature.")

Voir le guide how-to webhook.

Publié sous la licence MIT.