Optimieren Sie Ihre Umgebung für Entwicklung und Produktion: Ein Pydantic-Tutorial, Teil 2

Veröffentlicht: 2022-07-22

Entwickler können ihre eigenen schlimmsten Feinde sein. Ich habe unzählige Beispiele von Ingenieuren gesehen, die auf einem System entwickelt haben, das nicht zu ihrer Produktionsumgebung passt. Diese Dissonanz führt zu Mehrarbeit und dazu, dass Systemfehler erst später im Entwicklungsprozess erkannt werden. Die Angleichung dieser Setups wird letztendlich kontinuierliche Bereitstellungen erleichtern. Vor diesem Hintergrund erstellen wir eine Beispielanwendung auf unserer Django-Entwicklungsumgebung, vereinfacht durch Docker, Pydantic und Conda.

Eine typische Entwicklungsumgebung verwendet:

  • Ein lokales Repository;
  • Eine Docker-basierte PostgreSQL-Datenbank; und
  • Eine Conda-Umgebung (um Python-Abhängigkeiten zu verwalten).

Pydantic und Django eignen sich sowohl für einfache als auch für komplexe Projekte. Die folgenden Schritte zeigen eine einfache Lösung, die zeigt, wie unsere Umgebungen gespiegelt werden.

Git-Repository-Konfiguration

Bevor wir mit dem Schreiben von Code oder der Installation von Entwicklungssystemen beginnen, erstellen wir ein lokales Git-Repository:

 mkdir hello-visitor cd hello-visitor git init

Wir beginnen mit einer einfachen Python .gitignore -Datei im Repository-Root. In diesem Tutorial fügen wir diese Datei hinzu, bevor wir Dateien hinzufügen, die Git nicht nachverfolgen soll.

Django PostgreSQL-Konfiguration mit Docker

Django erfordert eine relationale Datenbank und verwendet standardmäßig SQLite. Normalerweise vermeiden wir SQLite für die Speicherung geschäftskritischer Daten, da es den gleichzeitigen Benutzerzugriff nicht gut handhabt. Die meisten Entwickler entscheiden sich für eine typischere Produktionsdatenbank wie PostgreSQL. Unabhängig davon sollten wir dieselbe Datenbank für Entwicklung und Produktion verwenden. Dieser architektonische Auftrag ist Teil der Zwölf-Faktoren-App.

Glücklicherweise ist der Betrieb einer lokalen PostgreSQL-Instanz mit Docker und Docker Compose ein Kinderspiel.

Um eine Verschmutzung unseres Stammverzeichnisses zu vermeiden, legen wir die Docker-bezogenen Dateien in separaten Unterverzeichnissen ab. Wir beginnen mit der Erstellung einer Docker Compose-Datei zur Bereitstellung von 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:

Als Nächstes erstellen wir eine docker-compose Umgebungsdatei, um unseren PostgreSQL-Container zu konfigurieren:

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

Der Datenbankserver ist nun definiert und konfiguriert. Starten wir unseren Container im Hintergrund:

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

Es ist wichtig, die Verwendung von sudo im vorherigen Befehl zu beachten. Es ist erforderlich, es sei denn, in unserer Entwicklungsumgebung werden bestimmte Schritte befolgt.

Datenbankerstellung

Lassen Sie uns eine Verbindung zu PostgreSQL herstellen und es mit einer Standard-Tool-Suite, pgAdmin4, konfigurieren. Wir verwenden dieselben Anmeldeinformationen, die zuvor in den Umgebungsvariablen konfiguriert wurden.

Lassen Sie uns nun eine neue Datenbank namens hello_visitor :

Ein pgAdmin4-Bildschirm in einem Browser, der die Registerkarte „Allgemein“ in einem Dialogfeld „Datenbank erstellen“ anzeigt. Das Datenbanktextfeld enthält den Wert hello_visitor, das Eigentümerfeld zeigt den Postgres-Benutzer an und das Kommentarfeld ist leer.

Nachdem unsere Datenbank eingerichtet ist, können wir unsere Programmierumgebung installieren.

Python-Umgebungsverwaltung über Miniconda

Wir müssen jetzt eine isolierte Python-Umgebung und die erforderlichen Abhängigkeiten einrichten. Für eine einfache Einrichtung und Wartung haben wir uns für Miniconda entschieden.

Lassen Sie uns unsere Conda-Umgebung erstellen und aktivieren:

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

