Эта статья посвящена очередному REST фреймворку (для Python 3), особенностью которого является использование веб-сокетов для обмена данными между клиентом и сервером. О том откуда пришла идея, с чем мне пришлось столкнулся при написании своей первой библиотеки для Python и что из этого в итоге получилось, я расскажу далее.


Для тех, кому интересна эта статья — пожалуйста, заходите под кат.

1. Идея проекта


Идея зародилась примерно в середине Апреле 2015, когда я задержался с коллегой на работе, с которым мы числимся на одном проекте в своей конторе. Чтобы как-то минимально себя развлечь, пока занимались непосредственно программированием, мы решили поговорить о различных интересных питоновских проектах. В процессе общения как-то спонтанно подошли к теме о собственных проектах и того, что можно было бы интересно использовать далее в своих проектах (не обязательно связанных с работой). При обсуждении непосредственно и возникла идея того, что было бы классно иметь достаточно «гибкий» фреймворк, который использует веб-сокеты, через которые можно передавать данные в обе стороны без каких-либо проблем. Каждый запрос приходит в JSON формате и содержит некоторые заголовки, которые привычны при использовании REST и HTTP протокола. И в качестве приятного дополнения предоставляет возможность передачи уведомлений (нотификаций) со стороны сервера клиенту из коробки по какому-то событию/тайм-ауту.

Естественно после столь продолжительного обсуждения я решился воплотить эту идею в жизнь (а почему бы и да?). Собственный интерес, энтузиазм и желание сделать что-нибудь полезное для развития экосистемы третьего Python'а только давало лишнюю мотивацию побыстрее приступить к делу.

2. Постановка целей


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

  • Постараться использовать asyncio при обработке клиентских запросов
  • Не более 1-2 зависимых модулей (чем меньше, тем лучше)
  • Не должна быть слишком сложной для понимания
  • Легкость в использовании (см. фреймворки Django REST, Flask, которые достаточно простые и гибкие)
  • Программист может заменить практически любой компонент, тогда, когда ему это необходимо

Естественно, выпустить в первой же версии все вышеупомянутое для меня было совсем чем-то нереальным, поскольку из процесса разработки я просто бы не выходил, поэтому в целях небольшого упрощения было принято решение разбить все на небольшие «кусочки». Их сделать, протестировать, пустить в релиз, а затем уже делать по аналогичной схеме все оставшееся. Сначала пишем то, что является наиболее критичным для библиотеки (роутинг, вьюхи, аутентификация, и т.д.), а позднее, по мере возможностей, добавляем новый функционал.

3. Подготовка к разработке: выбор между Aiohttp vs Gevent vs Autobahn.ws


Разработка началась примерно в конце Апреля 2015. Чтобы как-то облегчить себе работу при реализации проекта, начались поиски каких-либо уже готовых решений (или уже существующих библиотек, о которых ранее не предполагал). Библиотек, которые бы имели схожую идею с моей или хотя бы минимально имели из коробки то, что предполагается сделать – не нашлось. Это привело к усложнению задачи, поскольку большую часть необходимых компонентов потребуется написать самостоятельно, исходя из собственного понимания всех происходящих процессов.

Я решил начать непосредственно с библиотек, которые дают возможность использовать веб-сокеты. На тот момент времени было найдено несколько таких пакетов: aiohttp, gevent и autobahn.ws. У каждой библиотеки есть свои достоинства и недостатки, но, в первую очередь, я исходил из их возможностей и возможного дальнейшего переиспользования кода, чтобы не приходилось в очередной раз городить свои велосипеды, особенно там, где это не нужно.

Aiohttp – библиотека для веб-разработки, базирующая на стандартной библиотеке asyncio и разработанная svetlov. Не сказать, что у меня был какой-то большой реальный опыт использования этой библиотеки, хотя стоит отметить, что сделано множество вещей очень классно. Однако, предлагаемое решение с веб-сокетами показалось мне несколько низкоуровневым (хотя, в ряде случаев это действительно может быть удобно). Хотелось какого-то большего уровня абстракции (например, как в gevent-websocket или autobahn.ws, где в клиенте/сервере есть методы вроде onMessage и sendMessage, столь похожие на методы из событийно-ориентированного фреймворка Twisted). В остальном же – библиотека прекрасна.

