Написание framework на asyncio, aiohttp и мысли про Python3 часть первая

  • Tutorial

Года полтора назад встал вопрос совместимости написанного кода с Python3. Поскольку уже стало более менее очевидно, что развивается только Python3 и, рано или поздно, все библиотеки будут портированы под него. И во всех дистрибутивах по умолчанию будет тройка. Но постепенно, по мере изучения, что нового появилось в последних версиях Python мне все больше стал нравится Asyncio и, скорее, даже не Acyncio а написанный для работы с ним aiohttp. И, спустя какое то время, появилась небольшая обертка вокруг aiohttp в стиле like django. Кому интересно что из этого получилось прошу под кат.


Вторая часть


Введение
Краткий обзор других фреймворков на базе aiohttp
1. Структура
2. aiohttp и jinja2
3. aiohttp и роуты
4. Статика и GET, POST параметры, редиректы
5. Websocket
6. asyncio и mongodb, aiohttp, session, middleware
7. aiohttp, supervisor, nginx, gunicorn
8. После установки, о примерах.
9.RoadMap


Введение


На тот момент уже были готовы для Python3 практически все часто используемые в проектах библиотеки.
Почивший PIL был прекрасно заменен на Pillow, tweppy на twython, python-openid на python3-openid и т.д. Jinja2, xlrt, xlwt и прочие уже были с поддержкой Python3.


Грубо говоря, все, что надо было реализовать, это чтобы система отдавала данные в виде bytes:


def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return bytes("Hello World", 'utf-8')

Немного, с переименованием библиотек помучаться:


py   = sys.version_info
py3k = py >= (3, 0, 0)
if py3k:
    unicode = str
    import io as StringIO
    import builtins as __builtin__
else:   
    import StringIO

Ну и, естественно, по мере изучения что нового появилось, Python3 не мог не привлечь внимание Asyncio. Встроенный в python3 асинхронный движок. Появившийся, правда, только в 3.4 версии. И только с версии 3.5, которая вышла на днях, у него появился достаточно удачный синтаксический сахар, об этом чуть ниже.


Первое время, конечно, на нем что-то писать было дико неудобно и, насколько я понял, все по прежнему пользовались tornado, gevent, twisted или оберткой вокруг того же asynio и twistedautobuh. Достаточно неплохим продуктом. Но время шло и один из разработчиков asyncio svetlov создал достаточно быстро развивающийся асинхронный фреймворк aiohttp. Aiohttp упрощает разработку с помощью asyncio примерно до уровня flask или bottle.


Но с довольно легко подключаемыми websocket-ами и при желании позволяющий выполнять большинство операций асинхронно, и, на мой взгляд, с довольно небольшой ценой за это, особенно с оглядкой на python3.5.
Примерно это выглядит так:


#python3.4
@asyncio.coroutine
def read_data():
      data = yield from db.fetch('SELECT . . . ')

#python3.5
async def read_data():
      data = await db.fetch('SELECT ...')

Поскольку до сих пор для написания чатов, игрушек, конференций с webrtc, где есть websoket-ы мне приходилось пользоваться либо gevent либо autobah либо в некоторых случаях node.js, взвесив все за и против очень захотелось переписать свои библиотеки на aiohttp, который за последний год успел обрасти своей эко-системой, и рядом удобных возможностей. И так появилась эта публикация.


Надо еще добавить что в aiohttp вполне можно писать и синхронно, выполнять блокирующие операции, хотя это и не совсем правильно.


Дальше будет описана работа с aiohttp и создание небольшого фреймворка в стиле like django, с похожей структурой и возможностями.


Естественно от версии 0.1 ожидать каких то батареек не приходится, но думаю, что в следующей версии, уже можно будет увидеть много положительных сдвигов.


Краткий обзор других фреймворков на базе asyncio и aiohttp


Тут хочется привести очень краткий обзор, чтоб было общее представление о состоянии дел на данный момент с написанием асинхронных библиотек, упрощающих жизнь разработчиков, в Python3.
Все ниже перечисленные фреймворки можно поделить на две категории — те, которые по зависимостям тянут aiohttp и базируются на нем, и те, которые работают без него, только с asyncio.


