Optimisez votre environnement pour le développement et la production : un didacticiel Pydantic, partie 2
Publié: 2022-07-22Les 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
:
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 :
- Créez le fichier
src/.env
pour contenir nos paramètres d'environnement de développement. - Copiez les paramètres de
src/hello_visitor/settings.py
et ajoutez-les àsrc/.env
. - Supprimez ces lignes copiées du fichier
settings.py
. - 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 :
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 :
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.