Мне часто приходится делать небольшие сервера на tornado. В каких-то проектах нужна поддержка работы с redis, в каких-то нет. В других надо рендерить ReactJS. И во всех нужно логирование. Для начала я поднял локальный pypi репозитарий, собрал свои наработки в питоний пакет и радовался жизни. Достаточно было установить пакет, импортировать из него классы, отнаследоваться и радостно пилить код дальше.

А потом появилась мысль — а не поделиться ли своими наработками с людьми? Итак представляю вашему вниманию пакет torskel.

Сразу оговорюсь, что он дружит только с Python 3.5+ т. к. во всю использует async/await.

Это обёртка над торнадо, которая позволяет получить из коробки базовый функционал:

Установка библиотеки — pip install torskel

Асинхронная работа с Redis


По умолчанию выключена, чтобы включить надо установить опцию use_redis в True и сделать pip install aioredis

Пример использования:

import asyncio
from torskel.torskel_app import TorskelServer
from torskel.torskel_handler import TorskelHandler
import tornado.web

from tornado.options import options


options.define('use_redis', default=True, help='use redis', type=bool)

class RedisApplication(TorskelServer):
    def __init__(self, handlers, **settings):
        super().__init__(handlers, **settings)
        self.greeting = 'Hello redis!'


class RedisHandler(TorskelHandler):
    async def get(self):
        my_key = self.get_hash_str('my_key')
        await self.set_redis_exp_val(my_key, self.application.greeting, 3000, convert_to_json=False)
        res = await self.get_redis_val(my_key, from_json=False)
        self.write(res)
        await self.del_redis_val(my_key)
        self.finish()

redis_app = RedisApplication(handlers=[(r"/", RedisHandler)])

if __name__ == '__main__':
    redis_app.listen(8888)
    loop = asyncio.get_event_loop()
    redis_app.init_with_loop(loop)
    loop.run_forever()
    tornado.ioloop.IOLoop.instance().start()

Список доступных опций со значениями по умолчанию:

Определяет надо ли вообще использовать редис

use_redis=False

По умолчанию подключение через сокет файл

use_redis_socket=True

Создаем пул коннектов — минимум 5, максимум 10

redis_min_con=5
redis_max_con=10

Как попасть в редис?

redis_host='127.0.0.1'
redis_port=6379
redis_socket='/var/run/redis/redis.sock'

Настройка авторизации

redis_psw=''
База по умолчанию
redis_db=1

Шлём логи на почту


Настраивается следующими опциями:

options.define('use_mail_logging', default=False, help='SMTP log handler', type=bool)
options.define("log_mail_subj", default='', type=str)
options.define("log_mail_from", default='', type=str)
options.define("log_mail_to", default=[], type=list)
options.define("log_mail_host", default='', type=str)
options.define("log_mail_user", default='', type=str)
options.define("log_mail_psw", default='', type=str)

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

Http запросы


Просто обёртка над стандартным торнадовским AsyncHttpClient

import tornado.web
from torskel.torskel_app import TorskelServer
from torskel.torskel_handler i mport TorskelHandler


class HelloHttpHandler(TorskelHandler):
    async def get(self):
        res = await self.http_request_get('http://example.com')
        self.write(res)
        self.finish()


hello_http_app = TorskelServer(handlers=[(r"/", HelloHttpHandler)])

if __name__ == '__main__':
    hello_http_app.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Поддержка ReactJS


Включается опцией use_reactjs. Так же надо сделать pip install jinja2 Здесь нам уже понадобится набор js-разработчика, npm, webpack/gulp и прочие babel'ы

Рендер происходит функцией react_render. Предполагается, что вашем html шаблоне, вы подключаете скрипт строчкой

<script src="{{ assets['main']['js'] }}"> </script>

Этот момент довольно тонкий и в будущем я планирую его доработать.

import tornado.web
import os
from tornado.web import url
from tornado.options import options, define
from torskel.torskel_app import TorskelServer
from torskel.torskel_handler import TorskelHandler

