В этой статье рассказывается о том, что такое 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?».

