← Volver al catálogo
IA, ingeniería y MLOpsReferenciaIntermedioGratis

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).

Descargar SKILL.md

Descarga abierta · sin registro · para Python, pydantic-settings, Docker

// resultado_de_ejemplo

""" 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

Pythonpydantic-settingsDocker
Categoría
IA, ingeniería y MLOps
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// 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 €.