Otimize seu ambiente para desenvolvimento e produção: um tutorial do Pydantic, parte 2

Publicados: 2022-07-22

Os desenvolvedores podem ser seus próprios piores inimigos. Já vi inúmeros exemplos de engenheiros desenvolvendo em um sistema que não combina com seu ambiente de produção. Essa dissonância leva a um trabalho extra e não detecta erros do sistema até mais tarde no processo de desenvolvimento. O alinhamento dessas configurações facilitará as implantações contínuas. Com isso em mente, criaremos um aplicativo de exemplo em nosso ambiente de desenvolvimento Django, simplificado por meio do Docker, pydantic e conda.

Um ambiente de desenvolvimento típico usa:

  • Um repositório local;
  • Um banco de dados PostgreSQL baseado em Docker; e
  • Um ambiente conda (para gerenciar dependências do Python).

Pydantic e Django são adequados para projetos simples e complexos. As etapas a seguir mostram uma solução simples que destaca como espelhar nossos ambientes.

Configuração do repositório Git

Antes de começarmos a escrever código ou instalar sistemas de desenvolvimento, vamos criar um repositório Git local:

 mkdir hello-visitor cd hello-visitor git init

Começaremos com um arquivo .gitignore básico do Python na raiz do repositório. Ao longo deste tutorial, adicionaremos a este arquivo antes de adicionar arquivos que não queremos que o Git rastreie.

Configuração do Django PostgreSQL usando o Docker

O Django requer um banco de dados relacional e, por padrão, usa SQLite. Normalmente, evitamos o SQLite para armazenamento de dados de missão crítica, pois ele não lida bem com o acesso simultâneo de usuários. A maioria dos desenvolvedores opta por um banco de dados de produção mais típico, como o PostgreSQL. Independentemente disso, devemos usar o mesmo banco de dados para desenvolvimento e produção. Este mandato arquitetônico faz parte do aplicativo The Twelve-factor.

Felizmente, operar uma instância local do PostgreSQL com o Docker e o Docker Compose é muito fácil.

Para evitar poluir nosso diretório raiz, colocaremos os arquivos relacionados ao Docker em subdiretórios separados. Começaremos criando um arquivo Docker Compose para implantar o PostgreSQL:

 # docker-services/docker-compose.yml version: "3.9" services: db: image: "postgres:13.4" env_file: .env volumes: - hello-visitor-postgres:/var/lib/postgresql/data ports: - ${POSTGRES_PORT}:5432 volumes: hello-visitor-postgres:

Em seguida, criaremos um arquivo de ambiente docker-compose para configurar nosso contêiner PostgreSQL:

 # docker-services/.env POSTGRES_USER=postgres POSTGRES_PASSWORD=MyDBPassword123 # The 'maintenance' database POSTGRES_DB=postgres # The port exposed to localhost POSTGRES_PORT=5432

O servidor de banco de dados agora está definido e configurado. Vamos iniciar nosso container em segundo plano:

 sudo docker compose --project-directory docker-services/ up -d

É importante observar o uso de sudo no comando anterior. Será necessário, a menos que etapas específicas sejam seguidas em nosso ambiente de desenvolvimento.

Criação de banco de dados

Vamos nos conectar e configurar o PostgreSQL usando um conjunto de ferramentas padrão, pgAdmin4. Usaremos as mesmas credenciais de login configuradas anteriormente nas variáveis ​​de ambiente.

Agora vamos criar um novo banco de dados chamado hello_visitor :

Uma tela pgAdmin4 em um navegador mostrando a guia Geral em uma caixa de diálogo Criar banco de dados. O campo de texto do banco de dados contém o valor hello_visitor, o campo de proprietário exibe o usuário postgres e o campo de comentário está em branco.

Com nosso banco de dados no lugar, estamos prontos para instalar nosso ambiente de programação.

Gerenciamento de ambiente Python via Miniconda

Agora precisamos configurar um ambiente Python isolado e as dependências necessárias. Para simplicidade de configuração e manutenção, escolhemos o Miniconda.

