Spec-Zone.ru › Werkzeug 2.1

Руководство по Werkzeug

Добро пожаловать в руководство по Werkzeug, в котором мы создадим клон сервиса TinyURL, хранящий URL-адреса в экземпляре redis. Библиотеки, которые мы будем использовать для этого приложения, — Jinja 2 для шаблонов, redis для уровня базы данных и, конечно же, Werkzeug для уровня WSGI.

Вы можете использовать pip для установки необходимых библиотек:

pip install Jinja2 redis Werkzeug

Также убедитесь, что на вашем локальном компьютере запущен сервер redis. Если вы работаете на OS X, вы можете использовать brew для его установки:

brew install redis

Если вы работаете на Ubuntu или Debian, вы можете использовать apt-get:

sudo apt-get install redis-server

Redis был разработан для UNIX-систем и никогда по-настоящему не был предназначен для работы в Windows. Однако неофициальные порты работают достаточно хорошо в целях разработки. Вы можете получить их из github.

Представление сервиса Shortly

В этом руководстве мы вместе создадим простую службу сокращения URL-адресов с помощью Werkzeug. Пожалуйста, имейте в виду, что Werkzeug — это не фреймворк, это библиотека с инструментами для создания собственного фреймворка или приложения, и поэтому она очень гибкая. Использованный здесь подход — лишь один из многих возможных.

В качестве хранилища данных мы будем использовать redis вместо реляционной базы данных, чтобы всё было проще, и потому что redis отлично подходит для таких задач.

Конечный результат будет выглядеть примерно так:

a screenshot of shortly

Шаг 0: Введение в WSGI

Werkzeug — это библиотека утилит для WSGI. Сам WSGI — это протокол или соглашение, которое гарантирует, что ваше веб-приложение может общаться с веб-сервером и, что более важно, что веб-приложения хорошо работают вместе.

Базовое приложение «Hello World» в WSGI без помощи Werkzeug выглядит так:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['Hello World!'.encode('utf-8')]

Веб-приложение WSGI — это нечто, что можно вызвать и передать ему словарь environ и start_response вызываемую функцию. Environ содержит всю поступающую информацию, функция start_response используется для обозначения начала ответа. С помощью Werkzeug вам не нужно иметь дело напрямую ни с тем, ни с другим, так как предоставляются объекты запроса и ответа для работы с ними.

Данные запроса берут объект environ и позволяют получить доступ к данным из этого environ удобным способом. Объект ответа сам по себе является приложением WSGI и предоставляет гораздо более удобный способ создания ответов.

Вот как вы написали бы это приложение с использованием объектов ответа:

from werkzeug.wrappers import Response

def application(environ, start_response):
    response = Response('Hello World!', mimetype='text/plain')
    return response(environ, start_response)

И вот расширенная версия, которая анализирует строку запроса в URL (более важно, параметр name в URL для подстановки «World» другим словом):

from werkzeug.wrappers import Request, Response

def application(environ, start_response):
    request = Request(environ)
    text = f"Hello {request.args.get('name', 'World')}!"
    response = Response(text, mimetype='text/plain')
    return response(environ, start_response)

И это всё, что вам нужно знать о WSGI.

Шаг 1: Создание папок

Прежде чем начать, давайте создадим папки, необходимые для этого приложения:

/shortly
    /static
    /templates

Папка shortly — это не пакет Python, а просто место, куда мы помещаем наши файлы. Непосредственно в эту папку мы затем поместим наш основной модуль на следующих шагах. Файлы в папке static доступны пользователям приложения через HTTP. Это место, куда попадают файлы CSS и JavaScript. В папке templates мы сделаем так, чтобы Jinja2 искал шаблоны. Шаблоны, которые вы создадите позже в этом руководстве, будут помещены в эту папку.

Шаг 2: Основная структура

Теперь давайте перейдём к делу и создадим модуль для нашего приложения. Давайте создадим файл, названный shortly.py в папке shortly. Сначала нам понадобится много импортов. Я внесу все импорты сюда, даже если они не используются сразу, чтобы не запутаться:

import os
import redis
from werkzeug.urls import url_parse
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.utils import redirect
from jinja2 import Environment, FileSystemLoader

