В этой статье рассказывается о том, что такое Werkzeug и как Flask использует его для своей основной HTTP-функциональности. Изучая материал статьи, вы попутно разработаете собственное WSGI-совместимое приложение с использованием Werkzeug, чтобы создать похожий на Flask веб-фреймворк!
Эта статья предполагает, что у вас уже есть опыт работы с Flask.
Содержание
- Зависимости Flask
- Что собой представляет Werkzeug?
- Приложение Hello World
- Связующее ПО для обслуживания статических файлов
- Шаблоны
- Маршрутизация
- Обработка исключений
- Обработка запросов
- Обработка форм
- Почему бы не использовать Werkzeug вместо Flask?
- Заключение
Зависимости Flask
Вероятно, вы заметили, что при каждой установке Flask вы также устанавливаете следующие зависимости:
Flask — это обертка вокруг всех этих компонентов.
$ pip install Flask $ pip freeze blinker==1.7.0 click==8.1.7 Flask==3.0.0 itsdangerous==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.3 Werkzeug==3.0.1 # !!!
Что собой представляет Werkzeug?
Werkzeug — это набор библиотек, которые можно использовать для создания WSGI-совместимых веб-приложений на Python.
WSGI (Web Server Gateway Interface) — это интерфейс между веб-сервером и веб-приложением на базе Python. Он необходим для веб-приложений на Python, поскольку веб-сервер не может напрямую взаимодействовать с Python.
Иными словами, Werkzeug предоставляет набор утилит для создания приложения на Python, которое может взаимодействовать с сервером WSGI, например, с Gunicorn.
Werkzeug предоставляет следующую функциональность, используемую Flask:
- Обработка запросов
- Обработка ответов
- Маршрутизация URL
- Middleware
- HTTP-утилиты
- Обработка исключений
Кроме того, он предоставляет базовый сервер разработки с возможностью горячей перезагрузки.
Давайте рассмотрим пример создания веб-приложения с помощью Werkzeug. Мы также разберем, как Flask реализует аналогичную функциональность.
От редакции Pythonist: возможно, вас заинтересует статья «Лучшие книги по Flask для Python-разработчиков».
Приложение Hello World
В качестве введения в Werkzeug давайте начнем с создания приложения «Hello World», использующего некоторые из ключевых функций, предоставляемых Werkzeug.
Исходный код проекта, о котором пойдет речь в этой статье, вы можете найти на GitLab: https://gitlab.com/patkennedy79/werkzeug_movie_app.
Установка
Начните с создания нового проекта:
$ mkdir werkzeug_movie_app $ cd werkzeug_movie_app $ python3 -m venv venv $ source venv/bin/activate (venv)$
Установите Werkzeug, Jinja2 и redis-py:
(venv)$ pip install Werkzeug Jinja2 redis (venv)$ pip freeze > requirements.txt
Redis будет использоваться в качестве хранилища данных о фильмах.
Приложение
Werkzeug — это набор библиотек, используемых для создания WSGI-совместимого веб-приложения. Он не предоставляет высокоуровневых классов, как Flask
, для создания полноценного веб-приложения. Вместо этого вам нужно самостоятельно создать приложение из библиотек Werkzeug.
Создайте новый файл app.py в папке верхнего уровня вашего проекта:
from werkzeug.wrappers import Request, Response class MovieApp(object): """Implements a WSGI application for managing your favorite movies.""" def __init__(self): pass def dispatch_request(self, request): """Dispatches the request.""" return Response('Hello World!') def wsgi_app(self, environ, start_response): """WSGI application that processes requests and returns responses.""" request = Request(environ) response = self.dispatch_request(request) return response(environ, start_response) def __call__(self, environ, start_response): """The WSGI server calls this method as the WSGI application.""" return self.wsgi_app(environ, start_response) def create_app(): """Application factory function that returns an instance of MovieApp.""" app = MovieApp() return app
Класс MovieApp
реализует WSGI-совместимое веб-приложение, которое обрабатывает запросы от различных пользователей и генерирует ответы для них. Вот как этот класс взаимодействует с сервером WSGI:
Когда поступает запрос, он обрабатывается в функции wsgi_app()
:
def wsgi_app(self, environ, start_response): """WSGI application that processes requests and returns responses.""" request = Request(environ) response = self.dispatch_request(request) return response(environ, start_response)
Окружение (environ
) автоматически обрабатывается в классе Request
для создания объекта request
. Затем request
обрабатывается в dispatch_request()
. Для этого примера функция dispatch_request()
возвращает ответ ‘Hello World!’. Этот ответ затем возвращается из функции wsgi_app()
.
Сравнение с Flask
MovieApp — это упрощенная версия класса Flask
.
В классе Flask
функция wsgi_app()
фактически является WSGI-приложением, которое взаимодействует с WSGI-сервером. Кроме того, dispatch_request()
и full_dispatch_request()
используются для диспетчеризации запросов. Диспетчеризация сопоставляет URL с соответствующей функцией представления и обрабатывает исключения.
Сервер разработки
Добавьте следующий код в нижнюю часть файла app.py, чтобы запустить сервер разработки Werkzeug:
if __name__ == '__main__': # Run the Werkzeug development server to serve the WSGI application (MovieApp) from werkzeug.serving import run_simple app = create_app() run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
Запустите приложение:
(venv)$ python app.py
Перейдите по адресу http://127.0.0.1:5000, чтобы увидеть сообщение «Hello World!».
Сравнение с Flask
В классе Flask
есть эквивалентный метод run()
, который использует сервер разработки Werkzeug.
Связующее ПО для обслуживания статических файлов
В веб-приложениях связующее ПО (англ. middleware) — это программный компонент, который может быть добавлен в конвейер обработки запросов/ответов для выполнения определенной функции.
Одной из важных функций, которую должен выполнять веб-сервер/приложение, является обслуживание статических файлов (CSS, JavaScript и файлы изображений). Werkzeug предоставляет middleware для этой функциональности — SharedDataMiddleware
.
SharedDataMiddleware
идеально подходит для работы с сервером разработки Werkzeug для обслуживания статических файлов.
В производственной среде вы должны заменить сервер разработки Werkzeug и SharedDataMiddleware
на веб-сервер, такой как Nginx, и сервер WSGI, такой как Gunicorn.
Чтобы использовать SharedDataMiddleware
, добавьте в проект новую папку «static» вместе с папками «css» и «img»:
├── app.py ├── requirements.txt └── static ├── css └── img
В папку «static/img» добавьте логотип Flask (адрес — https://gitlab.com/patkennedy79/werkzeug_movie_app/-/blob/main/static/img/flask.png). Сохраните его под именем flask.png.
Далее разверните функцию фабрики приложения:
def create_app(): """Application factory function that returns an instance of MovieApp.""" app = MovieApp() app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { '/static': os.path.join(os.path.dirname(__file__), 'static') }) return app
Обновите импорты в верхней части:
import os from werkzeug.middleware.shared_data import SharedDataMiddleware from werkzeug.wrappers import Request, Response
Теперь при обработке запроса приложением Werkzeug (app
), он сначала направляется в SharedDataMiddleware
, чтобы определить, был ли запрошен статический файл:
Если запрашивается статический файл, SharedDataMiddleware
сгенерирует ответ со статическим файлом. В противном случае запрос передается по цепочке в приложение Werkzeug для обработки в wsgi_app()
.
Чтобы увидеть SharedDataMiddleware
в действии, запустите сервер и перейдите по адресу http://127.0.0.1:5000/static/img/flask.png. Вы увидите логотип Flask.
Полный список middleware-решений, предоставляемых Werkzeug, можно найти в документации по связующему ПО.
Сравнение с Flask
Flask не использует SharedDataMiddleware
. У него другой подход к обслуживанию статических файлов. По умолчанию, если статическая папка существует, Flask автоматически добавляет новое правило URL для обслуживания статических файлов.
Чтобы проиллюстрировать эту концепцию, запустите flask routes
в проекте верхнего уровня приложения Flask, и вы увидите:
(venv)$ flask routes Endpoint Methods Rule ----------- ------- ----------------------- index GET / static GET /static/<path:filename>
Шаблоны
Как обычно делается в проектах Flask, в качестве шаблонизатора для нашего приложения мы будем использовать Jinja.
Начнем с добавления в проект новой папки под названием «templates»:
├── app.py ├── requirements.txt ├── static │ ├── css │ └── img │ └── flask.png └── templates
Чтобы использовать Jinja, расширьте конструктор класса MovieApp
:
def __init__(self): """Initializes the Jinja templating engine to render from the 'templates' folder.""" template_path = os.path.join(os.path.dirname(__file__), 'templates') self.jinja_env = Environment(loader=FileSystemLoader(template_path), autoescape=True)
Добавьте импорт:
from jinja2 import Environment, FileSystemLoader
Сравнение с Flask
Flask также использует Jinja Environment для создания шаблонизатора.
В классе MovieApp
добавьте новый метод render_template()
:
def render_template(self, template_name, **context): """Renders the specified template file using the Jinja templating engine.""" template = self.jinja_env.get_template(template_name) return Response(template.render(context), mimetype='text/html')
Этот метод принимает template_name
и любые переменные для передачи шаблонизатору (**context
). Затем он генерирует Response
, используя метод render()
из Jinja.
Сравнение с Flask
Функция render_template()
выглядит знакомо, не так ли? Это одна из самых используемых функций во Flask.
Чтобы увидеть render_template()
в действии, обновите dispatch_request()
для рендеринга шаблона:
def dispatch_request(self, request): """Dispatches the request.""" return self.render_template('base.html')
Теперь все запросы к приложению будут отрисовывать шаблон templates/base.html.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Werkzeug Movie App</title> <!-- CSS file for styling the application --> <link rel="stylesheet" href="/static/css/style.css" type="text/css"> </head> <body> <h1>Werkzeug Movie App</h1> {% block body %} {% endblock %} </body> </html>
Обязательно добавьте этот шаблон в папку «templates» и сохраните копию https://gitlab.com/patkennedy79/werkzeug_movie_app/-/blob/main/static/css/style.css в static/css/style.css.
Запустите сервер. Перейдите на страницу http://127.0.0.1:5000. Теперь вы должны увидеть следующее:
Маршрутизация
Маршрутизация означает сопоставление URL с соответствующей функцией представления. Werkzeug предоставляет класс Map
, который позволяет сопоставлять URL с функциями представления с помощью объектов Rule
.
Давайте создадим объект Map в конструкторе MovieApp, чтобы проиллюстрировать, как это работает:
def __init__(self): """Initializes the Jinja templating engine to render from the 'templates' folder.""" template_path = os.path.join(os.path.dirname(__file__), 'templates') self.jinja_env = Environment(loader=FileSystemLoader(template_path), autoescape=True) self.url_map = Map([ Rule('/', endpoint='index'), Rule('/movies', endpoint='movies'), ])
Не забудьте про импорт:
from werkzeug.routing import Map, Rule
Каждый объект Rule
определяет URL и функцию представления (endpoint
), которую нужно вызвать, если URL совпадает:
self.url_map = Map([ Rule('/', endpoint='index'), Rule('/movies', endpoint='movies'), ])
Например, когда запрашивается домашняя страница (‘/’), должна быть вызвана функция представления index
.
Сравнение с Flask
Одна из удивительных особенностей Flask — декоратор @route, который используется для назначения URL для функции представления. Этот декоратор обновляет url_map для приложения Flask, аналогично прописанной вручную url_map
, которую мы определили выше.
Чтобы использовать сопоставление URL, необходимо обновить функцию dispatch_request()
:
def dispatch_request(self, request): """Dispatches the request.""" adapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = adapter.match() return getattr(self, endpoint)(request, **values) except HTTPException as e: return e
Теперь при поступлении запроса в dispatch_request()
, для попытки сопоставления (match()
) URL с записью будет использоваться url_map
. Если запрашиваемый URL включен в url_map
, то будет вызвана соответствующая функция представления (endpoint
). Если URL не будет найден в url_map
, то будет вызвано исключение. Об обработке исключений речь пойдет чуть позже.
Добавьте импорт:
from werkzeug.exceptions import HTTPException
Мы указали две функции представления в url_map
, поэтому давайте создадим их в классе MovieApp
:
def index(self, request): return self.render_template('base.html') def movies(self, request): return self.render_template('movies.html')
templates/base.html мы создали в предыдущем разделе, а templates/movies.html должен быть создан сейчас:
{% extends "base.html" %} {% block body %} <div class="table-container"> <table> <!-- Table Header --> <thead> <tr> <th>Index</th> <th>Movie Title</th> </tr> </thead> <!-- Table Elements (Rows) --> <tbody> <tr> <td>1</td> <td>Knives Out</td> </tr> <tr> <td>2</td> <td>Pirates of the Caribbean</td> </tr> <tr> <td>3</td> <td>Inside Man</td> </tr> </tbody> </table> </div> {% endblock %}
Этот файл шаблона использует наследование шаблонов, чтобы использовать base.html в качестве родительского шаблона. Он генерирует таблицу из трех фильмов.
Страница http://127.0.0.1:5000 должна выглядеть так же:
Однако если вы перейдете на http://127.0.0.1:5000/movies, то увидите таблицу фильмов:
Обработка исключений
Попробуйте перейти на http://127.0.0.1:5000/movies2:
Возвращаемая страница — это страница ошибки по умолчанию, когда URL не найден в url_map:
def dispatch_request(self, request): """Dispatches the request.""" adapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = adapter.match() return getattr(self, endpoint)(request, **values) except HTTPException as e: return e
Кроме того, в консоли вы должны увидеть следующее:
127.0.0.1 - - [07/Mar/2021 12:13:17] "GET /movies2 HTTP/1.1" 404 -
Давайте создадим пользовательскую страницу ошибки, расширив dispatch_request()
:
def dispatch_request(self, request): """Dispatches the request.""" adapter = self.url_map.bind_to_environ(request.environ) try: endpoint, values = adapter.match() return getattr(self, endpoint)(request, **values) except NotFound: return self.error_404() except HTTPException as e: return e
Обновите импорт:
from werkzeug.exceptions import HTTPException, NotFound
Теперь, если URL не будет найден в url_map
, он будет обрабатываться вызовом error_404()
. Создайте этот новый метод в классе MovieApp
:
def error_404(self): response = self.render_template("404.html") response.status_code = 404 return response
Создайте templates/404.html:
{% extends "base.html" %} {% block body %} <div class="error-description"> <h2>Page Not Found (404)</h2> <h4>What you were looking for is just not there!</h4> <h4><a href="/">Werkzeug Movie App</a></h4> </div> {% endblock %}
Теперь при переходе на сайт http://127.0.0.1:5000/movies2 вы должны увидеть дружелюбное сообщение:
Сравнение с Flask
Когда full_dispatch_request()
в классе Flask
обнаруживает исключение, оно изящно обрабатывается в handle_user_exceptions()
. Flask также позволяет создавать пользовательские страницы ошибок для всех кодов ошибок HTTP.
Обработка запросов
В этом разделе мы добавим в приложение форму, чтобы пользователь мог ввести свои любимые фильмы.
Redis
Как уже говорилось, для хранения фильмов мы будем использовать Redis — хранилище структур данных в памяти — из-за его высокой скорости чтения/записи и простоты настройки.
Установка
Установите и запустите Redis.
Самый быстрый способ запустить Redis — это использовать Docker:
$ docker run --name some-redis -d -p 6379:6379 redis
Проверка, что контейнер Redis запущен:
$ docker ps
Остановка работающего контейнера Redis:
$ docker stop some-redis # Use name of Docker container
Если вы не используете Docker, обратите внимание на следующие ресурсы:
- Install and Configure Redis on Mac OS X via Homebrew
- DigitalOcean — How to Install and Secure Redis on Ubuntu
Использование
Чтобы использовать Redis, начните с обновления конструктора MovieApp
для создания экземпляра StrictRedis
:
def __init__(self, config): # Updated!! """Initializes the Jinja templating engine to render from the 'templates' folder, defines the mapping of URLs to view methods, and initializes the Redis interface.""" template_path = os.path.join(os.path.dirname(__file__), 'templates') self.jinja_env = Environment(loader=FileSystemLoader(template_path), autoescape=True) self.url_map = Map([ Rule('/', endpoint='index'), Rule('/movies', endpoint='movies'), ]) self.redis = StrictRedis(config['redis_host'], config['redis_port'], decode_responses=True) # New!!
Конструктор (__init__()
) также имеет дополнительный аргумент (config
), который используется для создания экземпляра StrictRedis
.
Импорт:
from redis import StrictRedis
Параметры конфигурации, передаваемые в конструктор, должны быть указаны в функции фабрики приложения:
def create_app(): """Application factory function that returns an instance of MovieApp.""" app = MovieApp({'redis_host': '127.0.0.1', 'redis_port': 6379}) app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { '/static': os.path.join(os.path.dirname(__file__), 'static') }) return app
Обработка форм
Чтобы пользователь мог добавить фильм в хранилище Redis, нам нужно добавить новую функцию представления в url_map
:
def __init__(self, config): """Initializes the Jinja templating engine to render from the 'templates' folder, defines the mapping of URLs to view methods, and initializes the Redis interface.""" ... self.url_map = Map([ Rule('/', endpoint='index', methods=['GET']), Rule('/movies', endpoint='movies', methods=['GET']), Rule('/add', endpoint='add_movie', methods=['GET', 'POST']), # !!! ]) ...
Записи Rule
в url_map
были расширены, чтобы указать HTTP-методы, разрешенные для каждого URL. Кроме того, был добавлен URL ‘/add’:
Rule('/add', endpoint='add_movie', methods=['GET', 'POST']),
Если URL-адрес ‘/add’ будет запрошен с помощью методов GET или POST, то будет вызвана функция представления add_movie()
.
Далее нам нужно создать функцию представления add_movie()
в классе MovieApp
:
def add_movie(self, request): """Adds a movie to the list of favorite movies.""" if request.method == 'POST': movie_title = request.form['title'] self.redis.lpush('movies', movie_title) return redirect('/movies') return self.render_template('add_movie.html')
Импорт:
from werkzeug.utils import redirect
Если к ‘/add’ будет сделан GET-запрос, то функция add_movie()
отобразит файл templates/add_movie.html. Если к ‘/add’ будет сделан POST-запрос, то данные формы будут сохранены в хранилище Redis в списке movies
, и пользователь будет перенаправлен к списку фильмов.
Создайте файл шаблона templates/add_movie.html:
{% extends "base.html" %} {% block body %} <div class="form-container"> <form method="post"> <div class="field"> <label for="movieTitle">Movie Title:</label> <input type="text" id="movieTitle" name="title"/> </div> <div class="field"> <button type="submit">Submit</button> </div> </form> </div> {% endblock %}
Отображение фильмов
Поскольку теперь мы храним фильмы в Redis, функцию представления movie()
нужно обновить, чтобы она считывала данные из списка movies
в Redis:
def movies(self, request): """Displays the list of favorite movies.""" movies = self.redis.lrange('movies', 0, -1) return self.render_template('movies.html', movies=movies)
Список фильмов передается в файл шаблона templates/movies.html, который необходимо обновить, чтобы он перебирал этот список для создания таблицы фильмов:
{% extends "base.html" %} {% block body %} <div class="table-container"> <table> <!-- Table Header --> <thead> <tr> <th>Index</th> <th>Movie Title</th> </tr> </thead> <!-- Table Elements (Rows) --> <tbody> {% for movie in movies %} <tr> <td>{{ loop.index }}</td> <td>{{ movie }}</td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
Чтобы увидеть обработку формы в действии, перейдите на сайт http://127.0.0.1:5000/add и добавьте новый фильм:
После отправки формы вы должны быть автоматически перенаправлены в список фильмов (который может включать ранее добавленные фильмы):
Вот и все!
Почему бы не использовать Werkzeug вместо Flask?
Werkzeug предоставляет большую часть ключевых функций Flask, но Flask добавляет ряд мощных возможностей, таких как:
- Сессии
- Контексты приложений и запросов
- Блюпринты
- Функции обратного вызова запросов
- Утилиты:
- декоратор
@route
- функция
url_for()
- декоратор
- Команды CLI
- Обработка исключений
- Тестовый клиент
- Оболочка Flask
- Логирование
- Сигналы
- Расширения
Как и в случае с любым веб-фреймворком, не изобретайте велосипед! Flask является гораздо лучшим вариантом (по сравнению с Werkzeug) для веб-разработки благодаря своему богатому набору функций и большой коллекции расширений.
Заключение
В этой статье мы сделали обзор Werkzeug, который является одним из ключевых компонентов Flask, и показали, как с его помощью создать простое веб-приложение. Безусловно, важно понимать, как работают базовые библиотеки во Flask. Но сложность создания веб-приложения с помощью Werkzeug должна проиллюстрировать, насколько легко разрабатывать веб-приложения с помощью Flask!
А если вам интересно узнать, как тестировать приложения Werkzeug, ознакомьтесь с тестами для Werkzeug Movie App: https://gitlab.com/patkennedy79/werkzeug_movie_app/-/tree/main/tests.
Перевод статьи «What is Werkzeug?».