Otimize seu ambiente para desenvolvimento e produção: um tutorial do Pydantic, parte 2
Publicados: 2022-07-22Os 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
:
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:
- Crie o arquivo
src/.env
para manter nossas configurações de ambiente de desenvolvimento. - Copie as configurações de
src/hello_visitor/settings.py
e adicione-as asrc/.env
. - Remova essas linhas copiadas do arquivo
settings.py
. - 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:
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:
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.