Pulsarframework использующий asyncio и multiprocessing. Интегрируется с django, hello world на нем выглядит как обычный wsgi. На github есть достаточно много примеров использования, например чатов, автор, насколько я понял, любит angular.js


Pulsar-hello world
from pulsar.apps import wsgi

def hello(environ, start_response):
    data = b'Hello World!\n'
    response_headers = [ ('Content-type','text/plain'),  ('Content-Length', str(len(data)))  ]
    start_response('200 OK', response_headers)
    return [data]

if __name__ == '__main__':
    wsgi.WSGIServer(callable=hello).start()

Mufinframework базирующийся на aiohttp. У него есть некоторое количество плагинов, насколько я понял, написанных, по возможности, асинхронно. Также, имеется развернутое на Heroku тестовое приложение в виде чата.


Mufin - hello world
import muffin

app = muffin.Application('example')

@app.register('/', '/hello/{name}')
def hello(request):
    name = request.match_info.get('name', 'anonymous')
    return 'Hello %s!' % name

introduction — еще один базирующийся на aiohttp framework


Пример introduction
from interest import Service, http
class Service(Service):
    @http.get('/')
    def hello(self, request):
        return http.Response(text='Hello World!')

service = Service()
service.listen(host='127.0.0.1', port=9000, override=True, forever=True)

Spanner.py — позиционируется как микро web-framework написанный на python для людей :), автора вдохновляли Flask и express.js. Использует только asyncio. Выглядит действительно довольно лаконичным.


Пример
from webspanner import Spanner
app = Spanner()

@app.route('/')
def index(req, res):
      res.write("Hello world")

Growlerframework использующий только asyncio, авторы говорят что взяли идеи node.js и express.


Growler hello world
import asyncio
from growler import App
from growler.middleware import (Logger, Static, Renderer)

loop = asyncio.get_event_loop()
app = App('GrowlerServer', loop=loop)

# Добавление нескольких middleware приложений
app.use(Logger())
app.use(Static(path='public'))

@app.get('/')
def index(req, res):
    res.render("home")

Server = app.create_server(host='127.0.0.1', port=8000)
loop.run_forever()

astrid — Простой flask подобный framework основанный на aiohttp.


Пример
import os
from astrid import Astrid
from astrid.http import render, response

@app.route('/')
def index_handler(request):
    return response("Hello")

app.run()

1. Структура


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


Если нужно python 3.5 устанавливается так:


sudo add-apt-repository ppa:fkrull/deadsnakes
sudo apt-get update
sudo apt-get install python3.5 python3.5-dev

Библиотека — устанавливается через pip3 install:


apps->
      app->
            static
            templ
            view.py
            routes.py
     app1-> ...
     app2-> ...
core->
      core.py
      union.py
      utils.py 

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


apps->
      app->
            static
            templ
            view.py
            routes.py
      app1-> ...
      app2-> ...
static
templ
view.py
route.py
settings.py

Содержание файликов со списком роутов мы хотим видеть примерно таким:


from core.union import route

route('GET' ,   '/',         page   )
route( 'GET' ,  '/db',     test_db  )

А view где эти роуты обрабатываются такого типа:


async def page(request):
    return templ('index', request, {'key':'val'})

То есть, все выглядит довольно просто и достаточно удобно, кроме необязательной необходимости каждый раз писать вызов корутины @asyncio.coroutine или async def.


2. aiohttp, jinja2 и отладчик


Для aiohttp есть специально для него написанный дебагер и асинхронная обертка для jinja2. Их мы и будем использовать.


pip3 install aiohttp_jinja2

Простое подключение jinja2 выглядит примерно так:
import asyncio, jinja2, aiohttp_jinja2 
from aiohttp import web       

async def page(req):
    return aiohttp_jinja2.render_template('index.tpl', req,{'k':'v'})

async def init(loop):
    app = web.Application(loop=loop)
    aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('./'))
    app.router.add_route('GET', '/', page)
    srv = await loop.create_server(app.make_handler(), '127.0.0.1', 80)
    return srv

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

