Ottimizza il tuo ambiente per lo sviluppo e la produzione: un tutorial Pydantic, parte 2
Pubblicato: 2022-07-22Gli sviluppatori possono essere i loro peggiori nemici. Ho visto innumerevoli esempi di ingegneri che sviluppano su un sistema che non corrisponde al loro ambiente di produzione. Questa dissonanza porta a un lavoro extra e non rileva gli errori di sistema fino a più tardi nel processo di sviluppo. L'allineamento di queste configurazioni alla fine faciliterà le distribuzioni continue. Con questo in mente, creeremo un'applicazione di esempio sul nostro ambiente di sviluppo Django, semplificata tramite Docker, pydantic e conda.
Un tipico ambiente di sviluppo utilizza:
- Un repository locale;
- Un database PostgreSQL basato su Docker; e
- Un ambiente conda (per gestire le dipendenze Python).
Pydantic e Django sono adatti per progetti sia semplici che complessi. I passaggi seguenti mostrano una soluzione semplice che evidenzia come rispecchiare i nostri ambienti.
Configurazione del repository Git
Prima di iniziare a scrivere codice o installare sistemi di sviluppo, creiamo un repository Git locale:
mkdir hello-visitor cd hello-visitor git init
Inizieremo con un file Python .gitignore
di base nella radice del repository. Durante questo tutorial, aggiungeremo questo file prima di aggiungere file che non vogliamo che Git tenga traccia.
Configurazione di Django PostgreSQL tramite Docker
Django richiede un database relazionale e, per impostazione predefinita, utilizza SQLite. In genere evitiamo SQLite per l'archiviazione di dati mission-critical in quanto non gestisce bene l'accesso simultaneo degli utenti. La maggior parte degli sviluppatori sceglie un database di produzione più tipico, come PostgreSQL. Indipendentemente da ciò, dovremmo utilizzare lo stesso database per lo sviluppo e la produzione. Questo mandato architettonico fa parte dell'app The Twelve-factor.
Fortunatamente, gestire un'istanza PostgreSQL locale con Docker e Docker Compose è un gioco da ragazzi.
Per evitare di inquinare la nostra directory principale, inseriremo i file relativi a Docker in sottodirectory separate. Inizieremo creando un file Docker Compose per distribuire 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:
Successivamente, creeremo un file di ambiente docker-compose
per configurare il nostro contenitore PostgreSQL:
# docker-services/.env POSTGRES_USER=postgres POSTGRES_PASSWORD=MyDBPassword123 # The 'maintenance' database POSTGRES_DB=postgres # The port exposed to localhost POSTGRES_PORT=5432
Il server del database è ora definito e configurato. Iniziamo il nostro contenitore in background:
sudo docker compose --project-directory docker-services/ up -d
È importante notare l'uso di sudo nel comando precedente. Sarà necessario a meno che non vengano seguiti passaggi specifici nel nostro ambiente di sviluppo.
Creazione database
Connettiamoci e configuriamo PostgreSQL usando una suite di strumenti standard, pgAdmin4. Utilizzeremo le stesse credenziali di accesso precedentemente configurate nelle variabili di ambiente.
Ora creiamo un nuovo database chiamato hello_visitor
:
Con il nostro database in atto, siamo pronti per installare il nostro ambiente di programmazione.
Gestione dell'ambiente Python tramite Miniconda
Ora dobbiamo configurare un ambiente Python isolato e le dipendenze richieste. Per semplicità di installazione e manutenzione, abbiamo scelto Miniconda.
Creiamo e attiviamo il nostro ambiente conda:
conda create --name hello-visitor python=3.9 conda activate hello-visitor
Ora creeremo un file, hello-visitor/requirements.txt
, enumerando le nostre dipendenze 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
Successivamente, chiederemo a Python di installare queste dipendenze:
cd hello-visitor pip install -r requirements.txt
Le nostre dipendenze dovrebbero ora essere installate in preparazione per il lavoro di sviluppo dell'applicazione.
Ponteggi Django
Impalgheremo il nostro progetto e la nostra app eseguendo prima django-admin
, quindi eseguendo un file che genera, 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
Successivamente, dobbiamo configurare Django per caricare il nostro progetto. Il file settings.py
richiede una regolazione dell'array INSTALLED_APPS
per registrare la nostra app per la homepage
appena creata:
# src/hello_visitor/settings.py # ... INSTALLED_APPS = [ "homepage.apps.HomepageConfig", "django.contrib.admin", # ... ] # ...
Configurazione delle impostazioni dell'applicazione
Utilizzando l'approccio delle impostazioni pydantic e Django mostrato nella prima puntata, dobbiamo creare un file delle variabili di ambiente per il nostro sistema di sviluppo. Sposteremo le nostre impostazioni correnti in questo file come segue:
- Crea il file
src/.env
per mantenere le impostazioni dell'ambiente di sviluppo. - Copia le impostazioni da
src/hello_visitor/settings.py
e aggiungile asrc/.env
. - Rimuovere quelle righe copiate dal file
settings.py
. - Assicurati che la stringa di connessione al database utilizzi le stesse credenziali che abbiamo configurato in precedenza.
Il nostro file di ambiente, src/.env
, dovrebbe assomigliare a questo:
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" ]'
Configureremo Django per leggere le impostazioni dalle nostre variabili di ambiente usando pydantic, con questo frammento di codice:
# 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 # ...
Se riscontri problemi dopo aver completato le modifiche precedenti, confronta il nostro file settings.py
creato con la versione nel nostro repository di codice sorgente.
Creazione del modello
La nostra applicazione tiene traccia e mostra il conteggio dei visitatori della home page. Abbiamo bisogno di un modello per mantenere quel conteggio e quindi utilizzare il mapper relazionale a oggetti (ORM) di Django per inizializzare una singola riga del database tramite una migrazione dei dati.
Innanzitutto, creeremo il nostro modello 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}"
Successivamente, attiveremo una migrazione per creare le nostre tabelle di database:
# in the `src` folder python manage.py makemigrations python manage.py migrate
Per verificare che esista la tabella homepage_visitcounter
, possiamo visualizzare il database in pgAdmin4.
Successivamente, dobbiamo inserire un valore iniziale nella nostra tabella homepage_visitcounter
. Creiamo un file di migrazione separato per ottenere ciò utilizzando lo scaffolding di Django:
# from the 'src' directory python manage.py makemigrations --empty homepage
Regoleremo il file di migrazione creato per utilizzare il metodo VisitCounter.insert_visit_counter
che abbiamo definito all'inizio di questa sezione:
# 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), ]
Ora siamo pronti per eseguire questa migrazione modificata per l'app della homepage
:
# from the 'src' directory python manage.py migrate homepage
Verifichiamo che la migrazione sia stata eseguita correttamente osservando il contenuto della nostra tabella:
Vediamo che la nostra tabella homepage_visitcounter
esiste ed è stata popolata con un numero di visite iniziale pari a 0. Con il nostro database al quadrato, ci concentreremo sulla creazione della nostra interfaccia utente.
Crea e configura le nostre viste
Dobbiamo implementare due parti principali della nostra interfaccia utente: una vista e un modello.
Creiamo la visualizzazione della homepage
per incrementare il conteggio dei visitatori, salvarlo nel database e passare quel conteggio al modello per la visualizzazione:
# 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)
La nostra applicazione Django ha bisogno di ascoltare le richieste rivolte alla homepage
. Per configurare questa impostazione, aggiungeremo questo file:
# 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"), ]
Affinché la nostra applicazione sulla homepage
sia servita, dobbiamo registrarla in un file urls.py
diverso:
# 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), ]
Il modello HTML di base del nostro progetto vivrà in un nuovo file, 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>
Estenderemo il modello di base per la nostra app homepage
in un nuovo file, 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 %}
L'ultimo passaggio nella creazione della nostra interfaccia utente è dire a Django dove trovare questi modelli. Aggiungiamo un elemento del dizionario TEMPLATES['DIRS']
al nostro file settings.py
:
# src/hello_visitor/settings.py TEMPLATES = [ { ... 'DIRS': [BASE_DIR / 'templates'], ... }, ]
La nostra interfaccia utente è ora implementata e siamo quasi pronti per testare la funzionalità della nostra applicazione. Prima di eseguire i test, dobbiamo mettere in atto la parte finale del nostro ambiente: la memorizzazione nella cache del contenuto statico.
La nostra configurazione del contenuto statico
Per evitare di prendere scorciatoie architetturali sul nostro sistema di sviluppo, configureremo la memorizzazione nella cache del contenuto statico per rispecchiare il nostro ambiente di produzione.
Conserveremo tutti i file statici del nostro progetto in un'unica directory, src/static
e istruiremo Django a raccogliere quei file prima della distribuzione.
Useremo il logo di Toptal per la favicon
della nostra applicazione e lo memorizzeremo come src/static/favicon.ico
:
# from `src` folder mkdir static cd static wget https://frontier-assets.toptal.com/83b2f6e0d02cdb3d951a75bd07ee4058.png mv 83b2f6e0d02cdb3d951a75bd07ee4058.png favicon.ico
Successivamente, configureremo Django per raccogliere i file statici:
# 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"
Vogliamo solo memorizzare i nostri file statici originali nel repository del codice sorgente; non vogliamo memorizzare le versioni ottimizzate per la produzione. Aggiungiamo quest'ultimo al nostro .gitignore
con questa semplice riga:
staticfiles
Con il nostro repository di codice sorgente che memorizza correttamente i file richiesti, ora dobbiamo configurare il nostro sistema di memorizzazione nella cache per funzionare con questi file statici.
Memorizzazione nella cache di file statici
In produzione, e quindi anche nel nostro ambiente di sviluppo, utilizzeremo WhiteNoise per servire i file statici della nostra applicazione Django in modo più efficiente.
Registriamo WhiteNoise come middleware aggiungendo il seguente snippet al nostro file src/hello_visitor/settings.py
. L'ordine di registrazione è rigorosamente definito e WhiteNoiseMiddleware
deve apparire immediatamente dopo SecurityMiddleware
:
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
La memorizzazione nella cache dei file statici dovrebbe ora essere configurata nel nostro ambiente di sviluppo, consentendoci di eseguire la nostra applicazione.
Esecuzione del nostro server di sviluppo
Abbiamo un'applicazione completamente codificata e ora possiamo avviare il server Web di sviluppo incorporato di Django con questo comando:
# in the `src` folder python manage.py runserver
Quando andiamo a http://localhost:8000
, il conteggio aumenterà ogni volta che aggiorniamo la pagina:
Ora abbiamo un'applicazione funzionante che aumenterà il conteggio delle visite man mano che aggiorniamo la pagina.
Pronto per la distribuzione
Questo tutorial ha coperto tutti i passaggi necessari per creare un'app funzionante in un bellissimo ambiente di sviluppo Django che corrisponda alla produzione. Nella parte 3, tratteremo la distribuzione della nostra applicazione nel suo ambiente di produzione. Vale anche la pena esplorare i nostri esercizi aggiuntivi che evidenziano i vantaggi di Django e pydantic: sono inclusi nel repository di codice completo per questo tutorial pydantic.
Il Toptal Engineering Blog estende la sua gratitudine a Stephen Davidson per la revisione e il beta test degli esempi di codice presentati in questo articolo.