Saltar a contenido

Configuraciones y Variables de Entorno

En muchos casos, tu aplicaci贸n podr铆a necesitar algunas configuraciones o ajustes externos, por ejemplo, claves secretas, credenciales de base de datos, credenciales para servicios de correo electr贸nico, etc.

La mayor铆a de estas configuraciones son variables (pueden cambiar), como las URLs de bases de datos. Y muchas podr铆an ser sensibles, como los secretos.

Por esta raz贸n, es com煤n proporcionarlas en variables de entorno que son le铆das por la aplicaci贸n.

Consejo

Para entender las variables de entorno, puedes leer Variables de Entorno.

Tipos y validaci贸n

Estas variables de entorno solo pueden manejar strings de texto, ya que son externas a Python y tienen que ser compatibles con otros programas y el resto del sistema (e incluso con diferentes sistemas operativos, como Linux, Windows, macOS).

Eso significa que cualquier valor le铆do en Python desde una variable de entorno ser谩 un str, y cualquier conversi贸n a un tipo diferente o cualquier validaci贸n tiene que hacerse en c贸digo.

Pydantic Settings

Afortunadamente, Pydantic proporciona una gran utilidad para manejar estas configuraciones provenientes de variables de entorno con Pydantic: Settings management.

Instalar pydantic-settings

Primero, aseg煤rate de crear tu entorno virtual, act铆valo y luego instala el paquete pydantic-settings:

$ pip install pydantic-settings
---> 100%

Tambi茅n viene incluido cuando instalas los extras all con:

$ pip install "fastapi[all]"
---> 100%

Informaci贸n

En Pydantic v1 ven铆a incluido con el paquete principal. Ahora se distribuye como este paquete independiente para que puedas elegir si instalarlo o no si no necesitas esa funcionalidad.

Crear el objeto Settings

Importa BaseSettings de Pydantic y crea una sub-clase, muy similar a un modelo de Pydantic.

De la misma forma que con los modelos de Pydantic, declaras atributos de clase con anotaciones de tipos, y posiblemente, valores por defecto.

Puedes usar todas las mismas funcionalidades de validaci贸n y herramientas que usas para los modelos de Pydantic, como diferentes tipos de datos y validaciones adicionales con Field().

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Informaci贸n

En Pydantic v1 importar铆as BaseSettings directamente desde pydantic en lugar de desde pydantic_settings.

from fastapi import FastAPI
from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Consejo

Si quieres algo r谩pido para copiar y pegar, no uses este ejemplo, usa el 煤ltimo m谩s abajo.

Luego, cuando creas una instance de esa clase Settings (en este caso, en el objeto settings), Pydantic leer谩 las variables de entorno de una manera indiferente a may煤sculas y min煤sculas, por lo que una variable en may煤sculas APP_NAME a煤n ser谩 le铆da para el atributo app_name.

Luego convertir谩 y validar谩 los datos. As铆 que, cuando uses ese objeto settings, tendr谩s datos de los tipos que declaraste (por ejemplo, items_per_user ser谩 un int).

Usar el settings

Luego puedes usar el nuevo objeto settings en tu aplicaci贸n:

from fastapi import FastAPI
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()
app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Ejecutar el servidor

Luego, ejecutar铆as el servidor pasando las configuraciones como variables de entorno, por ejemplo, podr铆as establecer un ADMIN_EMAIL y APP_NAME con:

$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" fastapi run main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Consejo

Para establecer m煤ltiples variables de entorno para un solo comando, simplemente sep谩ralas con un espacio y ponlas todas antes del comando.

Y luego la configuraci贸n admin_email se establecer铆a en "deadpool@example.com".

El app_name ser铆a "ChimichangApp".

Y el items_per_user mantendr铆a su valor por defecto de 50.

Configuraciones en otro m贸dulo

Podr铆as poner esas configuraciones en otro archivo de m贸dulo como viste en Aplicaciones M谩s Grandes - M煤ltiples Archivos.

Por ejemplo, podr铆as tener un archivo config.py con:

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50


settings = Settings()

Y luego usarlo en un archivo main.py:

from fastapi import FastAPI

from .config import settings

app = FastAPI()


@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Consejo

Tambi茅n necesitar铆as un archivo __init__.py como viste en Aplicaciones M谩s Grandes - M煤ltiples Archivos.