Vamos criar e ativar nosso ambiente conda:

 conda create --name hello-visitor python=3.9 conda activate hello-visitor

Agora, vamos criar um arquivo, hello-visitor/requirements.txt , enumerando nossas dependências do Python:

 django # PostgreSQL database adapter: psycopg2 # Pushes .env key-value pairs into environment variables: python-dotenv pydantic # Utility library to read database connection information: dj-database-url # Static file caching: whitenoise # Python WSGI HTTP Server: gunicorn

Em seguida, pediremos ao Python para instalar essas dependências:

 cd hello-visitor pip install -r requirements.txt

Nossas dependências agora devem ser instaladas em preparação para o trabalho de desenvolvimento do aplicativo.

Django Scaffolding

Montaremos nosso projeto e aplicativo executando primeiro django-admin , depois executando um arquivo que ele gera, manage.py :

 # From the `hello-visitor` directory mkdir src cd src # Generate starter code for our Django project. django-admin startproject hello_visitor . # Generate starter code for our Django app. python manage.py startapp homepage

Em seguida, precisamos configurar o Django para carregar nosso projeto. O arquivo settings.py requer um ajuste no array INSTALLED_APPS para registrar nosso aplicativo de homepage recém-criado:

 # src/hello_visitor/settings.py # ... INSTALLED_APPS = [ "homepage.apps.HomepageConfig", "django.contrib.admin", # ... ] # ...

Configuração de configuração do aplicativo

Usando a abordagem de configurações pydantic e Django mostrada na primeira parte, precisamos criar um arquivo de variáveis ​​de ambiente para nosso sistema de desenvolvimento. Moveremos nossas configurações atuais para este arquivo da seguinte forma:

  1. Crie o arquivo src/.env para manter nossas configurações de ambiente de desenvolvimento.
  2. Copie as configurações de src/hello_visitor/settings.py e adicione-as a src/.env .
  3. Remova essas linhas copiadas do arquivo settings.py .
  4. Certifique-se de que a string de conexão do banco de dados use as mesmas credenciais que configuramos anteriormente.

Nosso arquivo de ambiente, src/.env , deve ficar assim:

 DATABASE_URL=postgres://postgres:MyDBPassword123@localhost:5432/hello_visitor DATABASE_SSL=False SECRET_KEY="django-insecure-sackl&7(1hc3+%#*4e=)^q3qiw!hnnui*-^($o8t@2^^qqs=%i" DEBUG=True DEBUG_TEMPLATES=True USE_SSL=False ALLOWED_HOSTS='[ "localhost", "127.0.0.1", "0.0.0.0" ]'

Vamos configurar o Django para ler as configurações de nossas variáveis ​​de ambiente usando pydantic, com este trecho de código:

 # src/hello_visitor/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""" # PostgreSQL DATABASE_URL: PostgresDsn DATABASE_SSL: bool = True # Django SECRET_KEY: str DEBUG: bool = False DEBUG_TEMPLATES: bool = False USE_SSL: bool = False ALLOWED_HOSTS: list 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=config.DATABASE_SSL) } SECRET_KEY = config.SECRET_KEY DEBUG = config.DEBUG DEBUG_TEMPLATES = config.DEBUG_TEMPLATES USE_SSL = config.USE_SSL ALLOWED_HOSTS = config.ALLOWED_HOSTS # ...

Se você encontrar algum problema após concluir as edições anteriores, compare nosso arquivo settings.py criado com a versão em nosso repositório de código-fonte.

Criação de modelo

Nosso aplicativo rastreia e exibe a contagem de visitantes da página inicial. Precisamos de um modelo para manter essa contagem e, em seguida, usar o mapeador relacional de objeto (ORM) do Django para inicializar uma única linha do banco de dados por meio de uma migração de dados.

Primeiro, criaremos nosso modelo VisitCounter :

 # hello-visitor/src/homepage/models.py """Defines the models""" from django.db import models class VisitCounter(models.Model): """ORM for VisitCounter""" count = models.IntegerField() @staticmethod def insert_visit_counter(): """Populates database with one visit counter. Call from a data migration.""" visit_counter = VisitCounter(count=0) visit_counter.save() def __str__(self): return f"VisitCounter - number of visits: {self.count}"

