Ir para o conteúdo

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 .env com ele.
  • Usar @lru_cache permite evitar ler o arquivo dotenv repetidamente a cada requisição, enquanto permite sobrescrevê-lo durante os testes.