Jetzt erstellen wir eine Datei, hello-visitor/requirements.txt , die unsere Python-Abhängigkeiten auflistet:

 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

Als Nächstes bitten wir Python, diese Abhängigkeiten zu installieren:

 cd hello-visitor pip install -r requirements.txt

Unsere Abhängigkeiten sollten nun als Vorbereitung für die Anwendungsentwicklungsarbeit installiert werden.

Django-Gerüst

Wir werden unser Projekt und unsere App rüsten, indem wir zuerst django-admin und dann eine Datei ausführen, die es generiert, 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

Als nächstes müssen wir Django konfigurieren, um unser Projekt zu laden. Die Datei settings.py erfordert eine Anpassung des Arrays INSTALLED_APPS , um unsere neu erstellte homepage -App zu registrieren:

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

Konfiguration der Anwendungseinstellungen

Unter Verwendung des im ersten Teil gezeigten pydantic- und Django-Einstellungsansatzes müssen wir eine Umgebungsvariablendatei für unser Entwicklungssystem erstellen. Wir verschieben unsere aktuellen Einstellungen wie folgt in diese Datei:

  1. Erstellen Sie die Datei src/.env , um unsere Entwicklungsumgebungseinstellungen zu speichern.
  2. Kopieren Sie die Einstellungen aus src/hello_visitor/settings.py und fügen Sie sie zu src/.env .
  3. Entfernen Sie diese kopierten Zeilen aus der Datei settings.py .
  4. Stellen Sie sicher, dass die Datenbankverbindungszeichenfolge dieselben Anmeldeinformationen verwendet, die wir zuvor konfiguriert haben.

Unsere Umgebungsdatei src/.env sollte wie folgt aussehen:

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

Wir werden Django so konfigurieren, dass es Einstellungen aus unseren Umgebungsvariablen mit pydantic liest, mit diesem Code-Snippet:

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

Wenn Sie nach Abschluss der vorherigen Änderungen auf Probleme stoßen, vergleichen Sie unsere gestaltete settings.py -Datei mit der Version in unserem Quellcode-Repository.

Modellerstellung

Unsere Anwendung verfolgt und zeigt die Besucherzahl der Homepage an. Wir brauchen ein Modell, das diese Anzahl hält, und verwenden dann den objektrelationalen Mapper (ORM) von Django, um eine einzelne Datenbankzeile über eine Datenmigration zu initialisieren.

Zuerst erstellen wir unser VisitCounter Modell:

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

Als Nächstes lösen wir eine Migration aus, um unsere Datenbanktabellen zu erstellen:

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

Um zu überprüfen, ob die Tabelle homepage_visitcounter existiert, können wir die Datenbank in pgAdmin4 anzeigen.

Als nächstes müssen wir einen Anfangswert in unsere homepage_visitcounter -Tabelle schreiben. Lassen Sie uns eine separate Migrationsdatei erstellen, um dies mit Django-Gerüsten zu erreichen:

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

Wir passen die erstellte Migrationsdatei an, um die Methode VisitCounter.insert_visit_counter zu verwenden, die wir zu Beginn dieses Abschnitts definiert haben:

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

Jetzt können wir diese modifizierte Migration für die homepage -App ausführen:

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

Lassen Sie uns überprüfen, ob die Migration korrekt ausgeführt wurde, indem wir uns den Inhalt unserer Tabelle ansehen:

Ein pgAdmin4-Bildschirm in einem Browser, der eine Abfrage „SELECT * FROM public.homepage_visitcounter ORDER BY id ASC“ zeigt. Die Registerkarte Datenausgabe zeigt, dass es eine Zeile in dieser Tabelle gibt. Der Feldwert für die Ersatzschlüssel-ID ist 1 und der Feldwert für die Anzahl ist 0.

Wir sehen, dass unsere homepage_visitcounter -Tabelle existiert und mit einer anfänglichen Besuchszahl von 0 gefüllt wurde. Nachdem unsere Datenbank quadratisch ist, konzentrieren wir uns auf die Erstellung unserer Benutzeroberfläche.

Erstellen und konfigurieren Sie unsere Ansichten

Wir müssen zwei Hauptteile unserer Benutzeroberfläche implementieren: eine Ansicht und eine Vorlage.

Wir erstellen die homepage , um die Besucherzahl zu erhöhen, speichern sie in der Datenbank und übergeben diese Zahl zur Anzeige an die Vorlage:

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