Em seguida, acionaremos uma migração para criar nossas tabelas de banco de dados:

 # in the `src` folder python manage.py makemigrations python manage.py migrate

Para verificar se a tabela homepage_visitcounter existe, podemos visualizar o banco de dados em pgAdmin4.

Em seguida, precisamos colocar um valor inicial em nossa tabela homepage_visitcounter . Vamos criar um arquivo de migração separado para fazer isso usando o scaffolding do Django:

 # from the 'src' directory python manage.py makemigrations --empty homepage

Ajustaremos o arquivo de migração criado para usar o método VisitCounter.insert_visit_counter que definimos no início desta seção:

 # src/homepage/migrations/0002_auto_-------_----.py # Note: The dashes are dependent on execution time. from django.db import migrations from ..models import VisitCounter def insert_default_items(apps, _schema_editor): """Populates database with one visit counter.""" # To learn about apps, see: # https://docs.djangoproject.com/en/3.2/topics/migrations/#data-migrations VisitCounter.insert_visit_counter() class Migration(migrations.Migration): """Runs a data migration.""" dependencies = [ ("homepage", "0001_initial"), ] operations = [ migrations.RunPython(insert_default_items), ]

Agora estamos prontos para executar esta migração modificada para o aplicativo da homepage :

 # from the 'src' directory python manage.py migrate homepage

Vamos verificar se a migração foi executada corretamente observando o conteúdo da nossa tabela:

Uma tela pgAdmin4 em um navegador mostrando uma consulta "SELECT * FROM public.homepage_visitcounter ORDER BY id ASC". A guia Saída de Dados mostra que há uma linha nessa tabela. O valor do campo de ID da chave substituta é 1 e o valor do campo de contagem é 0.

Vemos que nossa tabela homepage_visitcounter existe e foi preenchida com uma contagem de visitas inicial de 0. Com nosso banco de dados ao quadrado, vamos nos concentrar na criação de nossa interface do usuário.

Criar e configurar nossas visualizações

Precisamos implementar duas partes principais da nossa interface do usuário: uma visualização e um modelo.

Criamos a visualização da homepage para incrementar a contagem de visitantes, salvá-la no banco de dados e passar essa contagem para o modelo para exibição:

 # src/homepage/views.py from django.shortcuts import get_object_or_404, render from .models import VisitCounter def index(request): """View for the main page of the app.""" visit_counter = get_object_or_404(VisitCounter, pk=1) visit_counter.count += 1 visit_counter.save() context = {"visit_counter": visit_counter} return render(request, "homepage/index.html", context)

Nosso aplicativo Django precisa ouvir as solicitações direcionadas à homepage . Para definir essa configuração, adicionaremos este arquivo:

 # src/homepage/urls.py """Defines urls""" from django.urls import path from . import views # The namespace of the apps' URLconf app_name = "homepage" # pylint: disable=invalid-name urlpatterns = [ path("", views.index, name="index"), ]

Para que nosso aplicativo de homepage seja atendido, devemos registrá-lo em um arquivo urls.py diferente:

 # src/hello_visitor/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path("", include("homepage.urls")), path("admin/", admin.site.urls), ]

O template HTML base do nosso projeto ficará em um novo arquivo, src/templates/layouts/base.html :

 <!DOCTYPE html> {% load static %} <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous"> <title>Hello, visitor!</title> <link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/> </head> <body> {% block main %}{% endblock %} <!-- Option 1: Bootstrap Bundle with Popper --> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script> </body> </html>

Vamos estender o modelo base para nosso aplicativo de homepage em um novo arquivo, src/templates/homepage/index.html :

 {% extends "layouts/base.html" %} {% block main %} <main> <div class="container py-4"> <div class="p-5 mb-4 bg-dark text-white text-center rounded-3"> <div class="container-fluid py-5"> <h1 class="display-5 fw-bold">Hello, visitor {{ visit_counter.count }}!</h1> </div> </div> </div> </main> {% endblock %}