Gevent при первом рассмотрении был одним из тех первых пакетов, на которые было заострено внимания. И также быстро идея о использовании её была отклонена: на момент времени начала проекта (Апрель 2015) gevent не был портирован под третью ветку языка Python. Хотя, если бы все же она была портирована, то я использовал бы именно её, взяв при этом еще расширение gevent-websocket и все могло бы выйти очень даже неплохо. На момент написания статьи данная библиотека уже имеет поддержку третьей ветки, но переходить на нее сейчас я не вижу никакого смысла.

Autobahn.ws – это та библиотека, с которой мне уже ранее приходилось неоднократно сталкиваться при написании своих небольших pet-проектов и с которой у меня уже имеется некий минимальный опыт использования. Достаточно неплохое коммьюнити, плюс автор библиотеки всегда готов помочь в случае возникших проблем (например, когда у меня не получалось совместить ее с Twisted + wxPython, Тобиас очень хорошо объяснил мне как это можно сделать). Последние версии совместимы с asyncio, достаточно добавить декораторы в требуемых местах. Приятной особенностью еще было соответствие документу RFC6455 и наличие проверки входящих/исходящих данных (поступили/отправлены ли они в UTF-8 кодировке, что я считаю достаточно удобно). Поэтому было принято решение использовать именно её в качестве основы для будущей библиотеки.

4. Проблемы, возникшие при разработке


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

1) Получили запрос
2) Проверили что пришли необходимые данные, на основе которых станет понятно каким образом обработать запрос (тип операции, куда обращаемся, и т.д.)
3) Ищем обработчик, соответствующий поступившему запросу (конкретную точку входа и метод, который будет вызываться). Если ничего не нашли – возвращаем ошибку. Если же все отлично, то выбираем соответствующий обработчик и в него передаем полученные аргументы
4) Сформированный ответ привели к определенному формату (JSON, XML, и т.д.)
5) Вернули ответ клиенту

В теории все звучит довольно просто, на деле все оказалось все в точности наоборот. Единственное, что мне приходило в голову, это идти от высокого уровня абстракции к нижним. То есть я шел следующим образом, когда мы работаем с Autobahn.ws и asyncio loop:

1) Создаем экземпляр «фабрики», который будет использовать asyncio loop и принимать входящие подключения и обслуживать их. После выполненного «процесса рукопожатия» мы готовы получать запросы от клиента и выполнять их обработку.

2) Получили запрос от клиента в определенном формате. В нашем случае мы будем получать его в виде JSON следующим образом:

{
   'method': 'POST', 
   'url': '/users/create',
   'args': {
       'token': 'aGFicmFoYWJyX2FkbWlu'
   },
   'data': {
       'username': 'habrahabr',
       'password': 'mysupersecretpassword',
   }
}

Этот JSON имеет достаточно простую структуру. Клиент достаточно определить несколько важных для нас параметров:

  • method – тип операции над ресурсом (подобно тому, как это сделано в HTTP).
  • url – путь к ресурсу, с которым хотим работать.
  • args (опционально) – набор параметров, отсылаемых серверу. Наиболее близкая аналогия это определяемые параметры в URL'е HTTP запроса с помощью "?" и "&" символов, вроде «habrahabr.ru/?page=2&paginate_by=25». Это может быть какой-то список готовых данных (например, идентификаторы пользователей, которым надо назначить определенную группу) или просто набор аргументов для каких-либо фильтров, используемых на стороне сервера в процессе обработки запроса.
  • data (опционально) – набор данных, используемых при работе с ресурсом. В целом, можете считать, что это некий аналог телу HTTP запроса.
  • event_name (опционально) — некий уникальный идентификатор, с помощью которого можно понять от какого endpoint'а вернулись данные.

Примерно такого вида запросы будет ожидать получить наш сервер. Если какого-либо из обязательных аргументов нету – говорим об этом сразу (например, забыли добавить method). В противном случае идем далее по нашему списку.

3) Итак, запрос доставлен серверу, он правильном формате и корректен. Теперь мы хотим его обработать соответствующим образом и вернуть ответ. Однако, что нам для этого необходимо? С моей точки зрения, на первое время будет достаточно наличие системы роутинга, позволяющей зарегистрировать на определенный URL требуемый обработчик, который бы формировал соответствующий ответ, преобразовывал его в JSON, XML (или любой другой формат) и возвращал его клиенту.