Но нам надо вызывать шаблоны с разных мест и желательно максимально просто, например:


return templ('index', request, {'key':'val'})

И для самого шаблона нужно как-то сокращенно указывать путь. Мест, где могут хранится шаблоны, может быть несколько штук:


  1. Шаблоны лежащие в папке `templ` в корне самого проекта.
  2. Шаблоны которые лежат в модулях проектов или модулях библиотеки.

Поэтому условно договоримся, что если шаблоны лежат в корне какого-либо проекта, то будет просто указываться название шаблона, например 'template'. А шаблоны из модулей будут выглядеть примерно так:


return templ("apps.app:template", request, {'key':'val'})

где app компонента а template шаблона.


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


aiohttp_jinja2.setup(app, loader=jinja2.FunctionLoader ( load_templ ) )

Общий листинг функций которые собирают шаблоны:
def get_path(app):
    if type(app) == str:
        __import__(app)
        app = sys.modules[app] 
    return os.path.dirname(os.path.abspath(app.__file__))

def get_templ_path(path):
    module_name = ''; module_path = ''; file_name = ''; name_templ = 'default'; 
    if ':' in path:
        module_name, file_name = path.split(":", 1) # app.table main
        module_path = os.path.join( get_path( module_name), "templ")
    else:
        module_path = os.path.join( os.getcwd(), 'templ', name_templ)
    return module_name, module_path, file_name+'.tpl'

def render_templ(t, request, p):
    # если хотим написать параметры через = то p = dict(**p)
    return aiohttp_jinja2.render_template( t, request, p )

def load_templ(t, **p):
    (module_name, module_path, file_name) = get_templ_path(t)
    def load_template (module_path, file_name):
        path = os.path.join(module_path, file_name)
        template = ''
        filename = path if os.path.exists ( path ) else False
        if filename:
            with open(filename, "rb") as f:
                template = f.read()
        return template
    template = load_template( module_path, file_name)
    if not template: return 'Template not found {}' .format(t)
    return template.decode('UTF-8')

Тут хотелось бы остановится на последовательности действий:
1) Мы парсим наш путь к шаблону например 'apps.app:index', просто проверяем, что если в пути есть двоеточие, то значит шаблоны берутся не из корня проекта, и тогда вызываем функцию для поиска путей из импортов:


def get_path(app):
    if type(app) == str:
                 # импортирует модуль по имени. Например имя будет "news".
        __import__(app)
                # по имени "news" мы получаем сам модуль news и присваиваем его переменной app
        app = sys.modules[app] 
                # получаем путь к нашему модулю
            return os.path.dirname(os.path.abspath(app.__file__)) 

2) Зная пути и имя шаблона, читаем его с диска (замечу что asyncio не поддерживает асинхронные операции чтения с диска):


filename = path if os.path.exists ( path ) else False
if filename:
    with open(filename, "rb") as f:
        template = f.read()

Тут хотелось бы заметить один момент, часто в примерах к подключению jinja2 в том числе в aiohttp_jinja2 рекомендуется для инициализации применять FileSystemLoader просто передавая ему путь, или список путей, например:
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader('/templ/'))
А в нашем случае мы использовали FunctionLoader:
aiohttp_jinja2.setup(app, loader=jinja2.FunctionLoader ( load_templ ) )
Это связано с тем что мы хотим хранить шаблоны в разных директориях, для разных модулей, и не беспокоиться об одинаковых названиях. А в случае с FunctionLoader мы идём только по необходимым путям. В результате у нас модули имеют независимые пространства имён.


Для того, чтобы писать в вызове шаблона сокращенно templ напишем маленькую обертку и присвоим её builtins.templ, после чего сможем вызывать из любого места templ, не делая его импорт постоянно:


def render_templ(t, request, p):
    return aiohttp_jinja2.render_template( t, request, p )

builtins.templ = render_templ

aiohttp_debugtoolbar


aiohttp_debugtoolbar — подключается довольно легко, там где мы инициализируем наш app:


app = web.Application(loop=loop, middlewares=[ aiohttp_debugtoolbar.middleware ])
aiohttp_debugtoolbar.setup(app)