O último passo na criação da nossa UI é dizer ao Django onde encontrar esses templates. Vamos adicionar um item de dicionário TEMPLATES['DIRS'] ao nosso arquivo settings.py :

 # src/hello_visitor/settings.py TEMPLATES = [ { ... 'DIRS': [BASE_DIR / 'templates'], ... }, ]

Nossa interface de usuário já está implementada e estamos quase prontos para testar a funcionalidade do nosso aplicativo. Antes de fazermos nossos testes, precisamos colocar em prática a parte final do nosso ambiente: cache de conteúdo estático.

Nossa configuração de conteúdo estático

Para evitar atalhos de arquitetura em nosso sistema de desenvolvimento, configuraremos o cache de conteúdo estático para espelhar nosso ambiente de produção.

Manteremos todos os arquivos estáticos do nosso projeto em um único diretório, src/static , e instruiremos o Django a coletar esses arquivos antes da implantação.

Usaremos o logotipo da Toptal para o favicon do nosso aplicativo e o armazenaremos como src/static/favicon.ico :

 # from `src` folder mkdir static cd static wget https://frontier-assets.toptal.com/83b2f6e0d02cdb3d951a75bd07ee4058.png mv 83b2f6e0d02cdb3d951a75bd07ee4058.png favicon.ico

Em seguida, vamos configurar o Django para coletar os arquivos estáticos:

 # src/hello_visitor/settings.py # Static files (CSS, JavaScript, images) # a la https://docs.djangoproject.com/en/3.2/howto/static-files/ # # Source location where we'll store our static files STATICFILES_DIRS = [BASE_DIR / "static"] # Build output location where Django collects all static files STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_ROOT.mkdir(exist_ok=True) # URL to use when referring to static files located in STATIC_ROOT. STATIC_URL = "/static/" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

Queremos apenas armazenar nossos arquivos estáticos originais no repositório de código-fonte; não queremos armazenar as versões otimizadas para produção. Vamos adicionar o último ao nosso .gitignore com esta linha simples:

 staticfiles

Com nosso repositório de código-fonte armazenando corretamente os arquivos necessários, agora precisamos configurar nosso sistema de cache para trabalhar com esses arquivos estáticos.

Cache de arquivo estático

Na produção - e, portanto, também em nosso ambiente de desenvolvimento - usaremos o WhiteNoise para servir os arquivos estáticos do nosso aplicativo Django com mais eficiência.

Registramos o WhiteNoise como middleware adicionando o seguinte trecho ao nosso arquivo src/hello_visitor/settings.py . A ordem de registro é estritamente definida, e WhiteNoiseMiddleware deve aparecer imediatamente após SecurityMiddleware :

 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

O cache de arquivo estático agora deve ser configurado em nosso ambiente de desenvolvimento, permitindo que executemos nosso aplicativo.

Executando nosso servidor de desenvolvimento

Temos um aplicativo totalmente codificado e agora podemos iniciar o servidor web de desenvolvimento incorporado do nosso Django com este comando:

 # in the `src` folder python manage.py runserver

Quando navegamos para http://localhost:8000 , a contagem aumentará cada vez que atualizarmos a página:

Uma janela do navegador mostrando a tela principal do nosso aplicativo pydantic Django, que diz: "Olá, visitante!" em uma linha e "1" na próxima.

Agora temos um aplicativo em funcionamento que aumentará sua contagem de visitas à medida que atualizamos a página.

Pronto para implantar

Este tutorial cobriu todas as etapas necessárias para criar um aplicativo funcional em um belo ambiente de desenvolvimento Django que corresponda à produção. Na Parte 3, abordaremos a implantação de nosso aplicativo em seu ambiente de produção. Também vale a pena explorar nossos exercícios adicionais destacando os benefícios do Django e do pydantic: Eles estão incluídos no repositório de código completo para este tutorial pydantic.


O Toptal Engineering Blog agradece a Stephen Davidson pela revisão e teste beta dos exemplos de código apresentados neste artigo.