В этом пункте хочу я обратить ваше внимание на роутинг. Это достаточно важный момент, поскольку нам хотелось бы предоставлять доступ по некоторому фиксированному URL, чтобы получать, например, список текущих пользователей (вроде "/users/"). С другой стороны получать доступ и по URL имеющих вид "/users//", по которым требуется получать детальную информацию о пользователе. То есть роутинг первого вида мы будем рассматривать как простой, статический, а второй – как динамический, поскольку в пути к ресурсу присутствует параметр, меняющийся от запроса к запросу.

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

router = SimpleRouter()
router.register('/auth/login', LogIn, 'POST')
router.register('/users/{pk}', UserDetail, ['GET', 'PATCH'])

Мы будем выполнять анализ пути к такому ресурсу. И создавать endpoint, который будет обрабатывать запросы только определенного типа и только по указанному пути. Когда придет запрос на этот ресурс, нам будет достаточно пройтись по словарю, где ключом будет путь, а значением – обработчик. В случае, если обнаружен динамический путь, в момент получения запроса, и мы нашли требуемый обработчик, то будем пробрасывать обнаруженный динамический параметр в место обработки запроса, чтобы было возможным получить объект по ключу либо сделать какую-то иную операцию с использованием этого параметра.

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

4) Здорово, теперь кое что прояснилось. Умеем находить требуемые пути, обработчики для них, а с помощью регулярок находить и пробрасывать параметры (для случая если попался динамический путь). Далее мы смотрим на параметр method, указанный в JSON и стараемся найти соответствующий метод класса с вьюшки. Если он отсутствует – говорим об этом сразу и не выполняем каких-либо операций. В противном случае делаем вызов обнаруженного метода и формируем ответ.

5) Далее выполняем сериализацию данных (в том числе и для случаев с ошибками) в некоторый формат. По умолчанию все преобразуется в JSON формат.

6) Передаем сформированный ответ клиенту обратно по веб-сокету.

И вот по этому примерному плану я следовал до релиза 1.0. Было достаточно интересно написать свои вьюшки, систему роутинга и прочий интересный функционал. Хотя в процессе написания первого релиза, по ходу развития этого pet-проекта, потребовались модули с конфигурациями (в нашем случае это был модуль аналогичный тому, что есть в Django). Или, например, столь необходимая мне аутентификация медленно привела к реализации поддержки middleware и JSON Web Token модулей. Как и упоминалось ранее – делаем всевозможные модули самостоятельно, не стараемся тянуть что-то лишнее.

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

Если при написании первой версии написание кода и его отладка шла достаточно неплохо, то при реализации версии 1.1 я просто надолго повяз в отладке. Написание и портирование кода не занимало столь много времени, сколько поиск и детальный анализ того что происходит, например:

1) Анализ исходной кодовой базы Django REST фреймворка на предмет того, что и как происходит «под капотом»: что делаем когда хотим записать или прочитать определенный объект; когда и каким образом понимаем, что за поля были получены (и имеют ли они вообще какие-то связи с другими моделями) и во что требуется их сериализовать/десериализовать.

2) Сериализация моделей SQLAlchemy по аналогии с тем, как это происходит между Django REST кодом и Django ORM.

3) Иметь такую возможность работы с роутингом, чтобы можно было получить путь до некоторого объекта через уже написанный API (так, чтобы можно было и прочитать, и записать какие-то данные по полученным URL).

При разработке этой части функционала мне весьма сильно помогли исходные коды библиотеки как Django REST (которая во многом являлась основой для следующей версии), так и исходники SQLAlchemy + marshmallow-sqlalchemy библиотек, которые во многом помогли воплотить все задумки в жизнь.

Хоть и было затрачено очень много ресурсов, но конечный результат полностью оправдал все затраты – теперь мы имеем возможность работать с SQLAlchemy так, как мы привыкли это делать в Django REST. Работа с данными осуществляется одинаково и практически не имеет сильных отличий. Здорово, даже практически переучиваться нет необходимости: доступный API во многом идентичен тому, что используется в Django REST.

5. Текущее состояние проекта


На текущий момент времени библиотека предоставляет следующие возможности:

  • Роутинг
  • Поддержка function- и class-based вьюшек
  • Аутентификация через JSON Web Token (хоть и немного ограничено)
  • Поддержка файла с конфигурацией, подобной той, что есть в Django Framework
  • Сжатие передаваемых сообщений (если поддерживается браузером и установлено нужное расширение)
  • Сериализация моделей Django и SQLAlchemy ORM
  • Поддержка SSL

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


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

