Ottimizza il tuo ambiente per lo sviluppo e la produzione: un tutorial Pydantic, parte 2

Pubblicato: 2022-07-22

Gli 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 :

Una schermata pgAdmin4 all'interno di un browser che mostra la scheda Generale in una finestra di dialogo Crea database. Il campo di testo del database contiene il valore ciao_visitor, il campo del proprietario mostra l'utente postgres e il campo del commento è vuoto.

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:

  1. Crea il file src/.env per mantenere le impostazioni dell'ambiente di sviluppo.
  2. Copia le impostazioni da src/hello_visitor/settings.py e aggiungile a src/.env .
  3. Rimuovere quelle righe copiate dal file settings.py .
  4. 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:

Una schermata pgAdmin4 all'interno di un browser che mostra una query "SELECT * FROM public.homepage_visitcounter ORDER BY id ASC". La scheda Output dati mostra che c'è una riga all'interno di quella tabella. Il valore del campo ID chiave surrogata è 1 e il valore del campo conteggio è 0.

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:

Una finestra del browser che mostra la schermata principale della nostra pydantic applicazione Django, che dice "Ciao, visitatore!" su una riga e "1" su quella successiva.

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.