Usamos Python 3.12 o posterior. Si vienes de versiones anteriores, repasa sobre todo las anotaciones de tipos (list[str], X | None, Annotated) y async / await. FastAPI y Pydantic se apoyan en los tipos para validar, documentar y autocompletar.
Define endpoints con funciones de Python anotadas. A partir de los tipos valida la entrada, serializa la salida y genera la documentación interactiva en /docs.
Dependencias (Depends) para sesión de base de datos, usuario actual y permisos. Es el mecanismo central: apréndelo bien.
Routers por dominio (APIRouter), montados en main.py.
lifespan para abrir y cerrar conexiones al arrancar y apagar.
Los esquemas de entrada y salida de la API son modelos de Pydantic. La configuración (base de datos, llaves, URLs de integraciones) se lee del entorno con BaseSettings. Nunca se escribe una credencial en el código.
Si vienes de Pydantic v1: .dict() ahora es .model_dump(), orm_mode ahora es from_attributes y los validadores cambiaron de nombre.
Modelos con Mapped[...] y mapped_column, consultas con select() y sesión async con el driver asyncpg. Si tu referencia es SQLAlchemy 1.x, lee el tutorial completo: la API de consultas cambió.
Para lo que no debe hacer esperar a la persona: revisar vencimientos, sincronizar con un ERP, generar un reporte. Redis funciona como intermediario de mensajes.
httpx para llamar APIs externas con async. tenacity para reintentar fallas temporales con espera creciente. Cada cliente de integración vive en su propio módulo.
Las reglas de negocio tienen pruebas unitarias sin base de datos. Los endpoints, pruebas con el cliente de pruebas de FastAPI.
ruff da formato y revisa estilo. Lo que ruff marca se corrige antes de pedir revisión.
Docker Compose levanta el entorno completo (api, db, redis, worker) con un solo comando. Lo que corre en tu máquina es lo mismo que corre en el servidor.
La regla de Finanzas del análisis, escrita en el servidor. Primero la regla, en Python puro:
app/aprobaciones/rules.py
from dataclasses import dataclass
from decimal import Decimal
from typing import Literal
Tipo = Literal["compra", "gasto", "contratacion", "presupuesto"]
@dataclass(frozen=True)
class Solicitud:
tipo: Tipo
area: str
monto: Decimal
@dataclass(frozen=True)
class Resultado:
regla: str
aplica: bool
razon: str
decide: str
UMBRAL_FINANZAS = Decimal("20000")
def finanzas(s: Solicitud) -> Resultado:
es_gasto = s.tipo in ("compra", "gasto")
return Resultado(
regla="Compras y gastos sobre el umbral pasan por Finanzas",
aplica=es_gasto and s.monto > UMBRAL_FINANZAS,
razon=f"{s.monto} vs {UMBRAL_FINANZAS}" if es_gasto else "No es compra ni gasto",
decide="Finanzas",
)
Su prueba, sin base de datos ni HTTP:
tests/aprobaciones/test_rules.py
from decimal import Decimal
from app.aprobaciones.rules import Solicitud, finanzas
def test_compra_sobre_umbral_pasa_por_finanzas():
r = finanzas(Solicitud(tipo="compra", area="Operaciones", monto=Decimal("25000")))
assert r.aplica and r.decide == "Finanzas"
def test_contratacion_no_pasa_por_finanzas():
r = finanzas(Solicitud(tipo="contratacion", area="Personas", monto=Decimal("90000")))
assert not r.aplica
Y el endpoint, delgado: valida, llama al servicio y responde.
app/aprobaciones/router.py
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import Usuario, usuario_actual
from app.db import sesion
from . import service
from .rules import Tipo
router = APIRouter(prefix="/solicitudes", tags=["aprobaciones"])
Sesion = Annotated[AsyncSession, Depends(sesion)]
Yo = Annotated[Usuario, Depends(usuario_actual)]
class SolicitudIn(BaseModel):
tipo: Tipo
area: str
monto: Decimal = Field(gt=0)
class SolicitudOut(BaseModel):
id: int
estado: str
ruta: list[str]
@router.post("", response_model=SolicitudOut, status_code=201)
async def crear(datos: SolicitudIn, db: Sesion, yo: Yo):
return await service.crear(db, datos, autor=yo)