# -*- coding: utf-8 -*-
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, validates

Base = declarative_base()


class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    fullname = Column(String(50), default='Unknown')
    password = Column(String(512))
    addresses = relationship("Address", back_populates="user")

    @validates('name')
    def validate_name(self, key, name):
        assert '@' not in name
        return name

    def __repr__(self):
        return "<User(name='%s', fullname='%s', password='%s')>" % (self.name, self.fullname, self.password)


class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))
    user = relationship("User", back_populates="addresses")

    def __repr__(self):
        return "<Address(email_address='%s')>" % self.email_address

Теперь опишем соответствующие сериализаторы для этих двух моделей:

# -*- coding: utf-8 -*-
from app.db import User, Address
from aiorest_ws.db.orm.sqlalchemy import serializers

from sqlalchemy.orm import Query


class AddressSerializer(serializers.ModelSerializer):

    class Meta:
        model = Address
        fields = ('id', 'email_address')


class UserSerializer(serializers.ModelSerializer):
    addresses = serializers.PrimaryKeyRelatedField(queryset=Query(Address), many=True, required=False)

    class Meta:
        model = User

Как многие из успели заметить, в месте, где мы определили класс для сериализации пользователей, указано поле addresses, с аргументом queryset=Query(Address) в конструкторе класса PrimaryKeyRelatedField. Это сделано для того, чтобы сериализатор для SQLAlchemy ORM мог выстроить связь между полем addresses и таблицей, передавая в этот класс при сериализации первичные ключи. В какой-то степени это аналогично QuerySet из Django фреймворка.

Теперь реализуем вьюшки, позволяющие через некоторый доступный API работать с данными в этих таблицах:

# -*- coding: utf-8 -*-
from aiorest_ws.conf import settings
from aiorest_ws.db.orm.exceptions import ValidationError
from aiorest_ws.views import MethodBasedView

from app.db import User
from app.serializers import AddressSerializer, UserSerializer


class UserListView(MethodBasedView):

    def get(self, request, *args, **kwargs):
        session = settings.SQLALCHEMY_SESSION()
        users = session.query(User).all()
        return UserSerializer(users, many=True).data

    def post(self, request, *args, **kwargs):
        if not request.data:
            raise ValidationError('You must provide arguments for create.')

        if not isinstance(request.data, list):
            raise ValidationError('You must provide a list of objects.')

        serializer = UserSerializer(data=request.data, many=True)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return serializer.data