Затем мы можем создать основную структуру для нашего приложения и функцию для создания нового экземпляра, необязательно с фрагментом WSGI-среды, экспортирующей все файлы в папке static в веб:

class Shortly(object):

    def __init__(self, config):
        self.redis = redis.Redis(
            config['redis_host'], config['redis_port'], decode_responses=True
        )

    def dispatch_request(self, request):
        return Response('Hello World!')

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)


def create_app(redis_host='localhost', redis_port=6379, with_static=True):
    app = Shortly({
        'redis_host':       redis_host,
        'redis_port':       redis_port
    })
    if with_static:
        app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
            '/static':  os.path.join(os.path.dirname(__file__), 'static')
        })
    return app

Наконец, мы можем добавить фрагмент кода, который запустит локальный сервер разработки с автоматической перезагрузкой кода и отладчиком:

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    app = create_app()
    run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)

Основная идея заключается в том, что наш класс Shortly — это фактическое WSGI-приложение. Метод __call__ напрямую перенаправляет вызов на wsgi_app. Это сделано для того, чтобы мы могли обернуть wsgi_app для применения middleware, как мы делаем в функции create_app. Фактический метод wsgi_app затем создаёт объект Request и вызывает метод dispatch_request, который должен вернуть объект Response, который затем оценивается как WSGI-приложение снова. Как вы можете видеть: черепахи до бесконечности. И класс Shortly, который мы создаём, а также любой объект запроса в Werkzeug реализуют интерфейс WSGI. В результате вы даже можете вернуть другое WSGI-приложение из метода dispatch_request.

Функция-фабрика create_app может быть использована для создания нового экземпляра нашего приложения. Она не только передаёт некоторые параметры в качестве конфигурации приложению, но также необязательно добавляет middleware WSGI, который экспортирует статические файлы. Таким образом, мы можем получить доступ к файлам из папки static даже тогда, когда мы не настраиваем наш сервер для их предоставления, что очень полезно для разработки.

Интермедия: Запуск приложения

Теперь вы должны иметь возможность выполнить файл с помощью python и увидеть сервер на вашем локальном компьютере:

$ python shortly.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader: stat() polling

Он также сообщает вам, что релоадер активен. Он будет использовать различные методы, чтобы определить, изменился ли какой-либо файл на диске, и затем автоматически перезапустится.

Просто перейдите по URL-адресу, и вы должны увидеть «Hello World!».

Шаг 3: Среда

Теперь, когда у нас есть базовый класс приложения, мы можем заставить конструктор выполнять полезную работу и предоставить несколько вспомогательных методов, которые могут пригодиться. Нам нужно будет отображать шаблоны и подключаться к redis, поэтому давайте немного расширим класс:

def __init__(self, config):
    self.redis = redis.Redis(config['redis_host'], config['redis_port'])
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)

def render_template(self, template_name, **context):
    t = self.jinja_env.get_template(template_name)
    return Response(t.render(context), mimetype='text/html')

Шаг 4: Маршрутизация

Следующим шагом является маршрутизация. Маршрутизация — это процесс сопоставления и анализа URL-адреса, чтобы получить данные, которые мы можем использовать. Werkzeug предоставляет гибкую встроенную систему маршрутизации, которую мы можем использовать для этого. Способ её работы заключается в том, что вы создаёте экземпляр Map и добавляете несколько объектов Rule. Каждый правило имеет шаблон, который он пытается сопоставить с URL-адресом, и «точку входа». Точка входа — это обычно строка и может быть использована для уникальной идентификации URL-адреса. Мы также могли бы использовать это для автоматического обратного перехода к URL-адресу, но в этом руководстве мы этого делать не будем.

Просто добавьте это в конструктор:

self.url_map = Map([
    Rule('/', endpoint='new_url'),
    Rule('/<short_id>', endpoint='follow_short_link'),
    Rule('/<short_id>+', endpoint='short_link_details')
])

