Configurações e Variáveis de Ambiente¶
Em muitos casos a sua aplicação pode precisar de configurações externas, como chaves secretas, credenciais de banco de dados, credenciais para serviços de email, etc.
A maioria dessas configurações é variável (podem mudar), como URLs de bancos de dados. E muitas delas podem conter dados sensíveis, como tokens secretos.
Por isso é comum prover essas configurações como variáveis de ambiente que são utilizidas pela aplicação.
Variáveis de Ambiente¶
Dica
Se você já sabe o que são variáveis de ambiente e como utilizá-las, sinta-se livre para avançar para o próximo tópico.
Uma variável de ambiente (abreviada em inglês para "env var") é uma variável definida fora do código Python, no sistema operacional, e pode ser lida pelo seu código Python (ou por outros programas).
Você pode criar e utilizar variáveis de ambiente no terminal, sem precisar utilizar Python:
// Você pode criar uma env var MY_NAME usando
$ export MY_NAME="Wade Wilson"
// E utilizá-la em outros programas, como
$ echo "Hello $MY_NAME"
Hello Wade Wilson
// Criando env var MY_NAME
$ $Env:MY_NAME = "Wade Wilson"
// Usando em outros programas, como
$ echo "Hello $Env:MY_NAME"
Hello Wade Wilson
Lendo variáveis de ambiente com Python¶
Você também pode criar variáveis de ambiente fora do Python, no terminal (ou com qualquer outro método), e realizar a leitura delas no Python.
Por exemplo, você pode definir um arquivo main.py
com o seguinte código:
import os
name = os.getenv("MY_NAME", "World")
print(f"Hello {name} from Python")
Dica
O segundo parâmetro em os.getenv()
é o valor padrão para o retorno.
Se nenhum valor for informado, None
é utilizado por padrão, aqui definimos "World"
como o valor padrão a ser utilizado.
E depois você pode executar esse arquivo:
// Aqui ainda não definimos a env var
$ python main.py
// Por isso obtemos o valor padrão
Hello World from Python
// Mas se definirmos uma variável de ambiente primeiro
$ export MY_NAME="Wade Wilson"
// E executarmos o programa novamente
$ python main.py
// Agora ele pode ler a variável de ambiente
Hello Wade Wilson from Python
Como variáveis de ambiente podem ser definidas fora do código da aplicação, mas acessadas pela aplicação, e não precisam ser armazenadas (versionadas com git
) junto dos outros arquivos, é comum utilizá-las para guardar configurações.
Você também pode criar uma variável de ambiente específica para uma invocação de um programa, que é acessível somente para esse programa, e somente enquanto ele estiver executando.
Para fazer isso, crie a variável imediatamente antes de iniciar o programa, na mesma linha:
// Criando uma env var MY_NAME na mesma linha da execução do programa
$ MY_NAME="Wade Wilson" python main.py
// Agora a aplicação consegue ler a variável de ambiente
Hello Wade Wilson from Python
// E a variável deixa de existir após isso
$ python main.py
Hello World from Python
Dica
Você pode ler mais sobre isso em: The Twelve-Factor App: Configurações.
Tipagem e Validação¶
Essas variáveis de ambiente suportam apenas strings, por serem externas ao Python e por que precisam ser compatíveis com outros programas e o resto do sistema (e até mesmo com outros sistemas operacionais, como Linux, Windows e macOS).
Isso significa que qualquer valor obtido de uma variável de ambiente em Python terá o tipo str
, e qualquer conversão para um tipo diferente ou validação deve ser realizada no código.
Pydantic Settings
¶
Por sorte, o Pydantic possui uma funcionalidade para lidar com essas configurações vindas de variáveis de ambiente utilizando Pydantic: Settings management.
Instalando pydantic-settings
¶
Primeiro, instale o pacote pydantic-settings
:
$ pip install pydantic-settings
---> 100%
Ele também está incluído no fastapi quando você instala com a opção all
:
$ pip install "fastapi[all]"
---> 100%
Info
Na v1 do Pydantic ele estava incluído no pacote principal. Agora ele está distribuido como um pacote independente para que você possa optar por instalar ou não caso você não precise dessa funcionalidade.
Criando o objeto Settings
¶
Importe a classe BaseSettings
do Pydantic e crie uma nova subclasse, de forma parecida com um modelo do Pydantic.
Os atributos da classe são declarados com anotações de tipo, e possíveis valores padrão, da mesma maneira que os modelos do Pydantic.
Você pode utilizar todas as ferramentas e funcionalidades de validação que são utilizadas nos modelos do Pydantic, como tipos de dados diferentes e validações adicionei 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,
}
Info
Na versão 1 do Pydantic você importaria BaseSettings
diretamente do módulo pydantic
em vez do módulo 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ê quiser algo pronto para copiar e colar na sua aplicação, não use esse exemplo, mas sim o exemplo abaixo.
Portanto, quando você cria uma instância da classe Settings
(nesse caso, o objeto settings
), o Pydantic lê as variáveis de ambiente sem diferenciar maiúsculas e minúsculas, por isso, uma variável maiúscula APP_NAME
será usada para o atributo app_name
.
Depois ele irá converter e validar os dados. Assim, quando você utilizar aquele objeto settings
, os dados terão o tipo que você declarou (e.g. items_per_user
será do tipo int
).
Usando o objeto settings
¶
Depois, Você pode utilizar 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,
}
Executando o servidor¶
No próximo passo, você pode inicializar o servidor passando as configurações em forma de variáveis de ambiente, por exemplo, você poderia definir ADMIN_EMAIL
e APP_NAME
da seguinte forma:
$ 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 múltiplas variáveis de ambiente para um único comando basta separá-las utilizando espaços, e incluir todas elas antes do comando.
Assim, o atributo admin_email
seria definido como "deadpool@example.com"
.
app_name
seria "ChimichangApp"
.
E items_per_user
manteria o valor padrão de 50
.
Configurações em um módulo separado¶
Você também pode incluir essas configurações em um arquivo de um módulo separado como visto em Bigger Applications - Multiple Files.
Por exemplo, você pode adicionar 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 utilizar essa configuração em 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 precisa incluir um arquivo __init__.py
como visto em Bigger Applications - Multiple Files.
Configurações em uma dependência¶
Em certas ocasiões, pode ser útil fornecer essas configurações a partir de uma dependência, em vez de definir um objeto global settings
que é utilizado em toda a aplicação.
Isso é especialmente útil durante os testes, já que é bastante simples sobrescrever uma dependência com suas configurações personalizadas.
O arquivo de configuração¶
Baseando-se no exemplo anterior, seu arquivo config.py
seria parecido com isso:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
Perceba que dessa vez não criamos uma instância padrão settings = Settings()
.
O arquivo principal da aplicação¶
Agora criamos a dependência que retorna um novo objeto 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,
}
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,
}
Dica
Utilize a versão com Annotated
se possível.
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,
}
Dica
Vamos discutir sobre @lru_cache
logo mais.
Por enquanto, você pode considerar get_settings()
como uma função normal.
E então podemos declarar essas configurações como uma dependência na função de operação da rota e utilizar 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,
}
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,
}
Dica
Utilize a versão com Annotated
se possível.
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,
}
Configurações e testes¶
Então seria muito fácil fornecer uma configuração diferente durante a execução dos testes sobrescrevendo a dependência de 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
quando instanciamos um novo objeto Settings
, e então retornamos esse novo objeto.
Após isso, podemos testar se o valor está sendo utilizado.
Lendo um arquivo .env
¶
Se você tiver muitas configurações que variem bastante, talvez em ambientes distintos, pode ser útil colocá-las em um arquivo e depois lê-las como se fossem variáveis de ambiente.
Essa prática é tão comum que possui um nome, essas variáveis de ambiente normalmente são colocadas em um arquivo .env
, e esse arquivo é chamado de "dotenv".
Dica
Um arquivo iniciando com um ponto final (.
) é um arquivo oculto em sistemas baseados em Unix, como Linux e MacOS.
Mas um arquivo dotenv não precisa ter esse nome exato.
Pydantic suporta a leitura desses tipos de arquivos utilizando uma biblioteca externa. Você pode ler mais em Pydantic Settings: Dotenv (.env) support.
Dica
Para que isso funcione você precisa executar pip install python-dotenv
.
O arquivo .env
¶
Você pode definir um arquivo .env
com o seguinte conteúdo:
ADMIN_EMAIL="deadpool@example.com"
APP_NAME="ChimichangApp"
Obtendo configurações do .env
¶
E então adicionar o seguinte código em config.py
:
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 Model Config.
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.
Info
Na versão 1 do Pydantic a configuração é realizada por uma classe interna Config
, na versão 2 do Pydantic isso é feito com o atributo model_config
. Esse atributo recebe um dict
, para utilizar o autocomplete e checagem de erros do seu editor de texto você pode importar e utilizar SettingsConfigDict
para definir esse dict
.
Aqui definimos a configuração env_file
dentro da classe Settings
do Pydantic, e definimos o valor como o nome do arquivo dotenv que queremos utilizar.
Declarando Settings
apenas uma vez com lru_cache
¶
Ler o conteúdo de um arquivo em disco normalmente é uma operação custosa (lenta), então você provavelmente quer fazer isso apenas um vez e reutilizar o mesmo objeto settings depois, em vez de ler os valores a cada requisição.
Mas cada vez que fazemos:
Settings()
um novo objeto Settings
é instanciado, e durante a instanciação, o arquivo .env
é lido novamente.
Se a função da dependência fosse apenas:
def get_settings():
return Settings()
Iriamos criar um novo objeto a cada requisição, e estaríamos lendo o arquivo .env
a cada requisição. ⚠️
Mas como estamos utilizando o decorador @lru_cache
acima, o objeto Settings
é criado apenas uma vez, na primeira vez que a função é chamada. ✔️
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,
}
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,
}
Dica
Utilize a versão com Annotated
se possível.
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,
}
Dessa forma, todas as chamadas da função get_settings()
nas dependências das próximas requisições, em vez de executar o código interno de get_settings()
e instanciar um novo objeto Settings
, irão retornar o mesmo objeto que foi retornado na primeira chamada, de novo e de novo.
Detalhes Técnicos de lru_cache
¶
@lru_cache
modifica a função decorada para retornar o mesmo valor que foi retornado na primeira vez, em vez de calculá-lo novamente, executando o código da função toda vez.
Assim, a função abaixo do decorador é executada uma única vez para cada combinação dos argumentos passados. E os valores retornados para cada combinação de argumentos são sempre reutilizados para cada nova chamada da função com a mesma combinação de argumentos.
Por exemplo, se você definir uma função:
@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
return f"Hello {salutation} {name}"
Seu programa poderia executar dessa forma:
sequenceDiagram
participant code as Código
participant function as say_hi()
participant execute as Executar Função
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Camila")
function ->> execute: executar código da função
execute ->> code: retornar o resultado
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: retornar resultado armazenado
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick")
function ->> execute: executar código da função
execute ->> code: retornar o resultado
end
rect rgba(0, 255, 0, .1)
code ->> function: say_hi(name="Rick", salutation="Mr.")
function ->> execute: executar código da função
execute ->> code: retornar o resultado
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Rick")
function ->> code: retornar resultado armazenado
end
rect rgba(0, 255, 255, .1)
code ->> function: say_hi(name="Camila")
function ->> code: retornar resultado armazenado
end
No caso da nossa dependência get_settings()
, a função não recebe nenhum argumento, então ela sempre retorna o mesmo valor.
Dessa forma, ela se comporta praticamente como uma variável global, mas ao ser utilizada como uma função de uma dependência, pode facilmente ser sobrescrita durante os testes.
@lru_cache
é definido no módulo functools
que faz parte da biblioteca padrão do Python, você pode ler mais sobre esse decorador no link Python Docs sobre @lru_cache
.
Recapitulando¶
Você pode usar o módulo Pydantic Settings para gerenciar as configurações de sua aplicação, utilizando todo o poder dos modelos Pydantic.
- Utilizar dependências simplifica os testes.
- Você pode utilizar arquivos .env junto das configurações do Pydantic.
- Utilizar o decorador
@lru_cache
evita que o arquivo .env seja lido de novo e de novo para cada requisição, enquanto permite que você sobrescreva durante os testes.