Optimice su entorno para el desarrollo y la producción: un tutorial de Pydantic, parte 2
Publicado: 2022-07-22Los desarrolladores pueden ser sus propios peores enemigos. He visto innumerables ejemplos de ingenieros que desarrollan en un sistema que no coincide con su entorno de producción. Esta disonancia genera trabajo adicional y no detecta errores del sistema hasta más adelante en el proceso de desarrollo. La alineación de estas configuraciones facilitará en última instancia las implementaciones continuas. Con esto en mente, crearemos una aplicación de muestra en nuestro entorno de desarrollo Django, simplificada a través de Docker, pydantic y conda.
Un entorno de desarrollo típico utiliza:
- Un repositorio local;
- Una base de datos PostgreSQL basada en Docker; y
- Un entorno conda (para administrar las dependencias de Python).
Pydantic y Django son adecuados para proyectos tanto simples como complejos. Los siguientes pasos muestran una solución simple que destaca cómo reflejar nuestros entornos.
Configuración del repositorio Git
Antes de comenzar a escribir código o instalar sistemas de desarrollo, creemos un repositorio Git local:
mkdir hello-visitor cd hello-visitor git init
Comenzaremos con un archivo .gitignore
de Python básico en la raíz del repositorio. A lo largo de este tutorial, agregaremos a este archivo antes de agregar archivos que no queremos que Git rastree.
Configuración de Django PostgreSQL usando Docker
Django requiere una base de datos relacional y, de forma predeterminada, utiliza SQLite. Por lo general, evitamos SQLite para el almacenamiento de datos de misión crítica, ya que no maneja bien el acceso de usuarios simultáneos. La mayoría de los desarrolladores optan por una base de datos de producción más típica, como PostgreSQL. Independientemente, debemos usar la misma base de datos para el desarrollo y la producción. Este mandato arquitectónico es parte de The Twelve-factor App.
Afortunadamente, operar una instancia local de PostgreSQL con Docker y Docker Compose es muy sencillo.
Para evitar contaminar nuestro directorio raíz, colocaremos los archivos relacionados con Docker en subdirectorios separados. Comenzaremos creando un archivo Docker Compose para implementar 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:
A continuación, crearemos un archivo de entorno docker-compose
para configurar nuestro contenedor de PostgreSQL:
# docker-services/.env POSTGRES_USER=postgres POSTGRES_PASSWORD=MyDBPassword123 # The 'maintenance' database POSTGRES_DB=postgres # The port exposed to localhost POSTGRES_PORT=5432
El servidor de la base de datos ya está definido y configurado. Comencemos nuestro contenedor en segundo plano:
sudo docker compose --project-directory docker-services/ up -d
Es importante notar el uso de sudo en el comando anterior. Será necesario a menos que se sigan pasos específicos en nuestro entorno de desarrollo.
Creación de base de datos
Conectémonos y configuremos PostgreSQL usando un conjunto de herramientas estándar, pgAdmin4. Usaremos las mismas credenciales de inicio de sesión que las configuradas previamente en las variables de entorno.
Ahora vamos a crear una nueva base de datos llamada hello_visitor
:
Con nuestra base de datos en su lugar, estamos listos para instalar nuestro entorno de programación.
Gestión del entorno de Python a través de Miniconda
Ahora necesitamos configurar un entorno de Python aislado y las dependencias requeridas. Por simplicidad de configuración y mantenimiento, elegimos Miniconda.
Vamos a crear y activar nuestro entorno conda:
conda create --name hello-visitor python=3.9 conda activate hello-visitor
Ahora, crearemos un archivo, hello-visitor/requirements.txt
, enumerando nuestras dependencias de 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
A continuación, le pediremos a Python que instale estas dependencias:
cd hello-visitor pip install -r requirements.txt
Nuestras dependencias ahora deberían estar instaladas en preparación para el trabajo de desarrollo de la aplicación.
Andamios Django
Crearemos un andamiaje en nuestro proyecto y aplicación ejecutando primero django-admin
y luego ejecutando un archivo que genera, 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
A continuación, debemos configurar Django para cargar nuestro proyecto. El archivo settings.py
requiere un ajuste en la matriz INSTALLED_APPS
para registrar nuestra aplicación de homepage
de inicio recién creada:
# src/hello_visitor/settings.py # ... INSTALLED_APPS = [ "homepage.apps.HomepageConfig", "django.contrib.admin", # ... ] # ...
Configuración de la configuración de la aplicación
Usando el enfoque de configuración de pydantic y Django que se muestra en la primera entrega, necesitamos crear un archivo de variables de entorno para nuestro sistema de desarrollo. Moveremos nuestra configuración actual a este archivo de la siguiente manera:
- Cree el archivo
src/.env
para almacenar la configuración de nuestro entorno de desarrollo. - Copie la configuración de
src/hello_visitor/settings.py
y agréguela asrc/.env
. - Elimine esas líneas copiadas del archivo
settings.py
. - Asegúrese de que la cadena de conexión de la base de datos utilice las mismas credenciales que configuramos anteriormente.
Nuestro archivo de entorno, src/.env
, debería verse así:
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" ]'
Configuraremos Django para leer la configuración de nuestras variables de entorno usando pydantic, con este fragmento 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 # ...
Si encuentra algún problema después de completar las ediciones anteriores, compare nuestro archivo settings.py
creado con la versión en nuestro repositorio de código fuente.
Creación de modelos
Nuestra aplicación rastrea y muestra el recuento de visitantes de la página de inicio. Necesitamos un modelo para mantener ese conteo y luego usar el mapeador relacional de objetos (ORM) de Django para inicializar una sola fila de la base de datos a través de una migración de datos.
Primero, crearemos nuestro 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}"
A continuación, activaremos una migración para crear nuestras tablas de base de datos:
# in the `src` folder python manage.py makemigrations python manage.py migrate
Para verificar que existe la tabla homepage_visitcounter
, podemos ver la base de datos en pgAdmin4.
A continuación, debemos poner un valor inicial en nuestra tabla homepage_visitcounter
. Vamos a crear un archivo de migración separado para lograr esto usando el andamiaje de Django:
# from the 'src' directory python manage.py makemigrations --empty homepage
Ajustaremos el archivo de migración creado para usar el método VisitCounter.insert_visit_counter
que definimos al principio de esta sección:
# 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), ]
Ahora estamos listos para ejecutar esta migración modificada para la aplicación de la homepage
de inicio:
# from the 'src' directory python manage.py migrate homepage
Verifiquemos que la migración se ejecutó correctamente mirando el contenido de nuestra tabla:
Vemos que nuestra tabla homepage_visitcounter
existe y se ha rellenado con un recuento de visitas inicial de 0. Con nuestra base de datos al cuadrado, nos centraremos en crear nuestra interfaz de usuario.
Crear y configurar nuestras vistas
Necesitamos implementar dos partes principales de nuestra interfaz de usuario: una vista y una plantilla.
Creamos la vista de la homepage
de inicio para incrementar el recuento de visitantes, guardarlo en la base de datos y pasar ese recuento a la plantilla para su visualización:
# 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)
Nuestra aplicación Django necesita escuchar las solicitudes dirigidas a la homepage
de inicio. Para configurar esta configuración, agregaremos este archivo:
# 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 nuestra aplicación de homepage
de inicio sea servida, debemos registrarla en un archivo 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), ]
La plantilla HTML base de nuestro proyecto vivirá en un nuevo archivo, 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>
Ampliaremos la plantilla base para nuestra aplicación de homepage
de inicio en un nuevo archivo, 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 %}
El último paso para crear nuestra interfaz de usuario es decirle a Django dónde encontrar estas plantillas. Agreguemos un elemento de diccionario TEMPLATES['DIRS']
a nuestro archivo settings.py
:
# src/hello_visitor/settings.py TEMPLATES = [ { ... 'DIRS': [BASE_DIR / 'templates'], ... }, ]
Nuestra interfaz de usuario ya está implementada y estamos casi listos para probar la funcionalidad de nuestra aplicación. Antes de realizar nuestras pruebas, debemos colocar la pieza final de nuestro entorno: el almacenamiento en caché de contenido estático.
Nuestra configuración de contenido estático
Para evitar tomar atajos arquitectónicos en nuestro sistema de desarrollo, configuraremos el almacenamiento en caché de contenido estático para reflejar nuestro entorno de producción.
Mantendremos todos los archivos estáticos de nuestro proyecto en un solo directorio, src/static
, y le indicaremos a Django que recopile esos archivos antes de la implementación.
Usaremos el logotipo de Toptal para el favicon
de nuestra aplicación y lo almacenaremos 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
A continuación, configuraremos Django para recopilar los archivos 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"
Solo queremos almacenar nuestros archivos estáticos originales en el repositorio del código fuente; no queremos almacenar las versiones optimizadas para producción. Agreguemos este último a nuestro .gitignore
con esta simple línea:
staticfiles
Con nuestro repositorio de código fuente almacenando correctamente los archivos requeridos, ahora necesitamos configurar nuestro sistema de almacenamiento en caché para que funcione con estos archivos estáticos.
Almacenamiento en caché de archivos estáticos
En producción, y por lo tanto, también en nuestro entorno de desarrollo, usaremos WhiteNoise para servir los archivos estáticos de nuestra aplicación Django de manera más eficiente.
Registramos WhiteNoise como middleware agregando el siguiente fragmento a nuestro archivo src/hello_visitor/settings.py
. El orden de registro está estrictamente definido y WhiteNoiseMiddleware
debe aparecer inmediatamente después SecurityMiddleware
:
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
El almacenamiento en caché de archivos estáticos ahora debe configurarse en nuestro entorno de desarrollo, lo que nos permite ejecutar nuestra aplicación.
Ejecutando nuestro servidor de desarrollo
Tenemos una aplicación completamente codificada y ahora podemos iniciar nuestro servidor web de desarrollo integrado de Django con este comando:
# in the `src` folder python manage.py runserver
Cuando navegamos a http://localhost:8000
, el conteo aumentará cada vez que actualicemos la página:
Ahora tenemos una aplicación en funcionamiento que incrementará su número de visitas a medida que actualicemos la página.
Listo para implementar
Este tutorial ha cubierto todos los pasos necesarios para crear una aplicación que funcione en un hermoso entorno de desarrollo de Django que coincida con la producción. En la Parte 3, cubriremos la implementación de nuestra aplicación en su entorno de producción. También vale la pena explorar nuestros ejercicios adicionales que destacan los beneficios de Django y pydantic: están incluidos en el repositorio de código completo para este tutorial de pydantic.
El blog de ingeniería de Toptal agradece a Stephen Davidson por revisar y realizar pruebas beta de los ejemplos de código presentados en este artículo.