Optimieren Sie Ihre Django-Einstellungen mit Type Hints: A Pydantic Tutorial
Veröffentlicht: 2022-07-22Django-Projekte haben mich früher frustriert, weil mir eine robuste und skalierbare Möglichkeit zum Hinzufügen neuer Umgebungen fehlte. Indem ich pydantische und Python-artige Hinweise zusammenbrachte, baute ich die leistungsstarke Grundlage auf, die ich brauchte.
Wie in PEP 484 beschrieben, unterstützen Typhinweise die statische Analyse, aber dieselben Anmerkungen sind auch zur Laufzeit verfügbar. Pakete von Drittanbietern wie pydantic bieten Laufzeittypprüfungen an, die diese zusätzlichen Metadaten verwenden. Pydantic verwendet Hinweise vom Typ Python, um die Verwaltung von Einstellungsmetadaten und die Validierung von Laufzeitdaten zu unterstützen.
Dieses pydantic-Tutorial zeigt die weitreichenden positiven Auswirkungen der Verwendung der pydantic-Einstellungsverwaltung mit Django.
Unsere Konfiguration entspricht den Best Practices, die auf der Website der Twelve-Factor App beschrieben sind:
- Definieren Sie nicht konstante und geheime Konfigurationen als Umgebungsvariablen.
- Definieren Sie in Entwicklungsumgebungen Umgebungsvariablen in einer
.env
-Datei und fügen Sie die.env
-Datei zu.gitignore
. - Verwenden Sie die Mechanismen des Cloud-Anbieters, um (geheime) Umgebungsvariablen für die QA-, Staging- und Produktionsumgebungen zu definieren.
- Verwenden Sie eine einzelne
settings.py
-Datei, die sich selbst anhand der Umgebungsvariablen konfiguriert. - Verwenden Sie pydantic, um Umgebungsvariablen zu lesen, zu prüfen, zu validieren und in Python-Variablen umzuwandeln, die die Django-Konfigurationen definieren.
Alternativ erstellen einige Entwickler mehrere Einstellungsdateien wie settings_dev.py
und settings_prod.py
. Leider lässt sich dieser Ansatz nicht gut skalieren. Dies führt zu Codeduplizierung, Verwirrung, schwer zu findenden Fehlern und höherem Wartungsaufwand.
Mit den oben genannten Best Practices ist das Hinzufügen einer beliebigen Anzahl von Umgebungen einfach, klar definiert und fehlerfrei. Obwohl wir eine kompliziertere Umgebungskonfiguration untersuchen könnten, konzentrieren wir uns der Klarheit halber auf zwei: Entwicklung und Produktion.
Wie sieht das in der Praxis aus?
Pydantic-Einstellungsverwaltung und Umgebungsvariablen
Wir konzentrieren uns nun auf ein Beispiel sowohl in der Entwicklung als auch in der Produktion. Wir zeigen, wie jede Umgebung ihre Einstellungen anders konfiguriert und wie pydantic jede unterstützt.
Unsere Beispielanwendung erfordert eine von Django unterstützte Datenbank, daher müssen wir die Datenbankverbindungszeichenfolge speichern. Wir verschieben die Konfigurationsinformationen für die Datenbankverbindung mithilfe des Python-Pakets dj-database-url in eine Umgebungsvariable, DATABASE_URL
. Bitte beachten Sie, dass diese Variable vom Typ str
ist und wie folgt formatiert ist:
postgres://{user}:{password}@{hostname}:{port}/{database-name} mysql://{user}:{password}@{hostname}:{port}/{database-name} oracle://{user}:{password}@{hostname}:{port}/{database-name} sqlite:///PATH
In unserer Entwicklungsumgebung können wir aus Gründen der Benutzerfreundlichkeit eine in Docker enthaltene PostgreSQL-Instanz verwenden, während wir in unserer Produktionsumgebung auf einen bereitgestellten Datenbankdienst verweisen.
Eine weitere Variable, die wir definieren möchten, ist ein boolescher Wert, DEBUG
. Das DEBUG-Flag in Django darf in einer Produktionsbereitstellung niemals aktiviert werden. Es soll zusätzliches Feedback während der Entwicklung erhalten. Im Debug-Modus zeigt Django beispielsweise detaillierte Fehlerseiten an, wenn eine Ausnahme auftritt.
Unterschiedliche Werte für Entwicklung und Produktion könnten wie folgt definiert werden:
Variablennamen | Entwicklung | Produktion |
---|---|---|
DATABASE_URL | postgres://postgres:mypw@localhost:5432/mydb | postgres://foo1:foo2@foo3:5432/foo4 |
DEBUG | True | False |
Wir verwenden das pydantic-Einstellungsverwaltungsmodul, um diese verschiedenen Sätze von Umgebungsvariablenwerten je nach Umgebung zu verwalten.
Vorbereitende Schritte
Um dies in die Praxis umzusetzen, beginnen wir mit der Konfiguration unserer Entwicklungsumgebung, indem wir unsere einzelne .env
-Datei mit diesem Inhalt erstellen:
DATABASE_URL=postgres://postgres:mypw@localhost:5432/mydb DEBUG=True
Als Nächstes fügen wir die .env
-Datei der .gitignore-Datei des Projekts .gitignore
. Die .gitignore
-Datei vermeidet das Speichern potenziell sensibler Informationen in der Quellcodeverwaltung.
Während dieser Ansatz in unserer Entwicklungsumgebung gut funktioniert, verwendet unsere Produktionsumgebungsspezifikation einen anderen Mechanismus. Unsere Best Practices schreiben vor, dass Produktionsumgebungsvariablen Umgebungsgeheimnisse verwenden. Bei Heroku werden diese Geheimnisse beispielsweise als Konfigurationsvariablen bezeichnet und über das Heroku-Dashboard konfiguriert. Sie werden der bereitgestellten Anwendung als Umgebungsvariablen zur Verfügung gestellt:
Danach müssen wir die Konfiguration der Anwendung anpassen, um diese Werte automatisch aus beiden Umgebungen zu lesen.
Konfigurieren der settings.py
von Django
Beginnen wir mit einem neuen Django-Projekt, um die wesentliche Struktur für unser Beispiel bereitzustellen. Wir bauen ein neues Django-Projekt mit dem folgenden Terminalbefehl auf:
$ django-admin startproject mysite
Wir haben jetzt eine grundlegende Projektstruktur für das mysite
Projekt. Die Dateistruktur des Projekts ist wie folgt:
mysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py
Die Datei settings.py
enthält Boilerplate-Code, mit dem wir die Anwendungskonfiguration verwalten können. Es hat viele vordefinierte Standardeinstellungen, die wir an die jeweilige Umgebung anpassen müssen.
Um diese Anwendungseinstellungen mit Umgebungsvariablen und Pydantic zu verwalten, fügen Sie diesen Code am Anfang der Datei settings.py
hinzu:
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""" DATABASE_URL: PostgresDsn DEBUG: bool = False 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=True) } DEBUG = config.DEBUG
Dieser Code macht Folgendes:
- Definiert eine Klasse
SettingsFromEnvironment
, die von der Klasse BaseSettings von pydanticBaseSettings
. - Definiert
DATABASE_URL
undDEBUG
, legt deren Typ und optionale Vorgabe mit Python-Typhinweisen fest. - Definiert eine Klassenkonfiguration, die
Config
anweist, nach den Variablen in einer.env
-Datei zu suchen, wenn sie nicht in den Umgebungsvariablen des Systems vorhanden sind. - Instantiiert die
Config
-Klasse in das Objektconfig
; die gewünschten Variablen werden alsconfig.DATABASE_URL
undconfig.DEBUG
. - Definiert die regulären Django-Variablen
DATABASES
undDEBUG
aus diesenconfig
.
Derselbe Code läuft in allen Umgebungen und pydantic kümmert sich um Folgendes:
- Es sucht nach den Umgebungsvariablen
DATABASE_URL
undDEBUG
.- Wenn sie als Umgebungsvariablen definiert sind, wie in der Produktion, werden diese verwendet.
- Andernfalls werden diese Werte aus der
.env
-Datei abgerufen. - Wenn es keinen Wert findet, wird es wie folgt vorgehen:
- Für
DATABASE_URL
wird ein Fehler ausgegeben. - Für
DEBUG
weist es einen Standardwert vonFalse
zu.
- Für
- Wenn es eine Umgebungsvariable findet, überprüft es die Feldtypen und gibt einen Fehler aus, wenn einer von ihnen falsch ist:
- Für
DATABASE_URL
überprüft es, ob sein Feldtyp eine URL imPostgresDsn
-Stil ist. - Für
DEBUG
überprüft es, ob sein Feldtyp ein gültiger, nicht strikter pydantischer boolescher Wert ist.
- Für
Bitte beachten Sie die explizite Einstellung der Umgebungsvariable des Betriebssystems aus dem Konfigurationswert für DATABASE_URL
. Es mag überflüssig erscheinen, os.environ["DATABASE_URL"] = config.DATABASE_URL
da DATABASE_URL
bereits als externe Umgebungsvariable definiert ist. Dies erlaubt pydantic jedoch, diese Variable zu parsen, zu prüfen und zu validieren. Wenn die Umgebungsvariable DATABASE_URL
fehlt oder falsch formatiert ist, gibt pydantic eine eindeutige Fehlermeldung aus. Diese Fehlerprüfungen sind von unschätzbarem Wert, wenn die Anwendung von der Entwicklung in nachfolgende Umgebungen übergeht.
Wenn eine Variable nicht definiert ist, wird entweder ein Standardwert zugewiesen oder ein Fehler fordert dazu auf, sie zu definieren. Alle generierten Eingabeaufforderungen geben auch den gewünschten Variablentyp an. Ein Nebeneffekt dieser Prüfungen ist, dass neue Teammitglieder und DevOps-Ingenieure leichter erkennen, welche Variablen definiert werden müssen. Dadurch werden die schwer zu findenden Probleme vermieden, die entstehen, wenn die Anwendung ohne alle definierten Variablen ausgeführt wird.
Das ist es. Die Anwendung verfügt jetzt über eine wartbare Implementierung zur Verwaltung von Einstellungen, die eine einzelne Version von settings.py
. Das Schöne an diesem Ansatz ist, dass er es uns ermöglicht, die richtigen Umgebungsvariablen in einer .env
-Datei oder jedem anderen gewünschten Mittel anzugeben, das über unsere Hosting-Umgebung verfügbar ist.
Der skalierbare Pfad
Ich habe die Django-Einstellungsverwaltung mit pydantic-Laufzeiteingabe in allen meinen Django-Projekten verwendet. Ich habe festgestellt, dass dies zu kleineren, besser wartbaren Codebasen führt. Es bietet auch einen gut strukturierten, selbstdokumentierenden und skalierbaren Ansatz zum Hinzufügen neuer Umgebungen.
Der nächste Artikel in dieser Reihe ist eine Schritt-für-Schritt-Anleitung zum Erstellen einer Django-Anwendung von Grund auf neu, mit pydantic-Einstellungsverwaltung und deren Bereitstellung in Heroku.
Der Toptal Engineering Blog dankt Stephen Davidson für die Überprüfung der in diesem Artikel vorgestellten Codebeispiele.
In einer früheren Version dieses Artikels wurde eine bestimmte Python-Version hervorgehoben. Da Python seit mehreren Jahren Typhinweise unterstützt, haben wir den Einführungstext verallgemeinert, indem wir diesen Versionsverweis entfernt haben.