Configuraciones en una dependencia

En algunas ocasiones podr铆a ser 煤til proporcionar las configuraciones desde una dependencia, en lugar de tener un objeto global con settings que se use en todas partes.

Esto podr铆a ser especialmente 煤til durante las pruebas, ya que es muy f谩cil sobrescribir una dependencia con tus propias configuraciones personalizadas.

El archivo de configuraci贸n

Proveniente del ejemplo anterior, tu archivo config.py podr铆a verse como:

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50
馃 Other versions and variants
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

Nota que ahora no creamos una instance por defecto settings = Settings().

El archivo principal de la app

Ahora creamos una dependencia que devuelve un nuevo config.Settings().

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
馃 Other versions and variants
from functools import lru_cache

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Consejo

Hablaremos del @lru_cache en un momento.

Por ahora puedes asumir que get_settings() es una funci贸n normal.

Y luego podemos requerirlo desde la path operation function como una dependencia y usarlo donde lo necesitemos.

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
馃 Other versions and variants
from functools import lru_cache

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from .config import Settings

app = FastAPI()


@lru_cache
def get_settings():
    return Settings()


@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Configuraciones y pruebas

Luego ser铆a muy f谩cil proporcionar un objeto de configuraciones diferente durante las pruebas al sobrescribir una dependencia para get_settings:

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }
馃 Other versions and variants
from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }

Tip

Prefer to use the Annotated version if possible.

from fastapi.testclient import TestClient

from .config import Settings
from .main import app, get_settings

client = TestClient(app)


def get_settings_override():
    return Settings(admin_email="testing_admin@example.com")


app.dependency_overrides[get_settings] = get_settings_override


def test_app():
    response = client.get("/info")
    data = response.json()
    assert data == {
        "app_name": "Awesome API",
        "admin_email": "testing_admin@example.com",
        "items_per_user": 50,
    }

En la dependencia sobreescrita establecemos un nuevo valor para el admin_email al crear el nuevo objeto Settings, y luego devolvemos ese nuevo objeto.

Luego podemos probar que se est谩 usando.

Leer un archivo .env

Si tienes muchas configuraciones que posiblemente cambien mucho, tal vez en diferentes entornos, podr铆a ser 煤til ponerlos en un archivo y luego leerlos desde all铆 como si fueran variables de entorno.

Esta pr谩ctica es lo suficientemente com煤n que tiene un nombre, estas variables de entorno generalmente se colocan en un archivo .env, y el archivo se llama un "dotenv".

Consejo

Un archivo que comienza con un punto (.) es un archivo oculto en sistemas tipo Unix, como Linux y macOS.

Pero un archivo dotenv realmente no tiene que tener ese nombre exacto.

Pydantic tiene soporte para leer desde estos tipos de archivos usando un paquete externo. Puedes leer m谩s en Pydantic Settings: Dotenv (.env) support.

Consejo

Para que esto funcione, necesitas pip install python-dotenv.

El archivo .env

Podr铆as tener un archivo .env con:

ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"

Leer configuraciones desde .env

Y luego actualizar tu config.py con:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")
馃 Other versions and variants
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")

Tip

Prefer to use the Annotated version if possible.

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    model_config = SettingsConfigDict(env_file=".env")

Consejo

El atributo model_config se usa solo para configuraci贸n de Pydantic. Puedes leer m谩s en Pydantic: Concepts: Configuration.

from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    class Config:
        env_file = ".env"
馃 Other versions and variants
from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    class Config:
        env_file = ".env"

Tip

Prefer to use the Annotated version if possible.

from pydantic import BaseSettings


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    admin_email: str
    items_per_user: int = 50

    class Config:
        env_file = ".env"

Consejo

La clase Config se usa solo para configuraci贸n de Pydantic. Puedes leer m谩s en Pydantic Model Config.

Informaci贸n

En la versi贸n 1 de Pydantic la configuraci贸n se hac铆a en una clase interna Config, en la versi贸n 2 de Pydantic se hace en un atributo model_config. Este atributo toma un dict, y para obtener autocompletado y errores en l铆nea, puedes importar y usar SettingsConfigDict para definir ese dict.

