Simplifique suas configurações do Django com dicas de tipo: um tutorial do Pydantic

Publicados: 2022-07-22

Projetos Django costumavam me frustrar porque eu não tinha uma maneira robusta e escalável de adicionar novos ambientes. Ao reunir dicas do tipo pydantic e Python, construí a base poderosa de que precisava.

Conforme descrito no PEP 484, as dicas de tipo oferecem suporte à análise estática, mas essas mesmas anotações também estão disponíveis em tempo de execução. Pacotes de terceiros, como pydantic, oferecem verificação de tipo de tempo de execução que usa esses metadados adicionais. Pydantic usa dicas de tipo Python para ajudar a gerenciar metadados de configurações e realizar validação de dados de tempo de execução.

Este tutorial pydantic mostrará os efeitos positivos e de longo alcance do uso do gerenciamento de configurações pydantic com o Django.

Nossa configuração segue as melhores práticas descritas no site do aplicativo Twelve-Factor:

  1. Defina configurações não constantes e secretas como variáveis ​​de ambiente.
  2. Em ambientes de desenvolvimento, defina as variáveis ​​de ambiente em um arquivo .env e inclua o .env em .gitignore .
  3. Use os mecanismos do provedor de nuvem para definir variáveis ​​de ambiente (secretas) para os ambientes de controle de qualidade, preparação e produção.
  4. Use um único arquivo settings.py que se configura a partir das variáveis ​​de ambiente.
  5. Use pydantic para ler, verificar, validar e converter variáveis ​​de ambiente em variáveis ​​Python que definem as configurações do Django.

Como alternativa, alguns desenvolvedores criam vários arquivos de configurações, como settings_dev.py e settings_prod.py . Infelizmente, essa abordagem não escala bem. Isso leva à duplicação de código, confusão, bugs difíceis de encontrar e maiores esforços de manutenção.

Usando as melhores práticas mencionadas, adicionar qualquer número de ambientes é fácil, bem definido e à prova de erros. Embora possamos explorar uma configuração de ambiente mais complicada, vamos nos concentrar em duas para maior clareza: desenvolvimento e produção.

Como é isso na prática?

Gerenciamento de configurações do Pydantic e variáveis ​​de ambiente

Agora nos concentramos em um exemplo em desenvolvimento e produção. Mostramos como cada ambiente configura suas configurações de forma diferente e como o pydantic suporta cada um.

Nosso aplicativo de exemplo requer um banco de dados compatível com Django, então precisamos armazenar a string de conexão do banco de dados. Movemos as informações de configuração de conexão do banco de dados para uma variável de ambiente, DATABASE_URL , usando o pacote Python dj-database-url. Observe que esta variável é do tipo str e está formatada da seguinte forma:

 postgres://{user}:{password}@{hostname}:{port}/{database-name} mysql://{user}:{password}@{hostname}:{port}/{database-name} oracle://{user}:{password}@{hostname}:{port}/{database-name} sqlite:///PATH

Em nosso ambiente de desenvolvimento, podemos usar uma instância do PostgreSQL contida no Docker para facilitar o uso, enquanto em nosso ambiente de produção, apontaremos para um serviço de banco de dados provisionado.

Outra variável que queremos definir é um booleano, DEBUG . O sinalizador DEBUG no Django nunca deve ser ativado em uma implantação de produção. Destina-se a obter feedback adicional durante o desenvolvimento. Por exemplo, no modo de depuração, o Django exibirá páginas de erro detalhadas quando ocorrer uma exceção.

Valores diferentes para desenvolvimento e produção podem ser definidos da seguinte forma:

Nome variável Desenvolvimento Produção
DATABASE_URL postgres://postgres:mypw@localhost:5432/mydb postgres://foo1:foo2@foo3:5432/foo4
DEBUG True False

Usamos o módulo de gerenciamento de configurações pydantic para gerenciar esses diferentes conjuntos de valores de variáveis ​​de ambiente, dependendo do ambiente.

Etapas Preparatórias

Para colocar isso em prática, começamos a configurar nosso ambiente de desenvolvimento criando nosso único arquivo .env com este conteúdo:

 DATABASE_URL=postgres://postgres:mypw@localhost:5432/mydb DEBUG=True

Em seguida, adicionamos o arquivo .env ao arquivo .gitignore do projeto. O arquivo .gitignore evita salvar informações potencialmente confidenciais no controle de origem.

Enquanto essa abordagem funciona bem em nosso ambiente de desenvolvimento, nossa especificação de ambiente de produção usa um mecanismo diferente. Nossas práticas recomendadas determinam que as variáveis ​​de ambiente de produção usem segredos de ambiente. Por exemplo, no Heroku, esses segredos são chamados de Config Vars e são configurados por meio do Heroku Dashboard. Eles são disponibilizados para o aplicativo implantado como variáveis ​​de ambiente:

Uma captura de tela da interface da web do Config Vars. A barra lateral esquerda tem uma descrição: "Config vars altera a forma como seu aplicativo se comporta. Além de criar o seu próprio, alguns complementos vêm com seus próprios". A seção principal tem duas linhas, cada uma com dois campos de texto, um ícone de lápis e um ícone X. Os campos de texto têm os mesmos dados das colunas "Nome da variável" e "Produção" da tabela anterior. O canto superior direito tem um botão que diz "Hide Config Vars".