Подключается он через очень middleware, как написать свой будем говорить немного ниже.


Сам aiohttp_debugtoolbar у меня вызвал приятное впечатление, и все необходимое в нем присутствует, немного скриншотов:



Больше в спойлере




3. aiohttp и роуты


В aiohttp роуты выглядят достаточно просто, пример из документации с получением динамического параметра из адреса:


@asyncio.coroutine
def variable_handler(request):
    return web.Response( text="Hello, {}".format(request.match_info['name']))

app = web.Application()
app.router.add_route('GET', '/{name}', variable_handler)

Но поскольку у нас модульная система, нам необходимо вызывать роуты в каждом модуле свои, в файлике routes.py. И желательно упростить это максимально, например:


from core import route

route('GET',    '/',            page,       'page'    )
route('GET',    '/db',      test_db,          'test_db'       )

Тут придется воспользоватся глобальной переменной, хоть это не очень кошерно. Функция route имеет простой вид:


def route(t, r, func, name=None):
    routes.append((t, r, func, name))

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


    for res in routes:
        name = res[3]
        if name is None: name = '{}:{}'.format(res[0], res[2])
        app.router.add_route( res[0], res[1], res[2], name=name)

Естественно перед прохождением по всем роутам нам нужно инициализировать пути где находятся файлы routes.py. Мы это делаем с помощью функции, которая в упрощенном виде выглядит примерно так:


def union_routes( dir=settings.root ):
    name_app = dir.split(os.path.sep)
    name_app = name_app[len(name_app) - 1]
    for name in os.listdir(dir):
        path = os.path.join(dir, name)
                if os.path.isdir ( path ) and os.path.isfile ( os.path.join( path, 'routes.py' )):
            name = name_app+'.'+path[len(dir)+1:]+'.routes'
            builtins.__import__(name, globals=globals())

4. Отдача статики


Конечно по нормальному статику лучше отдавать с помощью nginx но наш фреймворк тоже должен уметь отдавать статику.
В aiohttp уже была функция отдачи статики но она была замечена чуть позже чем надо и уже была написана своя функция.
Распознавать статически файлы будем по роуту /static/path. Те файлы, которые расположены в корне проекта будут распознаваться по пути /static/static/file_name, а файлы в компонентах /static/modul_name/file_name.


Естественно, что все статические файлы будут лежать в папках /static любого модуля или проекта, и могут иметь любое количество вложенностей, скажем /static/img/big_img/.


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


app.router.add_route('GET', '/static/{component:[^/]+}/{fname:.+}', union_stat) 

Дальше в функции union_stat мы просто разбираем параметры роута {component:[^/]+}/{fname:.+} которые получили:


component = request.match_info.get('component', "st")
fname = request.match_info.get('fname', "st")

И формируем соотвествующие пути.


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


mimetype, encoding = mimetypes.guess_type(filename)
if mimetype: headers['Content-Type']       = mimetype
if encoding: headers['Content-Encoding'] = encoding

И читаем сам файл с диска.
В конце мы возвращаем заголовки и сам файл:


return web.Response( body=content, headers=MultiDict( headers ) )

Целиком функции выглядят так:
def union_stat(request, *args):
    component = request.match_info.get('component', "Anonymous")
    fname = request.match_info.get('fname', "Anonymous")
    path = os.path.join( settings.root, 'apps', component, 'static', fname ) 
    if component == 'static':
        path = os.path.join( os.getcwd(), 'static') 
    elif not os.path.exists( path ):
        path = os.path.join( os.getcwd(), 'apps', component, 'static' )
    else:
        path = os.path.join( settings.root, 'apps', component, 'static') 

    content, headers = get_static_file(fname, path)
    return web.Response(body=content, headers=MultiDict( headers ) )

