13 KiB
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.
/// tip | Consejo
Para entender las variables de entorno, puedes leer Variables de Entorno{.internal-link target=_blank}.
///
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{.internal-link target=_blank}, 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%
/// info | 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()
.
//// tab | Pydantic v2
{* ../../docs_src/settings/tutorial001.py hl[2,5:8,11] *}
////
//// tab | Pydantic v1
/// info | Información
En Pydantic v1 importarías BaseSettings
directamente desde pydantic
en lugar de desde pydantic_settings
.
///
{* ../../docs_src/settings/tutorial001_pv1.py hl[2,5:8,11] *}
////
/// tip | 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:
{* ../../docs_src/settings/tutorial001.py hl[18:20] *}
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="[email protected]" 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)
/// tip | 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 "[email protected]"
.
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{.internal-link target=_blank}.
Por ejemplo, podrías tener un archivo config.py
con:
{* ../../docs_src/settings/app01/config.py *}
Y luego usarlo en un archivo main.py
:
{* ../../docs_src/settings/app01/main.py hl[3,11:13] *}
/// tip | Consejo
También necesitarías un archivo __init__.py
como viste en Aplicaciones Más Grandes - Múltiples Archivos{.internal-link target=_blank}.
///
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:
{* ../../docs_src/settings/app02/config.py hl[10] *}
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()
.
{* ../../docs_src/settings/app02_an_py39/main.py hl[6,12:13] *}
/// tip | 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.
{* ../../docs_src/settings/app02_an_py39/main.py hl[17,19:21] *}
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
:
{* ../../docs_src/settings/app02/test_main.py hl[9:10,13,21] *}
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".
/// tip | 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.
/// tip | Consejo
Para que esto funcione, necesitas pip install python-dotenv
.
///
El archivo .env
Podrías tener un archivo .env
con:
ADMIN_EMAIL="[email protected]"
APP_NAME="ChimichangApp"
Leer configuraciones desde .env
Y luego actualizar tu config.py
con:
//// tab | Pydantic v2
{* ../../docs_src/settings/app03_an/config.py hl[9] *}
/// tip | Consejo
El atributo model_config
se usa solo para configuración de Pydantic. Puedes leer más en Pydantic: Concepts: Configuration.
///
////
//// tab | Pydantic v1
{* ../../docs_src/settings/app03_an/config_pv1.py hl[9:10] *}
/// tip | Consejo
La clase Config
se usa solo para configuración de Pydantic. Puedes leer más en Pydantic Model Config.
///
////
/// info | 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. ✔️
{* ../../docs_src/settings/app03_an_py39/main.py hl[1,11] *}
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.