Optimieren Sie Ihre Umgebung für Entwicklung und Produktion: Ein Pydantic-Tutorial, Teil 2
Veröffentlicht: 2022-07-22Entwickler 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
:
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:
- Erstellen Sie die Datei
src/.env
, um unsere Entwicklungsumgebungseinstellungen zu speichern. - Kopieren Sie die Einstellungen aus
src/hello_visitor/settings.py
und fügen Sie sie zusrc/.env
. - Entfernen Sie diese kopierten Zeilen aus der Datei
settings.py
. - 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:
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:
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.