Aqu铆 definimos la configuraci贸n env_file dentro de tu clase Pydantic Settings, y establecemos el valor en el nombre del archivo con el archivo dotenv que queremos usar.

Creando el Settings solo una vez con lru_cache

Leer un archivo desde el disco es normalmente una operaci贸n costosa (lenta), por lo que probablemente quieras hacerlo solo una vez y luego reutilizar el mismo objeto de configuraciones, en lugar de leerlo para cada request.

Pero cada vez que hacemos:

Settings()

se crear铆a un nuevo objeto Settings, y al crearse leer铆a el archivo .env nuevamente.

Si la funci贸n de dependencia fuera simplemente as铆:

def get_settings():
    return Settings()

crear铆amos ese objeto para cada request, y estar铆amos leyendo el archivo .env para cada request. 鈿狅笍

Pero como estamos usando el decorador @lru_cache encima, el objeto Settings se crear谩 solo una vez, la primera vez que se llame. 鉁旓笍

from functools import lru_cache
from typing import Annotated

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }
馃 Other versions and variants
from functools import lru_cache

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: Annotated[config.Settings, Depends(get_settings)]):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Tip

Prefer to use the Annotated version if possible.

from functools import lru_cache

from fastapi import Depends, FastAPI

from . import config

app = FastAPI()


@lru_cache
def get_settings():
    return config.Settings()


@app.get("/info")
async def info(settings: config.Settings = Depends(get_settings)):
    return {
        "app_name": settings.app_name,
        "admin_email": settings.admin_email,
        "items_per_user": settings.items_per_user,
    }

Entonces, para cualquier llamada subsiguiente de get_settings() en las dependencias de los pr贸ximos requests, en lugar de ejecutar el c贸digo interno de get_settings() y crear un nuevo objeto Settings, devolver谩 el mismo objeto que fue devuelto en la primera llamada, una y otra vez.

Detalles T茅cnicos de lru_cache

@lru_cache modifica la funci贸n que decora para devolver el mismo valor que se devolvi贸 la primera vez, en lugar de calcularlo nuevamente, ejecutando el c贸digo de la funci贸n cada vez.

As铆 que la funci贸n debajo se ejecutar谩 una vez por cada combinaci贸n de argumentos. Y luego, los valores devueltos por cada una de esas combinaciones de argumentos se utilizar谩n una y otra vez cada vez que la funci贸n sea llamada con exactamente la misma combinaci贸n de argumentos.

Por ejemplo, si tienes una funci贸n:

@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
    return f"Hello {salutation} {name}"

tu programa podr铆a ejecutarse as铆:

sequenceDiagram

participant code as C贸digo
participant function as say_hi()
participant execute as Ejecutar funci贸n

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Camila")
        function ->> execute: ejecutar c贸digo de la funci贸n
        execute ->> code: devolver el resultado
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: devolver resultado almacenado
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick")
        function ->> execute: ejecutar c贸digo de la funci贸n
        execute ->> code: devolver el resultado
    end

    rect rgba(0, 255, 0, .1)
        code ->> function: say_hi(name="Rick", salutation="Mr.")
        function ->> execute: ejecutar c贸digo de la funci贸n
        execute ->> code: devolver el resultado
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Rick")
        function ->> code: devolver resultado almacenado
    end

    rect rgba(0, 255, 255, .1)
        code ->> function: say_hi(name="Camila")
        function ->> code: devolver resultado almacenado
    end

En el caso de nuestra dependencia get_settings(), la funci贸n ni siquiera toma argumentos, por lo que siempre devolver谩 el mismo valor.

De esa manera, se comporta casi como si fuera solo una variable global. Pero como usa una funci贸n de dependencia, entonces podemos sobrescribirla f谩cilmente para las pruebas.

@lru_cache es parte de functools, que es parte del paquete est谩ndar de Python, puedes leer m谩s sobre 茅l en las docs de Python para @lru_cache.

Resumen

Puedes usar Pydantic Settings para manejar las configuraciones o ajustes de tu aplicaci贸n, con todo el poder de los modelos de Pydantic.

  • Al usar una dependencia, puedes simplificar las pruebas.
  • Puedes usar archivos .env con 茅l.
  • Usar @lru_cache te permite evitar leer el archivo dotenv una y otra vez para cada request, mientras te permite sobrescribirlo durante las pruebas.