Оптимизируйте свою среду для разработки и производства: руководство по Pydantic, часть 2
Опубликовано: 2022-07-22Разработчики могут быть сами себе злейшими врагами. Я видел бесчисленное множество примеров, когда инженеры разрабатывали систему, которая не соответствовала их производственной среде. Этот диссонанс приводит к дополнительной работе и отслеживанию системных ошибок до более поздних этапов процесса разработки. Согласование этих настроек в конечном итоге облегчит непрерывное развертывание. Имея это в виду, мы создадим пример приложения в нашей среде разработки Django, упростив его с помощью Docker, pydantic и conda.
Типичная среда разработки использует:
- Локальный репозиторий;
- База данных PostgreSQL на основе Docker; а также
- Среда conda (для управления зависимостями Python).
Pydantic и Django подходят как для простых, так и для сложных проектов. Следующие шаги демонстрируют простое решение, которое показывает, как отразить нашу среду.
Конфигурация Git-репозитория
Прежде чем мы начнем писать код или устанавливать системы разработки, давайте создадим локальный репозиторий Git:
mkdir hello-visitor cd hello-visitor git init
Мы начнем с базового файла Python .gitignore
в корне репозитория. В этом руководстве мы будем добавлять в этот файл перед добавлением файлов, которые мы не хотим отслеживать с помощью Git.
Конфигурация Django PostgreSQL с использованием Docker
Django требует реляционную базу данных и по умолчанию использует SQLite. Обычно мы избегаем SQLite для хранения критически важных данных, поскольку он плохо справляется с одновременным доступом пользователей. Большинство разработчиков выбирают более типичную производственную базу данных, такую как PostgreSQL. В любом случае, мы должны использовать одну и ту же базу данных для разработки и производства. Этот архитектурный мандат является частью приложения The Twelve-factor.
К счастью, работать с локальным экземпляром PostgreSQL с помощью Docker и Docker Compose очень просто.
Чтобы не загрязнять наш корневой каталог, мы поместим файлы, связанные с Docker, в отдельные подкаталоги. Мы начнем с создания файла Docker Compose для развертывания 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:
Далее мы создадим файл среды docker-compose
для настройки нашего контейнера PostgreSQL:
# docker-services/.env POSTGRES_USER=postgres POSTGRES_PASSWORD=MyDBPassword123 # The 'maintenance' database POSTGRES_DB=postgres # The port exposed to localhost POSTGRES_PORT=5432
Теперь сервер базы данных определен и настроен. Запустим наш контейнер в фоновом режиме:
sudo docker compose --project-directory docker-services/ up -d
Важно отметить использование sudo в предыдущей команде. Это потребуется, если в нашей среде разработки не будут выполнены определенные шаги.
Создание базы данных
Давайте подключимся и настроим PostgreSQL с помощью стандартного набора инструментов pgAdmin4. Мы будем использовать те же учетные данные для входа, которые ранее были настроены в переменных среды.
Теперь давайте создадим новую базу данных с именем hello_visitor
:
С нашей базой данных мы готовы установить нашу среду программирования.
Управление средой Python через Miniconda
Теперь нам нужно настроить изолированную среду Python и необходимые зависимости. Для простоты настройки и обслуживания мы выбрали Miniconda.
Давайте создадим и активируем нашу среду conda:
conda create --name hello-visitor python=3.9 conda activate hello-visitor
Теперь мы создадим файл hello-visitor/requirements.txt
, перечисляющий наши зависимости 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
Далее мы попросим Python установить эти зависимости:
cd hello-visitor pip install -r requirements.txt
Теперь наши зависимости должны быть установлены для подготовки к разработке приложения.
Джанго строительные леса
Мы создадим наш проект и приложение, сначала запустив django-admin
, а затем запустив сгенерированный файл 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
Далее нам нужно настроить Django для загрузки нашего проекта. Файл settings.py
требует настройки массива INSTALLED_APPS
для регистрации нашего недавно созданного приложения homepage
:
# src/hello_visitor/settings.py # ... INSTALLED_APPS = [ "homepage.apps.HomepageConfig", "django.contrib.admin", # ... ] # ...
Конфигурация настроек приложения
Используя подход к настройкам pydantic и Django, показанный в первой части, нам нужно создать файл переменных среды для нашей системы разработки. Мы переместим наши текущие настройки в этот файл следующим образом:
- Создайте файл
src/.env
для хранения настроек среды разработки. - Скопируйте настройки из
src/hello_visitor/settings.py
и добавьте их вsrc/.env
. - Удалите эти скопированные строки из файла
settings.py
. - Убедитесь, что в строке подключения к базе данных используются те же учетные данные, которые мы ранее настроили.
Наш файл окружения src/.env
должен выглядеть так:
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" ]'
Мы настроим Django для чтения настроек из наших переменных среды, используя pydantic, с помощью этого фрагмента кода:
# 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 # ...
Если у вас возникнут какие-либо проблемы после внесения предыдущих правок, сравните созданный нами файл settings.py
с версией в нашем репозитории исходного кода.
Создание модели
Наше приложение отслеживает и отображает количество посещений домашней страницы. Нам нужна модель для хранения этого количества, а затем использовать объектно-реляционный преобразователь Django (ORM) для инициализации одной строки базы данных посредством переноса данных.
Сначала мы создадим нашу модель 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}"
Затем мы запустим миграцию для создания таблиц нашей базы данных:
# in the `src` folder python manage.py makemigrations python manage.py migrate
Чтобы убедиться, что таблица homepage_visitcounter
существует, мы можем просмотреть базу данных в pgAdmin4.
Далее нам нужно поместить начальное значение в нашу таблицу homepage_visitcounter
. Давайте создадим отдельный файл миграции, чтобы выполнить это с помощью шаблонов Django:
# from the 'src' directory python manage.py makemigrations --empty homepage
Мы настроим созданный файл миграции для использования метода VisitCounter.insert_visit_counter
, который мы определили в начале этого раздела:
# 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), ]
Теперь мы готовы выполнить эту измененную миграцию для приложения homepage
:
# from the 'src' directory python manage.py migrate homepage
Давайте проверим правильность выполнения миграции, взглянув на содержимое нашей таблицы:
Мы видим, что наша таблица homepage_visitcounter
существует и была заполнена начальным количеством посещений, равным 0. Разобравшись с нашей базой данных, мы сосредоточимся на создании нашего пользовательского интерфейса.
Создание и настройка наших представлений
Нам нужно реализовать две основные части нашего пользовательского интерфейса: представление и шаблон.
Мы создаем представление homepage
для увеличения количества посетителей, сохраняем его в базе данных и передаем это количество в шаблон для отображения:
# 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)
Наше приложение Django должно прослушивать запросы, направленные на homepage
. Чтобы настроить этот параметр, мы добавим этот файл:
# 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"), ]
Чтобы наше приложение homepage
обслуживалось, мы должны зарегистрировать его в другом файле urls.py
:
# 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), ]
Базовый HTML-шаблон нашего проекта будет находиться в новом файле 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>
Мы расширим базовый шаблон для нашего приложения homepage
в новом файле 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 %}
Последний шаг в создании нашего пользовательского интерфейса — сообщить Django, где найти эти шаблоны. Давайте добавим элемент словаря TEMPLATES['DIRS']
в наш файл settings.py
:
# src/hello_visitor/settings.py TEMPLATES = [ { ... 'DIRS': [BASE_DIR / 'templates'], ... }, ]
Наш пользовательский интерфейс теперь реализован, и мы почти готовы протестировать функциональность нашего приложения. Прежде чем мы приступим к тестированию, нам нужно установить последнюю часть нашей среды: кэширование статического контента.
Наша конфигурация статического контента
Чтобы избежать архитектурных сокращений в нашей системе разработки, мы настроим кэширование статического контента, чтобы оно отражало нашу производственную среду.
Мы будем хранить все статические файлы нашего проекта в одном каталоге, src/static
, и проинструктируем Django собрать эти файлы перед развертыванием.
Мы будем использовать логотип Toptal для favicon
нашего приложения и хранить его как src/static/favicon.ico
:
# from `src` folder mkdir static cd static wget https://frontier-assets.toptal.com/83b2f6e0d02cdb3d951a75bd07ee4058.png mv 83b2f6e0d02cdb3d951a75bd07ee4058.png favicon.ico
Далее мы настроим Django для сбора статических файлов:
# 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"
Мы хотим хранить наши исходные статические файлы только в репозитории исходного кода; мы не хотим хранить версии, оптимизированные для производства. Давайте добавим последний в наш .gitignore
с помощью этой простой строки:
staticfiles
Теперь, когда наш репозиторий исходного кода правильно хранит необходимые файлы, нам нужно настроить нашу систему кэширования для работы с этими статическими файлами.
Кэширование статических файлов
В производстве — а значит, и в нашей среде разработки — мы будем использовать WhiteNoise для более эффективного обслуживания статических файлов нашего приложения Django.
Мы регистрируем WhiteNoise как промежуточное ПО, добавляя следующий фрагмент в наш файл src/hello_visitor/settings.py
. Порядок регистрации строго определен, и WhiteNoiseMiddleware
должен стоять сразу после SecurityMiddleware
:
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # ... ] STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
Кэширование статических файлов теперь должно быть настроено в нашей среде разработки, что позволит нам запустить наше приложение.
Запуск нашего сервера разработки
У нас есть полностью закодированное приложение, и теперь мы можем запустить встроенный веб-сервер разработки Django с помощью этой команды:
# in the `src` folder python manage.py runserver
Когда мы переходим на http://localhost:8000
, счетчик будет увеличиваться каждый раз, когда мы обновляем страницу:
Теперь у нас есть работающее приложение, которое будет увеличивать количество посещений при обновлении страницы.
Готов к развертыванию
В этом руководстве рассмотрены все шаги, необходимые для создания работающего приложения в красивой среде разработки Django, соответствующей рабочей среде. В части 3 мы рассмотрим развертывание нашего приложения в рабочей среде. Также стоит изучить наши дополнительные упражнения, подчеркивающие преимущества Django и pydantic: они включены в репозиторий с полным кодом для этого руководства по pydantic.
Блог Toptal Engineering выражает благодарность Стивену Дэвидсону за обзор и бета-тестирование примеров кода, представленных в этой статье.