Здесь мы создаём карту URL-адресов с тремя правилами. / для корня адресного пространства, где мы просто перенаправим вызов на функцию, которая реализует логику создания нового URL-адреса. Затем одно, которое следует за короткой ссылкой до целевого URL-адреса, и ещё одно с тем же правилом, но с плюсом (+) в конце, чтобы показать подробности ссылки.

Итак, как мы можем найти путь от точки входа к функции? Это зависит от вас. В этом руководстве мы будем делать это, вызывая метод on_ + точка входа в самом классе. Вот как это работает:

def dispatch_request(self, request):
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, f'on_{endpoint}')(request, **values)
    except HTTPException as e:
        return e

Мы привязываем карту URL-адресов к текущей среде и получаем обратно URLAdapter. Адаптер может быть использован для сопоставления запроса, а также для обратного перехода к URL-адресам. Метод match вернёт точку входа и словарь значений в URL-адресе. Например, правило для follow_short_link имеет переменную часть, называемую short_id. Когда мы перейдём по адресу http://localhost:5000/foo, мы получим следующие значения:

endpoint = 'follow_short_link'
values = {'short_id': 'foo'}

Если ничего не совпадёт, будет выброшено исключение NotFound, которое является HTTPException. Все исключения HTTP также являются WSGI-приложениями сами по себе, которые отображают страницу ошибки по умолчанию. Так что мы просто ловим их и возвращаем саму ошибку.

Если всё пройдёт хорошо, мы вызываем функцию on_ + точка входа и передаём ей запрос в качестве аргумента, а также все аргументы URL-адреса в качестве именованных аргументов, и возвращаем объект ответа, который метод возвращает.

Шаг 5: Первый вид

Давайте начнём с первого вида: того, который предназначен для новых URL-адресов:

def on_new_url(self, request):
    error = None
    url = ''
    if request.method == 'POST':
        url = request.form['url']
        if not is_valid_url(url):
            error = 'Please enter a valid URL'
        else:
            short_id = self.insert_url(url)
            return redirect(f"/{short_id}+")
    return self.render_template('new_url.html', error=error, url=url)

Эта логика должна быть понятна. В основном мы проверяем, является ли метод запроса POST, в этом случае мы проверяем URL-адрес и добавляем новую запись в базу данных, а затем перенаправляем на страницу подробностей. Это означает, что нам нужно написать функцию и вспомогательный метод. Для проверки URL-адресов этого достаточно:

def is_valid_url(url):
    parts = url_parse(url)
    return parts.scheme in ('http', 'https')

Для вставки URL-адреса всё, что нам нужно, это этот маленький метод в нашем классе:

def insert_url(self, url):
    short_id = self.redis.get(f'reverse-url:{url}')
    if short_id is not None:
        return short_id
    url_num = self.redis.incr('last-url-id')
    short_id = base36_encode(url_num)
    self.redis.set(f'url-target:{short_id}', url)
    self.redis.set(f'reverse-url:{url}', short_id)
    return short_id

reverse-url: + URL-адрес будет хранить короткий идентификатор. Если URL-адрес уже был отправлен, это не будет None, и мы можем просто вернуть это значение, которое будет коротким идентификатором. В противном случае мы увеличиваем ключ last-url-id и преобразуем его в базу-36. Затем мы сохраняем ссылку и обратную запись в redis. И вот функция преобразования в базу-36:

def base36_encode(number):
    assert number >= 0, 'positive integer required'
    if number == 0:
        return '0'
    base36 = []
    while number != 0:
        number, i = divmod(number, 36)
        base36.append('0123456789abcdefghijklmnopqrstuvwxyz'[i])
    return ''.join(reversed(base36))

Итак, для работы этого вида отсутствует шаблон. Мы создадим его позже, сначала напишем другие виды, а затем создадим шаблоны все вместе.

Шаг 6: Вид перенаправления

Вид перенаправления прост. Всё, что он должен сделать, — это найти ссылку в redis и перенаправить на неё. Кроме того, мы также увеличим счётчик, чтобы знать, сколько раз ссылка была нажата:

def on_follow_short_link(self, request, short_id):
    link_target = self.redis.get(f'url-target:{short_id}')
    if link_target is None:
        raise NotFound()
    self.redis.incr(f'click-count:{short_id}')
    return redirect(link_target)