class UserView(MethodBasedView):

    def get(self, request, id, *args, **kwargs):
        session = settings.SQLALCHEMY_SESSION()
        instance = session.query(User).filter(User.id == id).first()
        return UserSerializer(instance).data

    def put(self, request, id, *args, **kwargs):
        if not request.data:
            raise ValidationError('You must provide an updated instance.')

        session = settings.SQLALCHEMY_SESSION()
        instance = session.query(User).filter(User.id == id).first()
        if not instance:
            raise ValidationError('Object does not exist.')

        serializer = UserSerializer(instance, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return serializer.data


class CreateUserView(MethodBasedView):

    def post(self, request, *args, **kwargs):
        serializer = UserSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return serializer.data


class AddressView(MethodBasedView):

    def get(self, request, id, *args, **kwargs):
        session = settings.SQLALCHEMY_SESSION()
        instance = session.query(User).filter(User.id == id).first()
        return AddressSerializer(instance).data


class CreateAddressView(MethodBasedView):

    def post(self, request, *args, **kwargs):
        serializer = AddressSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return serializer.data

На текущий момент времени мы пишем отдельно вьюшки для работы с объектами и отдельно со списком объектов. В каждом из таких подклассов, унаследованных от MethodBasedView, реализуются конкретные обработчики, которые будут использоваться. Для каждого типа запроса (get/post/put/patch/ и т.п.) пишется свой обработчик.

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

# -*- coding: utf-8 -*-
from aiorest_ws.routers import SimpleRouter

from app.views import UserListView, UserView, CreateUserView, AddressView,     CreateAddressView

router = SimpleRouter()
router.register('/user/list', UserListView, 'GET')
router.register('/user/{id}', UserView, ['GET', 'PUT'], name='user-detail')
router.register('/user/', CreateUserView, ['POST'])
router.register('/address/{id}', AddressView, ['GET', 'PUT'], name='address-detail')
router.register('/address/', CreateAddressView, ['POST'])

Вообщем-то здесь все готово, остается только запустить сервер и подключиться через какой-нибудь клиент (Python + Autobahn.ws, используя JavaScript, и так далее, вариантов множество). Для примера я просто покажу парочку простых запросов с использованием Python + Authobahn.ws (оговорюсь заранее, пример с клиентом не идеален, здесь задача просто продемонстировать как мы можем это делать):

# -*- coding: utf-8 -*-
import asyncio
import json

from hashlib import sha256
from autobahn.asyncio.websocket import WebSocketClientProtocol,     WebSocketClientFactory


def hash_password(password):
    return sha256(password.encode('utf-8')).hexdigest()


class HelloClientProtocol(WebSocketClientProtocol):

    def onOpen(self):
        # Create new address
        request = {
            'method': 'POST',
            'url': '/address/',
            'data': {
                "email_address": 'some_address@google.com'
            },
            'event_name': 'create-address'
        }
        self.sendMessage(json.dumps(request).encode('utf8'))

        # Get users list
        request = {
            'method': 'GET',
            'url': '/user/list/',
            'event_name': 'get-user-list'
        }
        self.sendMessage(json.dumps(request).encode('utf8'))

        # Create new user with address
        request = {
            'method': 'POST',
            'url': '/user/',
            'data': {
                'name': 'Neyton',
                'fullname': 'Neyton Drake',
                'password': hash_password('123456'),
                'addresses': [{"id": 1}, ]
            },
            'event_name': 'create-user'
        }
        self.sendMessage(json.dumps(request).encode('utf8'))

        # Trying to create new user with same info, but we have taken an error
        self.sendMessage(json.dumps(request).encode('utf8'))

        # Update existing object
        request = {
            'method': 'PUT',
            'url': '/user/6/',
            'data': {
                'fullname': 'Definitely not Neyton Drake',
                'addresses': [{"id": 1}, {"id": 2}]
            },
            'event_name': 'partial-update-user'
        }
        self.sendMessage(json.dumps(request).encode('utf8'))


    def onMessage(self, payload, isBinary):
        print("Result: {0}".format(payload.decode('utf8')))


if __name__ == '__main__':
    factory = WebSocketClientFactory("ws://localhost:8080")
    factory.protocol = HelloClientProtocol

    loop = asyncio.get_event_loop()
    coro = loop.create_connection(factory, '127.0.0.1', 8080)
    loop.run_until_complete(coro)
    loop.run_forever()
    loop.close()

Более детально посмотреть весь исходный код примера можно здесь.

7. Дальнейшее развитие


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

  • Поддержка уведомлений
  • Просмотр через браузер документации к API (возможно в виде плагина для Swagger)
  • Модули для тестирования API
  • Клиенты для Python и JavaScript
  • Поддержка Pony и Peewee ORM'ов

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

8. И в заключении...


Мне кажется получилось достаточно неплохо для первого раза, не смотря на отсутствие какого-либо опыта в написании собственных библиотек. А внести свой вклад (пусть даже и небольшой) в развитие языка Python – хочется достаточно сильно. Не удивляйтесь тому, сколько времени было на это было затрачено: все делалось (и продолжает делаться) в свободное время и периодическими перерывами (поскольку регулярная работа с одним проектом очень утомляет, а развиваться хочется в нескольких направлениях одновременно).

Так или иначе, буду рад услышать все ваши предложения, идеи и улучшения по данной библиотеке в комментариях (или в виде пул реквестов у меня на GitHub). Не стесняйтесь задавать какие-либо вопросы относительно библиотеки и каких-то особенностей реализации – буду рад любому фидбеку.

Весь вышеприведенный код, а также исходники библиотеки aiorest-ws, можно посмотреть на GitHub. Примеры расположены в корне проекта, в каталоге examples. Документацию можно посмотреть здесь.
Поделиться с друзьями
-->

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


  1. p00h
    08.12.2016 22:18
    -3

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


    1. Relrin
      08.12.2016 22:45
      +1

      Зачем Вы указываете кодировку utf8 в заголовке файла? Проект же под третий питон?

      Да, проект написан под 3ку. Относительно наличия «utf8» в заголовке каждого файла могу лишь сказать что это банальная привычка. На работе (наверное, как и многие) все также пишу на 2ой ветке.


    1. novoxudonoser
      08.12.2016 23:26
      +2

      под linux иногда могут быть проблемы (в 3.4) с русскими комментариями без utf8 в заголовке файла


      1. svetlov
        09.12.2016 11:19
        +1

        чушь. Сказки для юных программистов.


  1. svetlov
    08.12.2016 22:26
    +1

    Нельзя вызывать блокирующий синхронный код ( `.save()` и т.д.) из асинхронного.

    Точнее, это можно делать пока у вас количество пользователей не больше десятка-двух.

    Потом всё начнёт залипать.


    1. Relrin
      08.12.2016 22:41
      +1

      В целом, согласен с вышесказанными. Этот момент реально стоит мне как-то продумать более детально, поскольку мы упираемся в количество имеющихся воркеров.
      Были идеи сделать что-то в связке с aiopg, чтобы появилась возможность работы асинхронно с БД (хоть только и PostgreSQL). Правда возникает вполне простой вопрос: а как мне можно перейти в асинхронный код при работе aiorest-ws с Django, у которого множества различных адаптеров есть под разные базы?
      Текущее решение не идеально, на текущий момент, но хоть есть от чего отталкиваться :)


      1. svetlov
        08.12.2016 23:18
        +1

        Нельзя быть немного беременной, а потом надеяться как-то продумать ситуацию более детально.


        Код или синхронный или — а\синхронный. Совмещать не удасться.


        1. frol
          08.12.2016 23:31
          +1

          Справедливости ради, если всё остальное в норме, то можно сказать, что работа с БД просто ещё не реализована правильно… и этот частный, хоть и частый случай, по-моему можно таки продумать более детально чуть позже (естественно, до того как предлагать применять в продакшене).


          1. svetlov
            09.12.2016 11:22
            +1

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


            1. frol
              09.12.2016 11:45

              Вы абсолютно правы, я знаком с asyncio, да и с остальными async инструментами, сугубо теоретически, причём именно из-за озвученной вами проблемы — нужно всё переписать чтобы сделать что-то маломальски пользователе-ориентированное. Раз вы говорите, что всё так плохо, то, видимо, автору нужно приоритезировать решение этой проблемы.


            1. renskiy
              10.12.2016 01:03
              +1

              Как-то на досуге портировал peewee под asyncio. В общем то работает, и интерфейс полностью сохраняется. Но вы правы, по сути вышла совсем новая библиотека. Но доводить решение до конца не стал по причине отсутствия понимания того, где это может пригодиться. Мне кажется из асинхронного приложения гораздо проще сделать вызов синхронного API, вместо сложных попыток приспособить не приспособленное.


              1. svetlov
                10.12.2016 08:07

                Просто так вызывать блокирующий синхронный код нельзя — event loop залипает.
                Запускать в thread pool — ненамного лучше. Под нагрузкой производительность начинает заметно проседать.


                Остается два выхода:


                1. Писать свои правильные асинхронные библиотеки
                2. Поднять руку, резко её опустить и сказать: "И зачем мне эта асинхра? Буду до пенсии писать на Django!".

                Первый вариант интересный и познавательный, второй — спокойный и стабильный.


                1. renskiy
                  10.12.2016 09:43
                  +1

                  Я имел в виду проще вызывать из асинхронного кода синхронные API методы через асинхронный HTTP клиент (который в большинстве асинхронных фреймворков идет из коробки). В этом случае асинхронный компонент выполняет только те задачи, которые требуют асинхронного подхода, а все остальное делегируется простым в реализации синхронным решениям вроде того же Django.

                  В случае же с БД обойтись без Django вообще почти не вариант. Там и админка, и генерация/применение/откат миграций, и удобный отлаженный ORM. На перенос всего этого хозяйства в асинхронный компонент уйдут годы. В промежуточный же этап в большинстве случаев придется поддерживать дублирование кода (например, описывать модели нужно будет в обоих компонентах).


                  1. svetlov
                    10.12.2016 10:08

                    1. Не думаю что это рабочее решение. С таким же успехом можно мастерить асинхронный Python код который для доступа к базе дергает Go или JavaScript. Меня от этой идеи в дрожь бросает (и не потому что я Go/JavaScript недолюбливаю).


                    2. Django — не ко всем бочкам затычка. И эта затычка очень часто оказывается не той формы как дырка. Как ORM она ужасна.


                    1. renskiy
                      10.12.2016 10:37
                      +1

                      Решений на свете много разных, и все как правило хороши в какой-то определенной области применения. Go/Javascript хороши тем, что сами могут выступать в роли легковесной асинхронной обертки над тяжелыми «все-в-одном» синхронными фреймворками. Кстати, весьма интригующе в этом ключе звучит ответ Игоря Сысоева на вопрос о том, чем он сейчас занимается.

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

                      Насчет ORM, по большому счету все они ужасны. Я думаю, что идея ORM не в том, чтобы предоставлять более удобный способ выполнять SQL запросы, а в том, чтобы объединить под одной общей абстракцией такие вещи как: описание модели, генерация/применение/откат миграций и осуществление самих запросов. Сложно представить решение, которое все перечисленное реализует при помощи нативного SQL — поддержание консистентного состояния системы в этом случае будет задачей очень сложной.


    1. tmnhy
      08.12.2016 22:52

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

      В итоге остановился на такой реализации, в доке к websockets описана, псевдокод:

      async def receive():
          """ Слушаем сокет """
      
      async def message_to_sent():
          """ Ждем сообщение для отправки """
      
      While True:
          listener_task = asyncio.ensure_future(receive())
          sender_task = asyncio.ensure_future(message_to_sent())
      
          done, pending = await asyncio.wait(
              [listener_task, sender_task], return_when=asyncio.FIRST_COMPLETED)
      
          if listener_task in done:
              # Процессинг входящих сообщений
              handler(listener_task.result())
          else:
              listener_task.cancel()
      
          if sender_task in done:
              # Отправка
              send_str(sender_task.result())
          else:
              sender_task.cancel()
      


      Или это можно сделать более «красиво»?


      1. svetlov
        08.12.2016 23:14

        `ws.send_str()` не корутина если что.
        Это намек


        1. tmnhy
          08.12.2016 23:19

          Это я знаю, а намёк не понял ) Будет «залипать» при большом количестве исходящих сообщений?

          А какие еще варианты для websocket клиента?


          1. svetlov
            09.12.2016 11:24

            sender_task не нужен, шлите данные откуда есть


  1. tmnhy
    08.12.2016 22:30
    -2

    4) Здорово, теперь кое что прояснилось.


    И после этого читать не получается. )
    Потому что не прояснилось и потому что тяжело и лениво вникать.

    Для кого и для чего материал?
    Если это тутор, то на какой уровень подготовки?

    Если это описание фреймворка, то слишком абстрактный пример и много «воды».
    Если это подход к построению фреймворка, то слишком много ненужных в этом случае деталей…


  1. frol
    08.12.2016 22:41

    Браво! Я просто не могу подобрать слов! Тема WebSockets в Python для меня была просто пыткой, сколько я ни пытался заставить себя погрузиться в неё, всё время какое-то отторжение происходило и я быстро находил на что отвлечься. aiorest-ws (по крайней мере по примерам и описанию) — это огромный шаг в сторону упрощения.


    Ваша реализация в стиле Flask мне импонирует, как и использование REST подхода. Swagger (OpenAPI) вам не факт, что поможет в плане какой-то готовой реализации (по крайней мере я не слышал о REST WebSockets поддержке ни в Swagger-UI, ни в Swagger-Codegen), но для HTTP RESTful API он просто божесвеннен, на мой взгляд, и я даже собрал демо на Flask-RESTplus (фреймворк для HTTP REST Swagger API) для более-менее жизненного примера, может что-то интересное для себя и в нём найдёте.


    Кстати, а ваш роутинг можно на модули разбивать, например, как Blueprint в Flask? Это гораздо удобнее, на мой взгляд, чем один общий router где-то там в корне проекта.


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


    1. Relrin
      08.12.2016 23:05
      +1

      Браво! Я просто не могу подобрать слов! Тема WebSockets в Python для меня была просто пыткой, сколько я ни пытался заставить себя погрузиться в неё, всё время какое-то отторжение происходило и я быстро находил на что отвлечься. aiorest-ws (по крайней мере по примерам и описанию) — это огромный шаг в сторону упрощения.

      Я бы сказал немного попроще – эксперимент. На текущий момент времени чего-то готового и работающего из коробки с веб-сокетами, в привычном стиле я так и не нашел (да и сейчас, вроде как ситуация не особо поменялась). Да, есть что-то вроде расширений (или плагинов) для того же Django REST, но немного не то, чего я ожидал бы увидеть.
      Вполне возможно, что существуют какие-то «закрытые» реализации подобных библиотек, которые имеют что-то схожее с тем, что я постарался описать, но не является open source. А жаль.

      Ваша реализация в стиле Flask мне импонирует, как и использование REST подхода. Swagger (OpenAPI) вам не факт, что поможет в плане какой-то готовой реализации (по крайней мере я не слышал о REST WebSockets поддержке ни в Swagger-UI, ни в Swagger-Codegen), но для HTTP RESTful API он просто божесвеннен, на мой взгляд, и я даже собрал демо на Flask-RESTplus (фреймворк для HTTP REST Swagger API) для более-менее жизненного примера, может что-то интересное для себя и в нём найдёте.

      Какого-то конкретного решения о структуре проекта (вроде тех, что предлагаются в Django или Flask) я не описывал. Пока просто выбирайте тот стиль что удобнее будет вам.
      Спасибо за ссылку, посмотрю на досуге. Вдруг чего-нибудь возьму на вооружение.

      Кстати, а ваш роутинг можно на модули разбивать, например, как Blueprint в Flask? Это гораздо удобнее, на мой взгляд, чем один общий router где-то там в корне проекта.

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

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

      Лишние трудности закаляют (надо же было себе какой-то челенж придумать), да и сделать что-то свое всегда интересно. Исходники библиотеки Marshmallow во многом хорошо подсказывали как решать ту или иную задачу при сериализации классов SQLAlchemy.


    1. VovanZ
      09.12.2016 01:43
      +2

      REST WebSockets
      Это оксюморон.


  1. Stronix
    09.12.2016 09:55

    Есть же JSON-RPC


    1. Relrin
      11.12.2016 14:05

      Хотя такая библиотека и есть, но нет необходимости её использовать, поскольку нам требуется лишь несколько полей. Быстрее (и проще) будет сделать что-то минималистичное самостоятельно.


      1. Stronix
        12.12.2016 09:15

        Я про протокол.


  1. estin
    09.12.2016 10:29

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

    Все же почему вебсокеты?


    1. Relrin
      11.12.2016 14:18

      Все просто. Если у тебя стоит задача получать данные в реальном времени, то при использовании HTTP чтобы получать свежие данные вы будете использовать опрос сервера с неким интервалом времени, что не очень удобно. В случае с веб-сокетами достаточно создать соединение с сервером один раз, подписаться на получение каких-то данных, и получать свежие данные от сервера без необходимости выполнять какой-либо long polling.


      1. estin
        12.12.2016 09:29

        В описанном вами кейсе я бы выбрал SSE (Server-Sent Events). И все свелось у меня к тому что запросы к серверу шли бы обычным образом без веб сокетов и прочих (тут много плюсов), а события по SSE каналу.


  1. VovanZ
    09.12.2016 12:20
    -4

    Простите, но я не могу не оставить это видео здесь.



  1. bosha
    10.12.2016 12:59

    Автор, тебе надо просто огромными буквами написать в github репозитарии, что твой фреймворк ни разу не асинхронный. Каждое обращение к БД в твоём фреймворке — это блокировка потока, а обращаться к БД получается надо будет почти всегда, и соответственно ты будешь блокировать event loop на каждый request.

    Есть aiopg который позволяет использовать query-builder алхимии, и никто не пытается примостить ORM алхимии до кучи к нему. Во первых — это не особо кому-то и нужно, во-вторых — это совсем не тривиальная задача.

    В общем случае, я бы на твоём месте прикрутил поддержку aiopg, переписал все твои вьюхи чтобы они сериализовали/десериалзовали объекты. Всю работу с БД (чтение, запись и т.д.) вынес бы в отдельные методы (корутины) которые надо было бы реализовывать пользователю библиотеки.


  1. zzzahon
    10.12.2016 14:52

    А как же tornado. Websockets и асинхронность.


    1. Relrin
      11.12.2016 14:12

      Выбор был основан в некоторой мере и на личных предпочтениях. С Tornado мне приходилось иметь дело, но предоставляемый API «из коробки» не очень нравится.


    1. kozzztik
      15.12.2016 10:11

      tornado с выходом asyncio несколько потерял актуальность.