def get_static_file( filename, root ):
    import mimetypes, time

    root = os.path.abspath(root) + os.sep
    filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
    headers = {}

    mimetype, encoding = mimetypes.guess_type(filename)
    if mimetype: headers['Content-Type'] = mimetype
    if encoding: headers['Content-Encoding'] = encoding

    stats = os.stat(filename)
    headers['Content-Length'] = stats.st_size
    from core.core import locale_date
    lm = locale_date("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime), 'en_US.UTF-8')
    headers['Last-Modified'] = str(lm)
    headers['Cache-Control'] = 'max-age=604800'
    with open(filename, 'rb') as f:
        content = f.read()
        f.close()
    return content, headers

UPD. Все таки, в последней версии библиотеки, отдача статики была немного переделана. Теперь, статика отдается силами aiohttp, из папки static в корне проекта.


path = os.path.join( os.path.dirname(__file__), 'static')
app.router.add_static('/static/', path, name='static')

В папке /static, должны находится относительные ссылки на все папки со статикой всех модулей, как стандартных самой библиотеки так и тех которые были созданы в проекте. Создаются, относительные ссылки, во время запуска команд, для создания структуры проекта или создания структуры приложения.


utils.py -p name_project
utils.py -a name_app

Пару слов скажу про POST, GET запросы и переадресацию в aiohttp. GET запросы выглядят довольно стандартно


async def get_get(request):
      query = request.GET['query']

async def get_post(request):
      data = await request.post()
      filename = data['mp3'].filename

Редирект по адресу заданyому в роуте 'test' c 302 ответом


async def redirect(request):
      data = await request.post() 
       . . . 
       url = request.app.router['test'].url()
       return web.HTTPFound( url )

Список всех ответов.


5. aiohttp и Websocket


Одна из самых приятных особенностей aiohttp это возможность легко подключать вебсокеты, просто вызвав в роуте функцию которая отвечает за их обработку. Без каких то лишних костылей.
Например, app.router.add_route('GET', '/ws', ws). Если рассматривать роут из нашей небольшой обертки, которую мы только что написали, то это может выглядеть так: route('GET', '/ws', ws )


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


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 + '/answer')
        elif msg.tp == aiohttp.MsgType.error:
            print('ws connection closed  %s' %  ws.exception())
    print('websocket connection closed')
    return ws   

Для примера, то же самое в случае с Node.JS, с использованием модуля ws:


var WebSocketServer = new require('ws');
var clients = {};
var webSocketServer = new WebSocketServer.Server({ port: 8081 });
webSocketServer.on('connection', function(ws) {
       var id = Math.random();
       clients[id] = ws;
       ws.on('message', function(message) {
            for (var key in clients) {
                     clients[key].send(message);
             }
      });
     ws.on('close', function() {
            console.log('Сonnection closed ' + id);
            delete clients[id];
      });
});

6. asyncio и mongodb, aiohttp, session, middleware


У aiohttp есть такой прекрасный инструмент как middleware, в разных случаях под этим термином понимают немного разные вещи, поэтому рассмотрим его на примере создания коннектора к базе.


У таких фреймворков как flask или bootle есть возможность вызвать какую либо функцию перед загрузкой всего остального или после, например, в bootle:


@bottle.hook('after_request')
def enable_cors():
    response.headers['Access-Control-Allow-Origin'] = '*'

В случае с aiohttp, в том числе и для примерно таких случаев, был придуман middleware.
Итак, мы хотим писать запросы к базе максимально просто, request.db:


def test_db(request):
        return templ('apps.app:db_test', request, { 'key': request.db.doc.find_one({"_id":"test"}) })

Для этого мы создадим middleware и инициализируем его в самом начале, это делается довольно просто, пример с уже инициализированным дебагером, сессиями и базой.


app = web.Application(loop=loop, middlewares=[ aiohttp_debugtoolbar.middleware, db_handler(), 
        session_middleware(EncryptedCookieStorage(b'Secret byte key')) ])

Ну и сама фабрика
def db_handler():
    async def factory(app, handler):
        async def middleware(request):
            if request.path.startswith('/static/') or request.path.startswith('/_debugtoolbar'):
                response = await handler(request)
                return response
            # инициализация
            db_inf = settings.database
            kw = {}
            if 'rs' in db_inf: kw['replicaSet'] = db_inf['rs']
            from pymongo import MongoClient
            mongo = MongoClient( db_inf['host'], 27017)
            db = mongo[ db_inf['name'] ]
            db.authenticate('admin', settings.database['pass'] )
            request.db = db
            # процессинг запроса (дальше по цепочки мидлверов и до приложения)
            response = await handler(request)
            mongo.close() 
            # экземеляр рабочего объекта по цепочке вверх до библиотеки
            return response
        return middleware
    return factory

