server usage

Перевод с дополнениями официальной документации aiohttp http server usage

Для Python3.5+

HTTP сервер

Создание и запуск простого веб-сервера HelloWorld

Импортируем нужные нам библиотеки [1].
Создаем обработчик запроса [2].
После чего нужно создать инстанс Application (приложение) [3] и зарегистрировать обработчики в маршрутизаторе (router) приложения указывая HTTP метод [4] или '*' для применения маршрута на все HTTP методы [5], путь и обработчик. Далее создаем сервер и запускаем его в цикле asyncio [6]: Это все!
import asyncio # [1]
import aiohttp

async def hello(request): # [2]
    return aiohttp.web.Response(body=b"HelloWorld")
async def hello(request): # [2]
    return aiohttp.web.Response(body=b"HelloWorld all methods")
app = aiohttp.web.Application() # [3]
app.router.add_route('GET', '/', hello) # [4]
app.router.add_route('*', '/all', hello_all) # [5]
loop = asyncio.get_event_loop() # [6]
handler = app.make_handler()
f = loop.create_server(handler, 'localhost', 8080)
srv = loop.run_until_complete(f)
print('serving on', srv.sockets[0].getsockname())
try:
    loop.run_forever()
except KeyboardInterrupt:
    print("serving off...")
finally:
    loop.run_until_complete(handler.finish_connections(1.0))
    srv.close()
    loop.run_until_complete(srv.wait_closed())
    loop.run_until_complete(app.finish())
loop.close()
print("goodbye")

Обработчик

Обработчиком может служить любой вызываемый объект, который принимает один аргумент типа Request и возвращает инстанс StreamResponse или дочернего класса, например, Response. Пример использования лямбда-функций в маршрутизаторе:
app.router.add_route(
    'GET', '/date',
    lambda _: web.Response(text=str(datetime.datetime.now())))
Если Вы хотите использовать классы, например для объединения функций в одну логическую группу, не проблема:
class Handler:

    def __init__(self):
        pass

    def handle_intro(self, request):
        return web.Response(body=b"Hello, world")

    async def handle_greeting(self, request):
        name = request.match_info.get('name', "Anonymous")
        txt = "Hello, {}".format(name)
        return web.Response(text=txt)

handler = Handler()
app.router.add_route('GET', '/intro', handler.handle_intro)
app.router.add_route('GET', '/greet/{name}', handler.handle_greeting)

Переменный (динамический) маршрут

Вы можете использовать переменные маршруты. Чтобы указать переменную в маршруте нужно в фигурных скобках указать название переменной ({var_name}). Например маршрут '/a/{name}/c' удовлетворяет пути '/a/любые символы кроме фигурных скобок и слеша/c' то есть будет применено регулярное выражения [^{}/]+. Чтобы применить свое регулярное выражение нужно использовать запись вида: {var_name: regular_expression}. Например, чтобы принимать только цифры можно указать следующий маршрут:
app.router.add_route('GET', r'/{var_name:\d+}', variable_handler)
Все значения переменных хранятся в словаре request.match_info и получить значение нужной переменной можно так: request.match_info['var_name']. Пример сервера который отдает количество дней в феврале переданного в урле года:
import calendar
. . .
app.router.add_route(
    'GET', '/{year:\d+}',
    lambda request: web.Response(
        text=str(calendar.monthrange(int(request.match_info['year']), 2)[1])))
. . .

Именованные маршруты и конструирование обратных урлов

Маршрут может иметь имя:
app.router.add_route('GET', '/root', handler, name='root')
В обработчике Вы можете построить урл для даного маршрута используя его имя:
>>> request.app.router['root'].url(query={"a": "b", "c": "d"})
'/root?a=b&c=d'
app.router.add_route('GET', r'/{user}/info',
                     variable_handler, name='handler')
>>> request.app.router['handler'].url(
...     parts={'user': 'john_doe'},
...     query="?a=b")
'/john_doe/info?a=b'

Просмотр маршрутов

Для просмотра всех маршрутов Вы можете использовать метод UrlDispatcher.routes(), который возвращает итерируемый RoutesView объект:
>>> print(app.router.routes(), sep="\n")
  at 0xb6979a04>
 
>>> print(len(app.router.routes()))
2

Пользовательские критерии маршрутизации