settings = {}

options.define('use_reactjs', default=True, help='use reactjs', type=bool)
options.define("react_assets_file", default='webpack-assets.json', type=str)

class MainHandler(TorskelHandler):
    def get(self):
        self.react_render('index.html')
        self.finish()

handlers = [
    url(r"/", MainHandler, name="IndexPage"),

]


class HelloReactApplication(TorskelServer):
    def __init__(self, handlers,  **settings):
        super().__init__(handlers,  **settings)

hello_react = HelloReactApplication(handlers, root_dir=os.path.dirname(__file__), **settings)

if __name__ == "__main__":
    hello_react.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

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

PS

Ссылка на гитхаб
github.com/frostspb/torskel

Комментарии (6)


  1. resetme
    23.11.2017 17:31

    Хотел бы ознакомиться с исходным кодом, но ссылку на репозитарий библиотеки в статье не нашел. Не могли бы дать?


  1. frostspb Автор
    23.11.2017 17:52

    Обновил, добавил ссылку на гитхаб в конце. скопирую и сюда
    github.com/frostspb/torskel


    1. resetme
      23.11.2017 18:34

      Спасибо, посмотрел. Маленький ревью кода:

      1. Не ловите все исключения, а только конкретные, те которые нужно отработать;
      2. Не оборачивайте logger во вспомогательные методы или функции, используйте его напрямую;
      3. Лучше сразу упасть при проблеме с импортом, чем потом когда приложение инициализируется;
      4. Надо следить за строгостью оформления кода по PEP8;
      5. Табы в коде Python — это зло;
      6. Если нацелены на большой охват аудитории, то не пишите докстринги на русском;

      Если не понятен какой-либо пункт, я постараюсь разъяснить отдельно.

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

      Если есть вопросы, я с удовольствием отвечу.


  1. frostspb Автор
    23.11.2017 20:54

    Спасибо, за отзыв. Есть вопросы.
    2 — почему оборачивать логгер плохо?
    3 — тут расчёт на то, что может понадобиться просто приложение, без подключенного функционала редиса/реакта, то есть получается что импортировать тот же aioredis необязательно
    по 4,5,6 — вопросов нет, так вышло, тоже буду исправлять в будущем.


    1. foldr
      24.11.2017 11:31

      3 — следствие того, что Вы в одной библиотеке пытаетесь собрать разный, несвязанный функционал. Слышали такое понятие Unix-way?


      Пишите программы, которые делают что-то одно и делают это хорошо

      То же применимо к библиотеке. Обертка для редиса — одно, для реакта — другое, логирование — третье. Тогда, если мне нужен будет какой-то функционал, я сделаю pip install torskel-redis,
      и не нужны будут костыли вроде use_redis=False. Соответственно, у разных библиотек должны быть разные setup файлы со своими зависимостями


      1 — добавлю про исключения. Не делайте try .. except всех исключений. Почему — см. на sof


    1. resetme
      24.11.2017 20:45

      2 — почему оборачивать логгер плохо?

      Он уже готов к использованию, не нужно оберток, он и так простой. Где-то в начале файла мы его получаем и используем в любом месте.
      3 — тут расчёт на то, что может понадобиться просто приложение, без подключенного функционала редиса/реакта, то есть получается что импортировать тот же aioredis необязательно

      Вам редко надо будет включать или отключать Redis в приложении. Проще aioredis сразу установить и импортировать. Иначе вам придется постоянно проверять переменную на None, если она не пустая только тогда вызывать функцию из пакета.

      Вот этим куском вы как бы эмулируете стандартное поведение Python при импорте без обработки исключения ImportError:
      try:
          import aioredis
      except ImportError:
          aioredis = None
      ...
                 if aioredis is None:
                      raise ImportError('Required package aioredis is missing')
                  else:
                      self.init_redis_pool(loop)
      ...