На что хотелось бы обратить внимание, поскольку на каждый запрос к серверу срабатывает middleware, первым делом, мы проверяем что в request не содержится адрес по которому мы получаем статику, а также адрес, по которому вызывается дебагер. Чтобы на каждый запрос не дергать базу.


def middleware(request):
     if request.path.startswith('/static/') or request.path.startswith('/_debugtoolbar'):
```После этого мы конектимся к базе:
```python
mongo = MongoClient( db_inf['host'], 27017)

А в конце закрываем соединение:


mongo.close() 

Все выглядит довольно просто, некоторые полезности от автора aiohttp svetlov инициализируются и созданы подобным образом через middleware.


Ну а дальше подробнее надо остановится на самом драйвере к mongodb. К большому сожалению он пока не асинхронный, правильнее сказать асинхронный драйвер есть есть но он давно заброшен и оставляет желать лучшего, и в нем нет поддержки gridFS, нет нововведений pymongo и тд.


Но все таки прогресс не стоит на месте и разработчик PyMongo и одновременно асинхронного драйвера к MongoDB для Tornado, MotorA. Jesse Jiryu Davis активно работает над интеграцией Asyncio в Motor. И уже обещает этой осенью выпустить версию 0.5 с поддержкой Asyncio.


7. aiohttp, supervisor, nginx, gunicorn


Запустить aiohttp можно несколькими способами:


  1. `aiohttp` лучше просто запускать с консоли если занимаемся разработкой, и с помощью `supervisor` еcли продакшен.
  2. Запустить с помощью `gunicorn` и `supervisor`.

Думаю для обоих случаев, в упрощенном варианте, подойдет настройка nginx как proxy, хотя gunicorn можно запустить через сокет при желании.


server {
            server_name       test.dev;
            location / {
                       proxy_pass http://127.0.0.1:8080;
           }
}

Aiohttp и supervisor


Устанавливаем supervisor:


apt install supervisor

В /etc/supervisor/conf.d/ создаем файл aio.conf и в нем:


[program:aio]
command=python3 index.py
directory=/path/to/project/
user=nobody
autorestart=true
redirect_stderr=true

После этого обновляем конфиги всех приложений, без перезапуска


supervisorctl reread
>>aio: available
>>erp: changed

Перезапуск приложений для которых обновился конфиг:


supervisorctl update
>>erp: stopped
>>erp: updated process group
>>aio: added process group

Смотрим статус приложений:


supervisorctl status
>>aio          RUNNING    pid 31570, uptime 0:06:49
>>erp          FATAL         Exited too quickly (process log may have details)

Теперь можно запустить простой сервер на aiohttp
import asyncio
from aiohttp import web

def test(request):
    return {'title': 'Hello' }

async def init(loop):
    app = web.Application( loop = loop )
    app.router.add_route('GET', '/', basic_handler, name='index')
    handler = app.make_handler()
    srv = await loop.create_server(handler, '127.0.0.1', 8080)
    return srv, handler

loop = asyncio.get_event_loop()
srv, handler = loop.run_until_complete(  init( loop )  )
try:  loop.run_forever()
except KeyboardInterrupt:  
          loop.run_until_complete(handler.finish_connections())

В случае с нашим небольшим фреймворком, в стартовом файле мы добавляем в sys.path нужные нам пути:


#путь к библиотеке
sys.path.append( settings.root )
#путь к проекту
sys.path.append( os.path.dirname( __file__ ) )

Aiohttp gunicorn и supervisor


Простой вариант для запуска с помощью gunicorn выглядит таким образом, тут нужно обратить внимание что мы не пишем над функцией index карутину, для вызова.


from aiohttp import web
def index(request):
    return web.Response(text="Hello!")

