Gestión de Configuración Python con Variables de Entorno
Guía práctica para externalizar la configuración de aplicaciones Python usando variables de entorno y pydantic-settings. Cubre patrones de typed settings, gestión de secretos y configuración por entorno (dev/staging/prod).
Descarga abierta · sin registro · para Python, pydantic-settings, Docker
""" NutriTrack API — Módulo de Configuración
Generado con la skill: configuracion-python-variables-entorno Cliente: NutriTrack SaaS (plataforma nutricional para clínicas dietéticas) Autor: Equipo Ingeniería CULTIVA IA Stack: Python 3.12 · FastAPI · PostgreSQL · Redis · OpenAI · Stripe · K8s
Uso
from app.config import settings # singleton ya validado
@app.get("/health")
def health():
return {"env": settings.environment, "debug": settings.debug}
Variables de entorno requeridas (ver .env.example al final del archivo): ENVIRONMENT, DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, REDIS_URL, OPENAI_API_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SECRET_KEY """
from future import annotations
import sys from enum import Enum from pathlib import Path from typing import Optional
from pydantic import Field, PostgresDsn, RedisDsn, computed_field, model_validator from pydantic_settings import BaseSettings
---------------------------------------------------------------------------
Enumerados de entorno
---------------------------------------------------------------------------
class Environment(str, Enum): LOCAL = "local" STAGING = "staging" PRODUCTION = "production"
---------------------------------------------------------------------------
Modelos de configuración anidados (Pattern 7)
---------------------------------------------------------------------------
class DatabaseSettings(BaseSettings): """Conexión a PostgreSQL (Supabase en prod, local en dev)."""
host: str = Field(default="localhost", alias="DB_HOST")
port: int = Field(default=5432, alias="DB_PORT")
name: str = Field(default="nutritrack", alias="DB_NAME")
user: str = Field(default="postgres", alias="DB_USER")
# Secreto: sin default → requerido en todos los entornos
password: str = Field(alias="DB_PASSWORD")
# Read replica opcional (Pattern 9: validación cruzada)
read_replica_host: Optional[str] = Field(default=None, alias="READ_REPLICA_HOST")
read_replica_port: int = Field(default=5432, alias="READ_REPLICA_PORT")
@computed_field # type: ignore[misc]
@property
def url(self) -> str:
return (
f"postgresql+asyncpg://{self.user}:{self.password}"
f"@{self.host}:{self.port}/{self.name}"
)
@model_validator(mode="after")
def check_replica_not_same_as_primary(self) -> "DatabaseSettings":
if (
self.read_replica_host
and self.read_replica_host == self.host
and self.read_replica_port == self.port
):
raise ValueError(
"READ_REPLICA_HOST no puede apuntar al mismo servidor que el primario."
)
return self
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
class RedisSettings(BaseSettings): """Conexión a Redis / Upstash para caché de sesiones y cola de tareas."""
url: str = Field(default="redis://localhost:6379/0", alias="REDIS_URL")
max_connections: int = Field(default=20, alias="REDIS_MAX_CONNECTIONS")
# TTL de caché nutricional en segundos (12 horas por defecto)
cache_ttl_secs: int = Field(default=43200, alias="REDIS_CACHE_TTL_SECS")
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
class OpenAISettings(BaseSettings): """Parámetros del cliente OpenAI para análisis nutricional."""
api_key: str = Field(alias="OPENAI_API_KEY")
model: str = Field(default="gpt-4o", alias="OPENAI_MODEL")
max_tokens: int = Field(default=1024, alias="OPENAI_MAX_TOKENS")
temperature: float = Field(default=0.2, alias="OPENAI_TEMPERATURE")
# Límite de RPM por clínica para evitar costes desbocados
rate_limit_rpm: int = Field(default=60, alias="OPENAI_RATE_LIMIT_RPM")
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
class StripeSettings(BaseSettings): """Pagos de suscripción de clínicas."""
secret_key: str = Field(alias="STRIPE_SECRET_KEY")
webhook_secret: str = Field(alias="STRIPE_WEBHOOK_SECRET")
# Price IDs de los planes (se configuran por entorno, no hardcodeados)
price_basic: Optional[str] = Field(default=None, alias="STRIPE_PRICE_BASIC")
price_pro: Optional[str] = Field(default=None, alias="STRIPE_PRICE_PRO")
price_enterprise: Optional[str] = Field(default=None, alias="STRIPE_PRICE_ENTERPRISE")
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
class SentrySettings(BaseSettings): """Error tracking (solo staging y prod)."""
dsn: Optional[str] = Field(default=None, alias="SENTRY_DSN")
traces_rate: float = Field(default=0.1, alias="SENTRY_TRACES_SAMPLE_RATE")
# Profiles solo en staging para no impactar latencia prod
profiles_rate: float = Field(default=0.0, alias="SENTRY_PROFILES_SAMPLE_RATE")
model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
---------------------------------------------------------------------------
Settings principal (singleton)
---------------------------------------------------------------------------
class Settings(BaseSettings): """ Configuración global de NutriTrack API.
Cargada una sola vez al arranque; importar `settings` en cualquier módulo:
from app.config import settings
Variables requeridas (sin default):
DB_PASSWORD, OPENAI_API_KEY, STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET, SECRET_KEY
"""
# -- Entorno & depuración ------------------------------------------------
environment: Environment = Field(
default=Environment.LOCAL,
alias="ENVIRONMENT",
description="Entorno de ejecución: local | staging | production",
)
debug: bool = Field(
default=False,
alias="DEBUG",
description="Activa modo debug (logs verbosos, SQL echo).",
)
log_level: str = Field(
default="INFO",
alias="LOG_LEVEL",
description="Nivel de log: DEBUG | INFO | WARNING | ERROR",
)
# -- Seguridad -----------------------------------------------------------
secret_key: str = Field(
alias="SECRET_KEY",
description="Clave para firmar JWTs y CSRF tokens. Generar con: openssl rand -hex 32",
)
allowed_hosts: list[str] = Field(
default=["localhost", "127.0.0.1"],
alias="ALLOWED_HOSTS",
description="Hosts permitidos separados por coma: api.nutritrack.io,staging.nutritrack.io",
)
cors_origins: list[str] = Field(
default=["http://localhost:3000"],
alias="CORS_ORIGINS",
description="Orígenes CORS separados por coma.",
)
# -- Feature flags -------------------------------------------------------
feature_ai_analysis: bool = Field(default=True, alias="FEATURE_AI_ANALYSIS")
feature_meal_planning: bool = Field(default=False, alias="FEATURE_MEAL_PLANNING")
feature_clinic_reports: bool = Field(default=True, alias="FEATURE_CLINIC_REPORTS")
# -- Sub-settings anidados (Pattern 7) -----------------------------------
# NOTA: con env_nested_delimiter="__" se leen como DB__HOST, REDIS__URL, etc.
# Aquí optamos por instanciar cada grupo de forma explícita para máxima
# compatibilidad con secretos K8s montados sin prefijo.
database: DatabaseSettings = Field(default_factory=DatabaseSettings)
redis: RedisSettings = Field(default_factory=RedisSettings)
openai: OpenAISettings = Field(default_factory=OpenAISettings)
stripe: StripeSettings = Field(default_factory=StripeSettings)
sentry: SentrySettings = Field(default_factory=SentrySettings)
# -- Propiedades computadas (Pattern 6) ----------------------------------
@computed_field # type: ignore[misc]
@property
def is_production(self) -> bool:
return self.environment == Environment.PRODUCTION
@computed_field # type: ignore[misc]
@property
def is_local(self) -> bool:
return self.environment == Environment.LOCAL
@computed_field # type: ignore[misc]
@property
def sentry_enabled(self) -> bool:
return self.sentry.dsn is not None and not self.is_local
# -- Validación cruzada (Pattern 9) --------------------------------------
@model_validator(mode="after")
def production_checks(self) -> "Settings":
if self.is_production:
if self.debug:
raise ValueError("DEBUG=true no está permitido en ENVIRONMENT=production.")
if not self.sentry.dsn:
raise ValueError("SENTRY_DSN es obligatorio en production.")
if self.secret_key in ("changeme", "dev-secret", ""):
raise ValueError("SECRET_KEY insegura en production.")
return self
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
# Soporte para secretos montados en /run/secrets (Docker/K8s — Pattern 8)
"secrets_dir": "/run/secrets",
"extra": "ignore",
}
---------------------------------------------------------------------------
Bootstrap del singleton — Fail Fast (Pattern 2)
---------------------------------------------------------------------------
try: settings = Settings() except Exception as exc: # pydantic ValidationError o cualquier otro import textwrap border = "=" * 65 print(border) print(" NUTRITRACK API — ERROR DE CONFIGURACIÓN AL ARRANQUE") print(border)
# Si es un ValidationError de Pydantic, listar cada campo roto
if hasattr(exc, "errors"):
for err in exc.errors(): # type: ignore[union-attr]
loc = " → ".join(str(l) for l in err["loc"])
msg = err["msg"]
input_ = err.get("input", "<no proporcionado>")
print(f"\n Campo : {loc}")
print(f" Error : {msg}")
print(f" Valor : {input_}")
else:
print(textwrap.indent(str(exc), " "))
print(f"\n{border}")
print(" Revisa tu archivo .env o las variables de entorno del proceso.")
print(f"{border}\n")
sys.exit(1)
---------------------------------------------------------------------------
Utilidades de diagnóstico (solo en local/staging)
---------------------------------------------------------------------------
def dump_settings_safe() -> dict: """ Devuelve un dict con los settings actuales ocultando secretos. Útil para el endpoint /debug/config (protegido por auth). """ SECRET_FIELDS = {"password", "api_key", "secret_key", "webhook_secret"}
def _mask(key: str, value: object) -> object:
if any(s in key.lower() for s in SECRET_FIELDS):
if isinstance(value, str) and len(value) > 4:
return f"{value[:2]}{'*' * (len(value) - 4)}{value[-2:]}"
return "***"
return value
data = settings.model_dump()
return {k: _mask(k, v) for k, v in data.items()}
---------------------------------------------------------------------------
.env.example (documentación de variables — copiar a .env y rellenar)
---------------------------------------------------------------------------
── Entorno ──────────────────────────────────────────────────────────────────
ENVIRONMENT=local # local | staging | production
DEBUG=true # false en staging/prod
LOG_LEVEL=DEBUG # INFO en prod
SECRET_KEY=<openssl rand -hex 32> # REQUERIDO
── Base de datos ────────────────────────────────────────────────────────────
DB_HOST=localhost
DB_PORT=5432
DB_NAME=nutritrack_dev
DB_USER=postgres
DB_PASSWORD= # REQUERIDO
READ_REPLICA_HOST= # Opcional: host de réplica de lectura
── Redis ────────────────────────────────────────────────────────────────────
REDIS_URL=redis://localhost:6379/0
REDIS_MAX_CONNECTIONS=20
REDIS_CACHE_TTL_SECS=43200
── OpenAI ───────────────────────────────────────────────────────────────────
OPENAI_API_KEY=sk-... # REQUERIDO
OPENAI_MODEL=gpt-4o
OPENAI_MAX_TOKENS=1024
OPENAI_TEMPERATURE=0.2
OPENAI_RATE_LIMIT_RPM=60
── Stripe ───────────────────────────────────────────────────────────────────
STRIPE_SECRET_KEY=sk_test_... # REQUERIDO
STRIPE_WEBHOOK_SECRET=whsec_... # REQUERIDO
STRIPE_PRICE_BASIC=price_xxx
STRIPE_PRICE_PRO=price_yyy
STRIPE_PRICE_ENTERPRISE=price_zzz
── Sentry (solo staging/prod) ───────────────────────────────────────────────
SENTRY_DSN=https://xxx@sentry.io/yyy
SENTRY_TRACES_SAMPLE_RATE=0.1
SENTRY_PROFILES_SAMPLE_RATE=0.0
── Feature flags ────────────────────────────────────────────────────────────
FEATURE_AI_ANALYSIS=true
FEATURE_MEAL_PLANNING=false
FEATURE_CLINIC_REPORTS=true
── Seguridad ────────────────────────────────────────────────────────────────
ALLOWED_HOSTS=localhost,127.0.0.1
CORS_ORIGINS=http://localhost:3000
---------------------------------------------------------------------------
// qué_hace
Enseña a gestionar la configuración de aplicaciones Python separando código y variables de entorno con validación tipada.
// cómo_lo_hace
Proporciona patrones reutilizables con pydantic-settings para crear un singleton de settings validado al arranque, con soporte para archivos .env y secretos montados en contenedores.
// ejemplo_de_uso
Úsala para que tu aplicación Python no arranque con variables de entorno erróneas en producción. Ej.: defines un singleton de settings con pydantic-settings que valida al inicio que DATABASE_URL y SECRET_KEY existen y tienen el tipo correcto.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…
// pase_cultiva_ia
Llévate todo el arsenal con el Pase
Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.
Pago único · IVA incluido · pago seguro con Stripe.
Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.