Depois disso, precisamos ajustar a configuração do aplicativo para ler esses valores de qualquer ambiente automaticamente.

Configurando o settings.py do Django

Vamos começar com um novo projeto Django para fornecer a estrutura essencial para o nosso exemplo. Nós montamos um novo projeto Django com o seguinte comando de terminal:

 $ django-admin startproject mysite

Agora temos uma estrutura de projeto básica para o projeto mysite . A estrutura de arquivos do projeto é a seguinte:

 mysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py

O arquivo settings.py contém código clichê que nos permite gerenciar a configuração do aplicativo. Ele tem muitas configurações padrão predefinidas que devemos ajustar ao ambiente relevante.

Para gerenciar essas configurações do aplicativo usando variáveis ​​de ambiente e pydantic, adicione este código ao topo do arquivo settings.py :

 import os from pathlib import Path from pydantic import ( BaseSettings, PostgresDsn, EmailStr, HttpUrl, ) import dj_database_url # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent class SettingsFromEnvironment(BaseSettings): """Defines environment variables with their types and optional defaults""" DATABASE_URL: PostgresDsn DEBUG: bool = False class Config: """Defines configuration for pydantic environment loading""" env_file = str(BASE_DIR / ".env") case_sensitive = True config = SettingsFromEnvironment() os.environ["DATABASE_URL"] = config.DATABASE_URL DATABASES = { "default": dj_database_url.config(conn_max_age=600, ssl_require=True) } DEBUG = config.DEBUG

Este código faz o seguinte:

  • Define uma classe SettingsFromEnvironment , herdando da classe BaseSettings do BaseSettings .
  • Define DATABASE_URL e DEBUG , definindo seu tipo e padrão opcional usando dicas de tipo Python.
  • Define uma classe Config dizendo ao pydantic para procurar as variáveis ​​em um arquivo .env se não estiver presente nas variáveis ​​de ambiente do sistema.
  • Instancia a classe Config na config do objeto; as variáveis ​​desejadas ficam disponíveis como config.DATABASE_URL e config.DEBUG .
  • Define as variáveis ​​regulares do Django DATABASES e DEBUG desses membros de config .

O mesmo código é executado em todos os ambientes e o pydantic cuida do seguinte:

  • Ele procura as variáveis ​​de ambiente DATABASE_URL e DEBUG .
    • Se definido como variáveis ​​de ambiente, como na produção, ele as usará.
    • Caso contrário, ele extrai esses valores do arquivo .env .
    • Se não encontrar um valor, ele fará o seguinte:
      • Para DATABASE_URL , ele gera um erro.
      • Para DEBUG , ele atribui um valor padrão de False .
  • Se encontrar uma variável de ambiente, ele verificará os tipos de campo e dará um erro se algum deles estiver errado:
    • Para DATABASE_URL , ele verifica se seu tipo de campo é uma URL no estilo PostgresDsn .
    • Para DEBUG , ele verifica se seu tipo de campo é um booleano pydantic válido e não estrito.

Observe a configuração explícita da variável de ambiente do sistema operacional do valor de configuração para DATABASE_URL . Pode parecer redundante definir os.environ["DATABASE_URL"] = config.DATABASE_URL porque DATABASE_URL já está definido como uma variável de ambiente externa. No entanto, isso permite que o pydantic analise, verifique e valide essa variável. Se a variável de ambiente DATABASE_URL estiver ausente ou formatada incorretamente, pydantic fornecerá uma mensagem de erro clara. Essas verificações de erros são inestimáveis ​​à medida que o aplicativo passa do desenvolvimento para os ambientes subsequentes.

Se uma variável não for definida, um padrão será atribuído ou um erro solicitará que ela seja definida. Qualquer prompt gerado também detalha o tipo de variável desejado. Um benefício colateral dessas verificações é que novos membros da equipe e engenheiros de DevOps descobrem mais facilmente quais variáveis ​​precisam ser definidas. Isso evita os problemas difíceis de encontrar que resultam quando o aplicativo é executado sem todas as variáveis ​​definidas.

É isso. O aplicativo agora tem uma implementação de gerenciamento de configurações sustentável usando uma única versão de settings.py . A beleza dessa abordagem é que ela nos permite especificar as variáveis ​​de ambiente corretas em um arquivo .env ou qualquer outro meio desejado disponível em nosso ambiente de hospedagem.

O caminho escalável

Eu tenho usado o gerenciamento de configurações do Django com digitação em tempo de execução pydantic em todos os meus projetos Django. Descobri que isso leva a bases de código menores e mais fáceis de manter. Ele também fornece uma abordagem bem estruturada, autodocumentada e escalável para adicionar novos ambientes.

O próximo artigo desta série é um tutorial passo a passo sobre como construir um aplicativo Django do zero, com gerenciamento de configurações pydantic e implantá-lo no Heroku.


O Toptal Engineering Blog agradece a Stephen Davidson por revisar os exemplos de código apresentados neste artigo.

Uma versão anterior deste artigo enfatizou uma versão específica do Python. Como o Python oferece suporte a dicas de tipo há vários anos, generalizamos o texto introdutório removendo esta referência de versão.