app = web.Application()
app.router.add_route('GET', '/', index)

Для запуска нашего фреймворка с помощью gunicorn мы немного упростим функцию инициализации, убрав оттуда карутину и все что касается сервера, и не забываем вернуть app.


Сама функция
def init_gunicorn():
    app = web.Application( middlewares=[ aiohttp_debugtoolbar.middleware, db_handler(), 
        session_middleware(EncryptedCookieStorage(b'Sixteen byte key')) ])
    aiohttp_debugtoolbar.setup(app)

    aiohttp_jinja2.setup(app, loader=jinja2.FunctionLoader ( load_templ ) )

    union_routes(os.path.join ( settings.root, 'apps' ) )
    union_routes(os.path.join ( os.getcwd(),  'apps' ) )

    for res in routes:
        app.router.add_route( res[2], res[0], res[1], name=res[3])
    app.router.add_route('GET', '/static/{component:[^/]+}/{fname:.+}', union_stat) 
    return app

Ну а в файле который будет уже запускать gunicorn мы просто вызываем


import  sys, os, settings
sys.path.append( settings.root )
sys.path.append( os.path.dirname( __file__ ) )

from core.union import init_gunicorn
app = init_gunicorn()

Теперь можно просто запустить сам gunicorn из папки с файлом


>> gunicorn app:app -k aiohttp.worker.GunicornWebWorker -b localhost:8080

Естественно что команду вызова можно просто прописать в конфигурации supervisor.


Для запуска gunicorn через supervisor у нас будет следующая конфигурация, в папке с проектом создаем файл gunicorn.conf.py в нем:


worker_class ='aiohttp.worker.GunicornWebWorker'
bind='127.0.0.1:8080'
workers=8
reload=True
user = "nobody"

В /etc/supervisor/conf.d/name.conf:


[program:name]
command=/usr/local/bin/gunicorn app:app -c /path/to/project/gunicorn.conf.py
directory=/path/to/project/
user=nobody
autorestart=true
redirect_stderr=true

Выполняем команды:


supervisorctl reread
supervisorctl update


8. После установки, о примерах.


Теперь мы можем установить нашу библиотечку


pip3 install tao1

Естественно после установки нам нужно развернуть проект и создать в нем пару модулей и т.д.
Команда utils.py -p name создаст нам проект в папке в которой мы её выполним, естественно, вместо -p можно написать --project или --startProject.
Команду utils.py -a name надо выполнять в директории apps вашего проекта и в ней так же опцию -a можно заменить на --app или --startApp ;-)


Сам utils.py устроен довольно просто.
Создание проекта или модуля выглядит так.
С помощью модуля argparse получаем опции из командной строки:


parser = argparse.ArgumentParser()
parser.add_argument('-project', '-startproject', '-p', type=str, help='Create project' )
parser.add_argument('-app', '-startapp', '-a',         type=str, help='Create app'     )
args = parser.parse_args()
```В зависимости от опций копируем уже заранее заготовленные файлы лежащие в библиотеке в нужное место:
```python
import shutil
shutil.copytree( os.path.join( os.path.dirname(__file__), 'sites', 'test'), str(args.project) )

А в файле setup.py где мы инициализируем наш пакет для установки в
https://pypi.python.org/ указываем scripts=['tao1/core/utils.py'].
Тогда после установки пакета файл utils.py будет помещен в /usr/local/bin/ (если говорить о ubuntu) и станет исполняемым.


9. Road map


Версия 0.2 — 0.5


  • Кеширование ( скорее всего memcached).
  • Мультиязычность.
  • Небольшой каркас для написания он-лайн игр.
  • Полноценная админка. Более менее полноценные блоги и интернет магазин.
  • Каркас для конструктора справочников и документов для создания своих конфигураций.

И по возможности, постараюсь сделать более менее удобный установщик, чтоб любой желающий мог, приходя из любой другой экосреды, например, мира php или Node, быстро удовлетворить своё любопытство. Хотя, возможно, это не совсем правильный подход.