В этом случае мы вручную выбросим исключение NotFound, если URL-адрес не существует, что приведет к функции dispatch_request и будет преобразовано в стандартный ответ 404.

Шаг 7: Вид детали

Вид детали ссылки очень похож, мы просто снова отображаем шаблон. В дополнение к поиску целевого объекта, мы также запрашиваем в Redis количество кликов по ссылке и устанавливаем значение по умолчанию 0, если такой ключ ещё не существует:

def on_short_link_details(self, request, short_id):
    link_target = self.redis.get(f'url-target:{short_id}')
    if link_target is None:
        raise NotFound()
    click_count = int(self.redis.get(f'click-count:{short_id}') or 0)
    return self.render_template('short_link_details.html',
        link_target=link_target,
        short_id=short_id,
        click_count=click_count
    )

Обратите внимание, что Redis всегда работает со строками, поэтому вам необходимо вручную преобразовать счётчик кликов в int.

Шаг 8: Шаблоны

И вот все шаблоны. Просто поместите их в папку templates. Jinja2 поддерживает наследование шаблонов, поэтому первым делом мы создадим шаблон макета с блоками, которые выступают в качестве заполнителей. Мы также настроили Jinja2 так, что он автоматически экранирует строки с помощью правил HTML, чтобы нам не приходилось тратить время на это самостоятельно. Это предотвращает атаки XSS и ошибки отображения.

layout.html:

<!doctype html>
<title>{% block title %}{% endblock %} | shortly</title>
<link rel=stylesheet href=/static/style.css type=text/css>
<div class=box>
  <h1><a href=/>shortly</a></h1>
  <p class=tagline>Shortly is a URL shortener written with Werkzeug
  {% block body %}{% endblock %}
</div>

new_url.html:

{% extends "layout.html" %}
{% block title %}Create New Short URL{% endblock %}
{% block body %}
  <h2>Submit URL</h2>
  <form action="" method=post>
    {% if error %}
      <p class=error><strong>Error:</strong> {{ error }}
    {% endif %}
    <p>URL:
      <input type=text name=url value="{{ url }}" class=urlinput>
      <input type=submit value="Shorten">
  </form>
{% endblock %}

short_link_details.html:

{% extends "layout.html" %}
{% block title %}Details about /{{ short_id }}{% endblock %}
{% block body %}
  <h2><a href="/{{ short_id }}">/{{ short_id }}</a></h2>
  <dl>
    <dt>Full link
    <dd class=link><div>{{ link_target }}</div>
    <dt>Click count:
    <dd>{{ click_count }}
  </dl>
{% endblock %}

Шаг 9: Стиль

Чтобы это выглядело лучше, чем уродливый черно-белый вариант, вот простой стильный файл:

static/style.css:

body        { background: #E8EFF0; margin: 0; padding: 0; }
body, input { font-family: 'Helvetica Neue', Arial,
              sans-serif; font-weight: 300; font-size: 18px; }
.box        { width: 500px; margin: 60px auto; padding: 20px;
              background: white; box-shadow: 0 1px 4px #BED1D4;
              border-radius: 2px; }
a           { color: #11557C; }
h1, h2      { margin: 0; color: #11557C; }
h1 a        { text-decoration: none; }
h2          { font-weight: normal; font-size: 24px; }
.tagline    { color: #888; font-style: italic; margin: 0 0 20px 0; }
.link div   { overflow: auto; font-size: 0.8em; white-space: pre;
              padding: 4px 10px; margin: 5px 0; background: #E5EAF1; }
dt          { font-weight: normal; }
.error      { background: #E8EFF0; padding: 3px 8px; color: #11557C;
              font-size: 0.9em; border-radius: 2px; }
.urlinput   { width: 300px; }

Бонус: Усовершенствования

Посмотрите на реализацию в примере словаря в репозитории Werkzeug, чтобы увидеть версию этого учебника с некоторыми незначительными усовершенствованиями, такими как пользовательская страница 404.

  • shortly в папке примеров

© 2007–2022 Pallets
Licensed under the BSD 3-clause License.
https://werkzeug.palletsprojects.com/en/2.1.x/tutorial/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API