Иногда Вы должны выбирать веб-обработчик учитывая не только HTTP метод и путь. UrlDispatcher не дает возможности использовать другие условия, но мы можем сделать второй слой маршрутизации. Следующий пример показывает маршрутизацию на основе HTTP Accept заголовка:
class AcceptChooser:

    def __init__(self):
        self._accepts = {}

    async def do_route(self, request):
        for accept in request.headers.getall('ACCEPT', []):
            acceptor = self._accepts.get(accept)
            if acceptor is not None:
                return (await acceptor(request))
        raise HTTPNotAcceptable()

    def reg_acceptor(self, accept, handler):
        self._accepts[accept] = handler


async def handle_json(request):
    # do json handling

async def handle_xml(request):
    # do xml handling

chooser = AcceptChooser()
app.router.add_route('GET', '/', chooser.do_route)

chooser.reg_acceptor('application/json', handle_json)
chooser.reg_acceptor('application/xml', handle_xml)

Рендеринг шаблонов

aiohttp.web с коробки не поддерживает рендеринг шаблонов, но Вы можете использовать aiohttp_jinja2 библиотеку, которая, как видно с названия, позволяет использовать jinja2 [http://jinja.pocoo.org/] в aiohttp. Оф документация, перевод ЖДИТЕ. Если Вы предпочитаете использовать Mako [http://www.makotemplates.org/], то Вы можете использовать библиотеку aiohttp_mako.

Сессии пользователей

aiohttp.web не поддерживает сессии, но Вы можете использовать стороннею библиотеку aiohttp_session. Оф документация, перевод ЖДИТЕ Пример:
import asyncio
import time
from aiohttp import web
from aiohttp_session import get_session, session_middleware
from aiohttp_session.cookie_storage import EncryptedCookieStorage

async def handler(request):
    session = await get_session(request)
    session['last_visit'] = time.time()
    return web.Response(body=b'OK')

async def init(loop):
    app = web.Application(middlewares=[session_middleware(
        EncryptedCookieStorage(b'Sixteen byte key'))])
    app.router.add_route('GET', '/', handler)
    srv = await loop.create_server(
        app.make_handler(), '0.0.0.0', 8080)
    return srv

loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

Поддержка Expect заголовка

aiohttp.web поддерживает Expect заголовки. Чтобы указать обработчик для этого заголовка нужно указать его в параметре expect_handler маршрута, если не указан обработчик юудет возвращено HTTP/1.1 100 Continue. Этот обработчик будет вызван после получения всех заголовков и перед вызовом промежуточных слоев и обработчика маршрута. Он может вернуть None если обработку запроса нужно продолжить как в ничем не бывало. Если обработчик возвратит экземпляр класса  StreamResponse, то RequestHandler будет использовать его в качестве ответа, а если все проверки прошли и можно отправлять подтверждение на прием, то обработчик должен вернуть HTTP/1.1 100 Continue:

async def check_auth(request):
    if request.version != aiohttp.HttpVersion11:
        return
    if request.headers.get('AUTHORIZATION') is None:
        return web.HTTPForbidden()
    request.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
async def hello(request):
    return web.Response(body=b"Hello, world")
app = web.Application()
app.router.add_route('GET', '/', hello, expect_handler=check_auth)

Загрузка файлов

Для загрузки файла нужно чтобы у Вас была форма для получения фалов с значением enctype в "multipart/form-data". Пример формы для загрузки mp3 фйлов:
<form action="/store_mp3" method="post" accept-charset="utf-8"
      enctype="multipart/form-data">
    <label for="mp3">Mp3</label>
    <input id="mp3" name="mp3" type="file" value="" />
    <input type="submit" value="submit" />
</form>
Далее нужно создать обработчик запроса по урлу что мы указали в форме ('/store_mp3'). Объект файла (FileField) находится в объекте запроса который мы можем извлечь с Request.post() сопрограммы. Пример простого хранилища:
import asyncio
import os
from aiohttp import web

basedir = os.path.dirname(os.path.realpath(__file__))
dir_files = os.path.join(basedir, 'files/')

async def files(request):
    return web.Response(
        text='''<html><body>
            Список:<ul><li>%s</li></ul>
            <form action="/store_file" method="post" accept-charset="utf-8"
                  enctype="multipart/form-data">

                <label for="file">Добавить</label>
                <input id="file" name="file" type="file" value="" />

                <input type="submit" value="добавить" />
            </form>
            </body></html>''' % '</li><li>'.join(os.listdir(dir_files)),
        content_type="text/html")

async def file_store(request):
    data = await request.post()
    if data['file'] == b'':
        return web.HTTPFound('/files')
    filename = data['file'].filename
    input_file = data['file'].file
    with open(os.path.join(dir_files, filename), 'wb') as f:
        f.write(input_file.read())
    return web.Response(
        text='Файл добавлен. Чтобы посмотреть список перейдите по '
             '<a href="%s">ссылке</a>' % request.app.router['files'].url(),
        content_type="text/html")

app = web.Application()
app.router.add_route('GET', '/files', files, name="files")
app.router.add_route('POST', '/store_file', file_store)

loop = asyncio.get_event_loop()
handler = app.make_handler()
f = loop.create_server(handler, 'localhost', 8080)
srv = loop.run_until_complete(f)
print('serving on', srv.sockets[0].getsockname())
try:
    loop.run_forever()
except KeyboardInterrupt:
    print("serving off...")
finally:
    loop.run_until_complete(handler.finish_connections(1.0))
    srv.close()
    loop.run_until_complete(srv.wait_closed())
    loop.run_until_complete(app.finish())
loop.close()
print("goodbye")

Вебсокеты

aiohttp.web работает с вебсокетами с коробки. Вы должны создать объект WebSocketResponse в обработчике и общаться с пиром используя методы этого объекта. Пример простого эхо-сервера:

async def websocket_handler(request):

    ws = web.WebSocketResponse()
    await ws.prepare(request)

    async for msg in ws:
        if msg.tp == aiohttp.MsgType.text:
            if msg.data == 'close':
                await ws.close()
            else:
                ws.send_str(msg.data)
        elif msg.tp == aiohttp.MsgType.error:
            print('ws connection closed with exception %s' %
                  ws.exception())

    print('websocket connection closed')

    return ws
Как для чтения (например await ws.receive() или async for msg in ws:) так и для записи Вы должны использовать только одну задачу, но асинхронная отправка данных (например ws.send_str('data')) может быть в нескольких задачах. Для реализации SockJS-совместимого кода нужно использовать библиотеку aiohttp-based.

Исключения

aiohttp.web описывает исключения со списка кодов состояния HTTP. Вы можете или вернуть объект исключения из обработчика, или вызвать его. Следующие фрагменты одинаковы:
async def handler(request):
    return aiohttp.web.HTTPFound('/redirect')
и:
async def handler(request):
    raise aiohttp.web.HTTPFound('/redirect')
Каждый класс включает в себя код состояния в соответствии с RFC 2068: коды 100-300 это не ошибки, 400-ые - ошибки клиента, а 500-е — сервера. Иерархия HTTP исключений:
Exception
  HTTPException
    HTTPSuccessful
      * 200 - HTTPOk
      * 201 - HTTPCreated
      * 202 - HTTPAccepted
      * 203 - HTTPNonAuthoritativeInformation
      * 204 - HTTPNoContent
      * 205 - HTTPResetContent
      * 206 - HTTPPartialContent
    HTTPRedirection
      * 300 - HTTPMultipleChoices
      * 301 - HTTPMovedPermanently
      * 302 - HTTPFound
      * 303 - HTTPSeeOther
      * 304 - HTTPNotModified
      * 305 - HTTPUseProxy
      * 307 - HTTPTemporaryRedirect
    HTTPError
      HTTPClientError
        * 400 - HTTPBadRequest
        * 401 - HTTPUnauthorized
        * 402 - HTTPPaymentRequired
        * 403 - HTTPForbidden
        * 404 - HTTPNotFound
        * 405 - HTTPMethodNotAllowed
        * 406 - HTTPNotAcceptable
        * 407 - HTTPProxyAuthenticationRequired
        * 408 - HTTPRequestTimeout
        * 409 - HTTPConflict
        * 410 - HTTPGone
        * 411 - HTTPLengthRequired
        * 412 - HTTPPreconditionFailed
        * 413 - HTTPRequestEntityTooLarge
        * 414 - HTTPRequestURITooLong
        * 415 - HTTPUnsupportedMediaType
        * 416 - HTTPRequestRangeNotSatisfiable
        * 417 - HTTPExpectationFailed
      HTTPServerError
        * 500 - HTTPInternalServerError
        * 501 - HTTPNotImplemented
        * 502 - HTTPBadGateway
        * 503 - HTTPServiceUnavailable
        * 504 - HTTPGatewayTimeout
        * 505 - HTTPVersionNotSupported
Все HTTP исключения, кроме тех что будут описаны далее, имеют одинаковую конструкцию:
HTTPNotFound(*, headers=None, reason=None,
             body=None, text=None, content_type=None)
Заголовки что прописаны в параметре headers будут добавлены в заголовки ответа.
Классы HTTPMultipleChoices, HTTPMovedPermanently, HTTPFound, HTTPSeeOther, HTTPUseProxy, HTTPTemporaryRedirect имеют следующую конструкцию:
HTTPFound(location, *, headers=None, reason=None,
          body=None, text=None, content_type=None)
где location это URI по которому клиенту следует перейти или URI созданного ресурса. Пример редиректа можно посмотреть в предыдущем примере по загрузке файлов:
return web.HTTPFound('/files')
В HTTPMethodNotAllowed (метод не поддерживается) нужно передавать первым аргументом метод который был запрошен, а во втором список поддерживаемых методов:
HTTPMethodNotAllowed(method, allowed_methods, *,
                     headers=None, reason=None,
                     body=None, text=None, content_type=None)

Доступ к данным

При использовании aiohttp нельзя использовать глобальные переменные (технически можно но на оф документации написано что не нужно, да и вообще не нужно использовать глобальные переменные). aiohttp.web.Application и aiohttp.web.Request поддерживают интерфейс collections.abc.MutableMapping (т. е. они объекты подобны словарю — dict-подобные), что позволяет их использовать в качестве хранилища данных. Чтобы хранить глобальные переменные можно использовать экземпляр Application:
app['global_var'] = 3.5
app['my_private_key'] = data
и далее без проблем можно извлечь их в обработчике:
async def handler(request):
    data = request.app['my_private_key'
Также Вы можете хранить данные в Request, но при этом эти данные будут жить пока живет запрос:
async def handler(request):
  request['my_private_key'] = "data"
В основном, это полезно для промежуточного ПО (middlewares) и сигналов обработчиков, для хранения данных и дальнейшей их обработки в цепочке обработчиков. Чтобы избежать конфликта данных с другими пользователями и другими сторонними библиотеками используйте уникальные ключи основывая их на уникальных данных, например, название компании, УРЛ и т. п.

Промежуточные слои (Middlewares)

Application имеет необязательный параметр middlewares, в который можно передать последовательность промежуточных слоев:
app = web.Application(middlewares=[middleware_factory_1,
                                   middleware_factory_2])
Самый простой завод промежуточного слоя:
async def middleware_factory(app, handler):
    async def middleware(request):
        return await handler(request)
    return middleware
Каждый завод является сопрограммой, который принимает два параметра: app - экземпляр приложения и handler — следующий обработчик в цепочке промежуточных слоев. Последнему обработчику будет передан он сам (вызван resolve()). Промежуточный слой должен возвращать новую сопрограмму обернутую в значение переданное в параметре handler. Функция middleware (с примера) аналогична веб-обработчику: должна принимать один параметр запроса и возвращать ответ (response) или поднимать исключение. Завод является сопрограммой поэтому он может делать вызовы await на создание нового обработчика, например вызов бд и т. п.

Сигналы

Помимо промежуточных слоев мы также можем использовать механизм сигналов. Например, промежуточные слои могут изменять только HTTP заголовки неподготовленного ответа (смотрите метод prepare()), но если нам нужно изменить заголовки для потоковых ответов и вебсокетов, то тогда мы можем воспользоваться методом on_response_prepare:
async def on_prepare(request, response):
    response.headers['My-Header'] = 'value'
app.on_response_prepare.append(on_prepare)
Обработчики сигналов не должны ничего возвращать, но они могут изменять параметры входящих объектов.

Поддержка CORS

aiohttp.web не поддерживает Cross-Origin Resource Sharing, но есть плагин для этого: aiohttp_cors.

Панель инструментов отладки

aiohttp_debugtoolbar очень полезная библиотека, которая предоставляет панель инструментов для отладки вашего aiohttp.web приложения. Установка используя pip:
$ pip install aiohttp_debugtoolbar
Чтобы прикрутить его к вашему приложению нужно добавить его в промежуточные слои и вызвать aiohttp_debugtoolbar.setup:
import aiohttp_debugtoolbar
from aiohttp_debugtoolbar import toolbar_middleware_factory

app = web.Application(loop=loop,
                      middlewares=[toolbar_middleware_factory])
aiohttp_debugtoolbar.setup(app)
debugtoolbar готов к использованию. Наслаждайтесь!!!

Комментариев нет:

Отправить комментарий