Unsere Django-Anwendung muss auf Anfragen hören, die an die homepage gerichtet sind. Um diese Einstellung zu konfigurieren, fügen wir diese Datei hinzu:

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

Damit unsere homepage -Anwendung bedient werden kann, müssen wir sie in einer anderen urls.py -Datei registrieren:

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

Die Basis-HTML-Vorlage unseres Projekts wird in einer neuen Datei 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>

Wir erweitern die Basisvorlage für unsere homepage -App in einer neuen Datei, 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 %}

Der letzte Schritt beim Erstellen unserer Benutzeroberfläche besteht darin, Django mitzuteilen, wo diese Vorlagen zu finden sind. Fügen wir unserer settings.py -Datei ein TEMPLATES['DIRS'] Wörterbuchelement hinzu:

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

Unsere Benutzeroberfläche ist jetzt implementiert und wir sind fast bereit, die Funktionalität unserer Anwendung zu testen. Bevor wir unsere Tests durchführen, müssen wir den letzten Teil unserer Umgebung einrichten: statisches Content-Caching.

Unsere statische Inhaltskonfiguration

Um architektonische Abkürzungen in unserem Entwicklungssystem zu vermeiden, konfigurieren wir das statische Inhaltscaching so, dass es unsere Produktionsumgebung widerspiegelt.

Wir speichern alle statischen Dateien unseres Projekts in einem einzigen Verzeichnis, src/static , und weisen Django an, diese Dateien vor der Bereitstellung zu sammeln.

Wir verwenden das Logo von Toptal für das favicon unserer Anwendung und speichern es als src/static/favicon.ico :

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

Als Nächstes konfigurieren wir Django, um die statischen Dateien zu sammeln:

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

Wir wollen nur unsere ursprünglichen statischen Dateien im Quellcode-Repository speichern; Wir möchten die produktionsoptimierten Versionen nicht speichern. Lassen Sie uns letzteres mit dieser einfachen Zeile zu unserer .gitignore hinzufügen:

 staticfiles

Da unser Quellcode-Repository die erforderlichen Dateien korrekt speichert, müssen wir nun unser Caching-System so konfigurieren, dass es mit diesen statischen Dateien funktioniert.

Statisches Datei-Caching

In der Produktion – und damit auch in unserer Entwicklungsumgebung – werden wir WhiteNoise verwenden, um die statischen Dateien unserer Django-Anwendung effizienter bereitzustellen.

Wir registrieren WhiteNoise als Middleware, indem wir das folgende Snippet zu unserer Datei src/hello_visitor/settings.py . Die Registrierungsreihenfolge ist streng definiert, und WhiteNoiseMiddleware muss unmittelbar nach SecurityMiddleware erscheinen:

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

Statisches Datei-Caching sollte jetzt in unserer Entwicklungsumgebung konfiguriert werden, damit wir unsere Anwendung ausführen können.

Betrieb unseres Entwicklungsservers

Wir haben eine vollständig codierte Anwendung und können jetzt den eingebetteten Entwicklungs-Webserver unseres Django mit diesem Befehl starten:

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

Wenn wir zu http://localhost:8000 navigieren, erhöht sich die Anzahl jedes Mal, wenn wir die Seite aktualisieren:

Ein Browserfenster, das den Hauptbildschirm unserer pydantischen Django-Anwendung zeigt, der sagt: "Hallo, Besucher!" in einer Zeile und "1" in der nächsten.

Wir haben jetzt eine funktionierende Anwendung, die ihre Besuchszahl erhöht, wenn wir die Seite aktualisieren.

Einsatzbereit

Dieses Tutorial hat alle Schritte behandelt, die zum Erstellen einer funktionierenden App in einer schönen Django-Entwicklungsumgebung erforderlich sind, die der Produktion entspricht. In Teil 3 behandeln wir die Bereitstellung unserer Anwendung in ihrer Produktionsumgebung. Es lohnt sich auch, unsere zusätzlichen Übungen zu erkunden, die die Vorteile von Django und Pydantic hervorheben: Sie sind im Code-Complete-Repository für dieses Pydantic-Tutorial enthalten.


Der Toptal Engineering Blog dankt Stephen Davidson für die Überprüfung und das Beta-Testen der in diesem Artikel vorgestellten Codebeispiele.