Spec-Zone.ru › Werkzeug 0.15

Руководство по 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!']

Приложению 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 = 'Hello %s!' % 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
import urlparse
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.wsgi 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'])

    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 для применения посредников, как мы делаем в функции create_app. Фактический метод wsgi_app затем создаёт объект Request и вызывает метод dispatch_request, который должен вернуть объект Response, который затем будет обработан как приложение WSGI. Как видите: черепахи до конца. Как создаваемый нами класс Shortly, так и любой объект запроса в Werkzeug реализуют интерфейс WSGI. В результате вы можете даже вернуть другое приложение WSGI из метода dispatch_request.

Функция-фабрика create_app может использоваться для создания нового экземпляра нашего приложения. Она не только передаст некоторые параметры в качестве конфигурации приложению, но также необязательно добавит 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-адреса. И ещё одно, которое следует за короткой ссылкой на целевой URL-адрес, а также другое с тем же правилом, но с плюсом (+) в конце, чтобы показать детали ссылки.

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

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

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

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

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

Если всё работает нормально, мы вызываем функцию on_ + endpoint и передаём в неё запрос в качестве аргумента, а также все аргументы 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('/%s+' % short_id)
    return self.render_template('new_url.html', error=error, url=url)

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

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

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

def insert_url(self, url):
    short_id = self.redis.get('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('url-target:' + short_id, url)
    self.redis.set('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('url-target:' + short_id)
    if link_target is None:
        raise NotFound()
    self.redis.incr('click-count:' + short_id)
    return redirect(link_target)

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

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

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

def on_short_link_details(self, request, short_id):
    link_target = self.redis.get('url-target:' + short_id)
    if link_target is None:
        raise NotFound()
    click_count = int(self.redis.get('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–2020 Pallets
Licensed under the BSD 3-clause License.
https://werkzeug.palletsprojects.com/en/0.15.x/tutorial/

Spec-Zone.ru

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