Configurações e Variáveis de Ambiente¶
Em muitos casos, sua aplicação pode precisar de configurações externas, por exemplo chaves secretas, credenciais de banco de dados, credenciais para serviços de e-mail, etc.
A maioria dessas configurações é variável (pode mudar), como URLs de banco de dados. E muitas podem ser sensíveis, como segredos.
Por esse motivo, é comum fornecê-las em variáveis de ambiente lidas pela aplicação.
Dica
Para entender variáveis de ambiente, você pode ler Variáveis de Ambiente.
Tipagem e validação¶
Essas variáveis de ambiente só conseguem lidar com strings de texto, pois são externas ao Python e precisam ser compatíveis com outros programas e com o resto do sistema (e até com diferentes sistemas operacionais, como Linux, Windows, macOS).
Isso significa que qualquer valor lido em Python a partir de uma variável de ambiente será uma str, e qualquer conversão para um tipo diferente ou validação precisa ser feita em código.
Pydantic Settings¶
Felizmente, o Pydantic fornece uma ótima utilidade para lidar com essas configurações vindas de variáveis de ambiente com Pydantic: Settings management.
Instalar pydantic-settings¶
Primeiro, certifique-se de criar seu ambiente virtual, ativá-lo e então instalar o pacote pydantic-settings:
$ pip install pydantic-settings
---> 100%
Ele também vem incluído quando você instala os extras all com:
$ pip install "fastapi[all]"
---> 100%
Informação
No Pydantic v1 ele vinha incluído no pacote principal. Agora é distribuído como um pacote independente para que você possa optar por instalá-lo ou não, caso não precise dessa funcionalidade.
Criar o objeto Settings¶
Importe BaseSettings do Pydantic e crie uma subclasse, muito parecido com um modelo do Pydantic.
Da mesma forma que com modelos do Pydantic, você declara atributos de classe com anotações de tipo e, possivelmente, valores padrão.
Você pode usar as mesmas funcionalidades e ferramentas de validação que usa em modelos do Pydantic, como diferentes tipos de dados e validações adicionais com 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,
}
Informação
No Pydantic v1 você importaria BaseSettings diretamente de pydantic em vez de 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,
}
Dica
Se você quer algo rápido para copiar e colar, não use este exemplo, use o último abaixo.
Então, quando você cria uma instância dessa classe Settings (neste caso, no objeto settings), o Pydantic vai ler as variáveis de ambiente sem diferenciar maiúsculas de minúsculas; assim, uma variável em maiúsculas APP_NAME ainda será lida para o atributo app_name.
Em seguida, ele converterá e validará os dados. Assim, quando você usar esse objeto settings, terá dados dos tipos que declarou (por exemplo, items_per_user será um int).
Usar o settings¶
Depois você pode usar o novo objeto settings na sua aplicação:
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,
}
Executar o servidor¶
Em seguida, você executaria o servidor passando as configurações como variáveis de ambiente, por exemplo, você poderia definir ADMIN_EMAIL e APP_NAME com:
$ 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)
Dica
Para definir várias variáveis de ambiente para um único comando, basta separá-las com espaço e colocá-las todas antes do comando.
Então a configuração admin_email seria definida como "deadpool@example.com".
O app_name seria "ChimichangApp".
E items_per_user manteria seu valor padrão de 50.
Configurações em outro módulo¶
Você pode colocar essas configurações em outro arquivo de módulo como visto em Aplicações Maiores - Múltiplos Arquivos.
Por exemplo, você poderia ter um arquivo config.py com:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
settings = Settings()
E então usá-lo em um arquivo 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,
}
Dica
Você também precisaria de um arquivo __init__.py como visto em Aplicações Maiores - Múltiplos Arquivos.
Configurações em uma dependência¶
Em algumas ocasiões, pode ser útil fornecer as configurações a partir de uma dependência, em vez de ter um objeto global settings usado em todos os lugares.
Isso pode ser especialmente útil durante os testes, pois é muito fácil sobrescrever uma dependência com suas próprias configurações personalizadas.
O arquivo de configuração¶
Vindo do exemplo anterior, seu arquivo config.py poderia ser assim:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
Perceba que agora não criamos uma instância padrão settings = Settings().
O arquivo principal da aplicação¶
Agora criamos uma dependência que retorna um novo 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,
}
Dica
Vamos discutir o @lru_cache em breve.
Por enquanto, você pode assumir que get_settings() é uma função normal.
E então podemos exigi-la na função de operação de rota como dependência e usá-la onde for necessário.
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,
}
Configurações e testes¶
Então seria muito fácil fornecer um objeto de configurações diferente durante os testes criando uma sobrescrita de dependência 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,
}
Na sobrescrita da dependência definimos um novo valor para admin_email ao criar o novo objeto Settings, e então retornamos esse novo objeto.
Depois podemos testar que ele é usado.
Lendo um arquivo .env¶
Se você tiver muitas configurações que possivelmente mudam bastante, talvez em diferentes ambientes, pode ser útil colocá-las em um arquivo e então lê-las como se fossem variáveis de ambiente.
Essa prática é tão comum que tem um nome: essas variáveis de ambiente são comumente colocadas em um arquivo .env, e o arquivo é chamado de "dotenv".
Dica
Um arquivo começando com um ponto (.) é um arquivo oculto em sistemas tipo Unix, como Linux e macOS.
Mas um arquivo dotenv não precisa ter exatamente esse nome de arquivo.
O Pydantic tem suporte para leitura desses tipos de arquivos usando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support.
Dica
Para isso funcionar, você precisa executar pip install python-dotenv.
O arquivo .env¶
Você poderia ter um arquivo .env com:
ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"
Ler configurações do .env¶
E então atualizar seu config.py com:
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")
Dica
O atributo model_config é usado apenas para configuração do Pydantic. Você pode ler mais em 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"
Dica
A classe Config é usada apenas para configuração do Pydantic. Você pode ler mais em Pydantic Model Config.
Informação
Na versão 1 do Pydantic a configuração era feita em uma classe interna Config, na versão 2 do Pydantic é feita em um atributo model_config. Esse atributo recebe um dict, e para ter autocompletar e erros inline você pode importar e usar SettingsConfigDict para definir esse dict.
Aqui definimos a configuração env_file dentro da sua classe Settings do Pydantic e definimos o valor como o nome do arquivo dotenv que queremos usar.
Criando o Settings apenas uma vez com lru_cache¶
Ler um arquivo do disco normalmente é uma operação custosa (lenta), então você provavelmente vai querer fazer isso apenas uma vez e depois reutilizar o mesmo objeto de configurações, em vez de lê-lo a cada requisição.
Mas toda vez que fizermos:
Settings()
um novo objeto Settings seria criado e, na criação, ele leria o arquivo .env novamente.
Se a função de dependência fosse assim:
def get_settings():
return Settings()
criaríamos esse objeto para cada requisição e leríamos o arquivo .env para cada requisição. ⚠️
Mas como estamos usando o decorador @lru_cache por cima, o objeto Settings será criado apenas uma vez, na primeira vez em que for chamado. ✔️
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,
}
Em qualquer chamada subsequente de get_settings() nas dependências das próximas requisições, em vez de executar o código interno de get_settings() e criar um novo objeto Settings, ele retornará o mesmo objeto que foi retornado na primeira chamada, repetidamente.
Detalhes Técnicos do lru_cache¶
@lru_cache modifica a função que decora para retornar o mesmo valor que foi retornado na primeira vez, em vez de calculá-lo novamente executando o código da função todas as vezes.
Assim, a função abaixo dele será executada uma vez para cada combinação de argumentos. E então os valores retornados para cada uma dessas combinações de argumentos serão usados repetidamente sempre que a função for chamada com exatamente a mesma combinação de argumentos.
Por exemplo, se você tiver uma função:
@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
return f"Hello {salutation} {name}"
seu programa poderia executar assim:
sequenceDiagram
participant code as Code
participant function as say_hi()
participant execute as Execute function
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Camila")
function ->> execute: execute function code
execute ->> code: return the result
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: return stored result
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick")
function ->> execute: execute function code
execute ->> code: return the result
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick", salutation="Mr.")
function ->> execute: execute function code
execute ->> code: return the result
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Rick")
function ->> code: return stored result
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: return stored result
end
No caso da nossa dependência get_settings(), a função nem recebe argumentos, então ela sempre retorna o mesmo valor.
Dessa forma, ela se comporta quase como se fosse apenas uma variável global. Mas como usa uma função de dependência, podemos sobrescrevê-la facilmente para testes.
@lru_cache faz parte de functools, que faz parte da biblioteca padrão do Python; você pode ler mais sobre isso na documentação do Python para @lru_cache.
Recapitulando¶
Você pode usar Pydantic Settings para lidar com as configurações da sua aplicação, com todo o poder dos modelos Pydantic.
- Usando uma dependência você pode simplificar os testes.
- Você pode usar arquivos
.envcom ele. - Usar
@lru_cachepermite evitar ler o arquivo dotenv repetidamente a cada requisição, enquanto permite sobrescrevê-lo durante os testes.