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
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,
}
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,
}
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,
}
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")
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"
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 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,
}
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 library 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.