P.S. Все постарался описать максимально кратко. Естественно, в этой версии даже для заявленных возможностей скорее всего есть масса ошибок, очевидных и не очень, поэтому прошу сообщать. А также всех кого заинтересовала эта библиотека и вообще развитие темы Asyncio в данном формате. Пишите свои замечания и пожелания для функционала и я постараюсь по возможности исправить и реализовать.


Исправления грамматических неточностей и ошибок приветствуются в личке.


Библиотека на github
Документация на readthedocs


Вторая часть


Используемые материалы:


pep-0492
Блог svetlov автора aiohttp
Документация по aiohttp на github
Документация по aiohttp на readthedocs
Документация по aiohttp-jinja2 readthedocs
Документация по yield from
aiohttp_session
Асинхронный драйвер
aio-libs — список библиотек
Еще один более полный список

Поделиться публикацией

Комментарии 12

    +8
    Вы же сюда пришли и за критикой в том числе, верно?

    Предлагаю сильно поменять roadmap:
    1. Понять зачем это вообще нужно.
    Серьезно. И дело даже не в том, что это очередной фреймворк, который никому особо не нужен, т.к. ничего нового не несет. А не несет он по двум причинам: asyncio почти не используется; asyncio используется неправильно.
    Почему почти не используется? Потому что вы в конфиге размещаетесь за асинхронным nginx'ом и за асинхронным gunicorn'ом (worker_class=gevent).
    Почему используется неправильно?
    github.com/alikzao/tao1/blob/master/tao1/core/core.py#L34
    github.com/alikzao/tao1/blob/master/tao1/core/core.py#L40
    Две совершенно ненужных корутины. asyncio нужно использовать в тех местах, где может быть задержка — I/O, работа с сетью и прочее. Использовать ее в поиске по словарю — довольно странная затея.

    2. Исходный код.
    Он, мягко говоря, хромает. Куча закомментированных строк, полное несоответствие PEP8

    assert sys.version >= '3.3', 'Please use Python 3.4 or higher.'
    

    Просите версию 3.4, а сравниваете с 3.3. И кстати, данный метод сравнения версий может принести много сюрпризов, если версия питона изменится с 3.9 на 3.10.

    github.com/alikzao/tao1/blob/master/tao1/core/utils_.py#L2
    Везде в файлах встречаются подобные строки. Они не нужны, если у вас питоной третьей версии, потому что стандартная кодировка для Python3 — UTF-8.
    Первая строчка (#!/usr/bin/env python) тоже не выглядит особо нужной. Вы же не делаете все файлы исполняемыми по умолчанию?

    В общем, вы уж извините, но все это выглядит так, как будто вы делаете что-то, но не знаете что именно и зачем.
      –1
      Дико плюсую. Часто на github вижу код, где буквально через строчку yield.
      0
      Конструктивная везде приветствуется.

      >>> это очередной фреймворк, который никому особо не нужен, т.к. ничего нового не несет.
      Им пользоваться никто не заставляет. И я написал что это не над asynio обертка, а над aiohttp, но мне для него захотелось модульную структуру к которой я привык.

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

      >>> asyncio почти не используется
      Я не предлагаю использовать низкоуровневый asyncio, чтоб этого не делать был написан aiohttp.

      По поводу # coding: utf-8 это старый атавизм, торопился и просто забыл его везде удалить.
      Закомментированные строчки для альфа версии это нормально, тем более их не так и много.

        +2
        Пилите-пилите, хе-хе :)

        Из aiohttp то Flask пытаются сделать, то Django — и то и другое, думаю, зря. Разве что как эксперимент, чтобы поучиться писать.

        Хотя сами попытки подтверждают, что библиотека получилась довольно удачная — можно крутить так и эдак.
          0
          Всё было идеально, пока внезапно шоткаты для реквеста не оказались в глобальном неймспейсе библиотеки ;)
            0
            Не хочется — не используй :)
            На самом деле самый правильный способ — это явное использование ClientSession
              0
              Вот-вот на уровне модуля рядом с web шоткаты, которые ещё и не совсем правильно юзать ;)
          +1
          В motor кстати, поддержку asyncio смержили, уже месяца два юзаю, полёт нормальный :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое