Optimisez votre environnement pour le développement et la production : un didacticiel Pydantic, partie 2

Publié: 2022-07-22

Les développeurs peuvent être leurs propres pires ennemis. J'ai vu d'innombrables exemples d'ingénieurs développant sur un système qui ne correspond pas à leur environnement de production. Cette dissonance entraîne un travail supplémentaire et ne détecte les erreurs système que plus tard dans le processus de développement. L'alignement de ces configurations facilitera finalement les déploiements continus. Dans cet esprit, nous allons créer un exemple d'application sur notre environnement de développement Django, simplifié via Docker, pydantic et conda.

Un environnement de développement typique utilise :

  • Un référentiel local ;
  • Une base de données PostgreSQL basée sur Docker ; et
  • Un environnement conda (pour gérer les dépendances Python).

Pydantic et Django conviennent aux projets aussi bien simples que complexes. Les étapes suivantes présentent une solution simple qui met en évidence la façon de refléter nos environnements.

Configuration du référentiel Git

Avant de commencer à écrire du code ou à installer des systèmes de développement, créons un dépôt Git local :

 mkdir hello-visitor cd hello-visitor git init

Nous allons commencer avec un fichier Python .gitignore de base à la racine du référentiel. Tout au long de ce didacticiel, nous ajouterons à ce fichier avant d'ajouter des fichiers que nous ne voulons pas que Git suive.

Configuration de Django PostgreSQL à l'aide de Docker

Django nécessite une base de données relationnelle et, par défaut, utilise SQLite. Nous évitons généralement SQLite pour le stockage de données critiques car il ne gère pas bien l'accès simultané des utilisateurs. La plupart des développeurs optent pour une base de données de production plus typique, comme PostgreSQL. Quoi qu'il en soit, nous devrions utiliser la même base de données pour le développement et la production. Ce mandat architectural fait partie de The Twelve-factor App.

Heureusement, l'exploitation d'une instance PostgreSQL locale avec Docker et Docker Compose est un jeu d'enfant.

Pour éviter de polluer notre répertoire racine, nous placerons les fichiers liés à Docker dans des sous-répertoires séparés. Nous allons commencer par créer un fichier Docker Compose pour déployer 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:

Ensuite, nous allons créer un fichier d'environnement docker-compose pour configurer notre conteneur PostgreSQL :

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

Le serveur de base de données est maintenant défini et configuré. Commençons notre conteneur en arrière-plan :

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

Il est important de noter l'utilisation de sudo dans la commande précédente. Il sera nécessaire à moins que des étapes spécifiques ne soient suivies dans notre environnement de développement.

Création de base de données

Connectons-nous et configurons PostgreSQL à l'aide d'une suite d'outils standard, pgAdmin4. Nous utiliserons les mêmes identifiants de connexion que ceux précédemment configurés dans les variables d'environnement.

Créons maintenant une nouvelle base de données nommée hello_visitor :

Un écran pgAdmin4 dans un navigateur affichant l'onglet Général dans une boîte de dialogue Créer une base de données. Le champ de texte de la base de données contient la valeur hello_visitor, le champ du propriétaire affiche l'utilisateur postgres et le champ de commentaire est vide.

Avec notre base de données en place, nous sommes prêts à installer notre environnement de programmation.

Gestion de l'environnement Python via Miniconda

Nous devons maintenant configurer un environnement Python isolé et les dépendances requises. Pour la simplicité d'installation et de maintenance, nous avons choisi Miniconda.

Créons et activons notre environnement conda :

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

Maintenant, nous allons créer un fichier, hello-visitor/requirements.txt , énumérant nos dépendances 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

Ensuite, nous demanderons à Python d'installer ces dépendances :

 cd hello-visitor pip install -r requirements.txt

Nos dépendances doivent maintenant être installées en préparation du travail de développement de l'application.

Django Échafaudage

Nous allons échafauder notre projet et notre application en exécutant d'abord django-admin , puis en exécutant un fichier qu'il génère, 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

Ensuite, nous devons configurer Django pour charger notre projet. Le fichier settings.py nécessite un ajustement du tableau INSTALLED_APPS pour enregistrer notre application de page d' homepage nouvellement créée :

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

Configuration des paramètres d'application

En utilisant l'approche des paramètres pydantic et Django présentée dans le premier épisode, nous devons créer un fichier de variables d'environnement pour notre système de développement. Nous allons déplacer nos paramètres actuels dans ce fichier comme suit :

  1. Créez le fichier src/.env pour contenir nos paramètres d'environnement de développement.
  2. Copiez les paramètres de src/hello_visitor/settings.py et ajoutez-les à src/.env .
  3. Supprimez ces lignes copiées du fichier settings.py .
  4. Assurez-vous que la chaîne de connexion à la base de données utilise les mêmes informations d'identification que nous avons précédemment configurées.

Notre fichier d'environnement, src/.env , devrait ressembler à ceci :

 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" ]'

Nous allons configurer Django pour lire les paramètres de nos variables d'environnement à l'aide de pydantic, avec cet extrait de code :

 # 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 vous rencontrez des problèmes après avoir terminé les modifications précédentes, comparez notre fichier settings.py conçu avec la version de notre référentiel de code source.

Création de modèle

Notre application suit et affiche le nombre de visiteurs de la page d'accueil. Nous avons besoin d'un modèle pour conserver ce nombre, puis utiliser le mappeur relationnel objet (ORM) de Django pour initialiser une seule ligne de base de données via une migration de données.

Tout d'abord, nous allons créer notre modèle 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}"

Ensuite, nous allons déclencher une migration pour créer nos tables de base de données :

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

Pour vérifier que la table homepage_visitcounter existe, nous pouvons afficher la base de données dans pgAdmin4.

Ensuite, nous devons mettre une valeur initiale dans notre table homepage_visitcounter . Créons un fichier de migration séparé pour accomplir cela en utilisant l'échafaudage Django :

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

Nous allons ajuster le fichier de migration créé pour utiliser la méthode VisitCounter.insert_visit_counter que nous avons définie au début de cette section :

 # 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), ]

Nous sommes maintenant prêts à exécuter cette migration modifiée pour l'application de la homepage d'accueil :

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

Vérifions que la migration s'est bien déroulée en regardant le contenu de notre table :

Un écran pgAdmin4 dans un navigateur affichant une requête "SELECT * FROM public.homepage_visitcounter ORDER BY id ASC". L'onglet Sortie de données indique qu'il existe une ligne dans cette table. La valeur du champ d'ID de clé de substitution est 1 et la valeur du champ de comptage est 0.

Nous voyons que notre table homepage_visitcounter existe et a été remplie avec un nombre de visites initial de 0. Avec notre base de données au carré, nous nous concentrerons sur la création de notre interface utilisateur.

Créer et configurer nos vues

Nous devons implémenter deux parties principales de notre interface utilisateur : une vue et un modèle.

Nous créons la vue de la page d' homepage pour incrémenter le nombre de visiteurs, l'enregistrons dans la base de données et transmettons ce nombre au modèle pour l'affichage :

 # 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)

Notre application Django a besoin d'écouter les requêtes destinées à la page d' homepage . Pour configurer ce paramètre, nous allons ajouter ce fichier :

 # 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"), ]

Pour que notre application de homepage d'accueil soit servie, nous devons l'enregistrer dans un fichier urls.py différent :

 # 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), ]

Le modèle HTML de base de notre projet vivra dans un nouveau fichier, 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>

Nous allons étendre le modèle de base de notre application de homepage d'accueil dans un nouveau fichier, 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 %}

La dernière étape de la création de notre interface utilisateur consiste à indiquer à Django où trouver ces modèles. Ajoutons un élément de dictionnaire TEMPLATES['DIRS'] à notre fichier settings.py :

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

Notre interface utilisateur est maintenant implémentée et nous sommes presque prêts à tester les fonctionnalités de notre application. Avant de faire nos tests, nous devons mettre en place la dernière pièce de notre environnement : la mise en cache de contenu statique.

Notre configuration de contenu statique

Pour éviter de prendre des raccourcis architecturaux sur notre système de développement, nous allons configurer la mise en cache du contenu statique pour refléter notre environnement de production.

Nous conserverons tous les fichiers statiques de notre projet dans un seul répertoire, src/static , et demanderons à Django de collecter ces fichiers avant le déploiement.

Nous utiliserons le logo de Toptal pour le favicon de notre application et le stockerons sous src/static/favicon.ico :

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

Ensuite, nous allons configurer Django pour collecter les fichiers statiques :

 # 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"

Nous voulons uniquement stocker nos fichiers statiques d'origine dans le référentiel de code source ; nous ne voulons pas stocker les versions optimisées pour la production. Ajoutons ce dernier à notre .gitignore avec cette simple ligne :

 staticfiles

Avec notre référentiel de code source stockant correctement les fichiers requis, nous devons maintenant configurer notre système de mise en cache pour qu'il fonctionne avec ces fichiers statiques.

Mise en cache de fichiers statiques

En production, et donc également dans notre environnement de développement, nous utiliserons WhiteNoise pour servir plus efficacement les fichiers statiques de notre application Django.

Nous enregistrons WhiteNoise en tant que middleware en ajoutant l'extrait de code suivant à notre fichier src/hello_visitor/settings.py . L'ordre d'enregistrement est strictement défini, et WhiteNoiseMiddleware doit apparaître immédiatement après SecurityMiddleware :

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

La mise en cache des fichiers statiques doit maintenant être configurée dans notre environnement de développement, nous permettant d'exécuter notre application.

Exécution de notre serveur de développement

Nous avons une application entièrement codée et pouvons maintenant lancer le serveur Web de développement intégré de Django avec cette commande :

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

Lorsque nous naviguons vers http://localhost:8000 , le nombre augmentera à chaque fois que nous actualiserons la page :

Une fenêtre de navigateur montrant l'écran principal de notre application pydantic Django, qui dit "Hello, visitor!" sur une ligne et "1" sur la suivante.

Nous avons maintenant une application fonctionnelle qui augmentera son nombre de visites au fur et à mesure que nous actualiserons la page.

Prêt à déployer

Ce tutoriel a couvert toutes les étapes nécessaires pour créer une application fonctionnelle dans un bel environnement de développement Django qui correspond à la production. Dans la partie 3, nous aborderons le déploiement de notre application dans son environnement de production. Cela vaut également la peine d'explorer nos exercices supplémentaires mettant en évidence les avantages de Django et de pydantic : ils sont inclus dans le référentiel code-complete de ce didacticiel pydantic.


Le blog Toptal Engineering exprime sa gratitude à Stephen Davidson pour avoir révisé et testé en version bêta les exemples de code présentés dans cet article.