Сказ о том как я свой REST фреймворк с веб-сокетами писал
Для тех, кому интересна эта статья — пожалуйста, заходите под кат.
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 "" % (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 "" % 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.
Документацию можно посмотреть здесь.
Комментарии (7)
8 декабря 2016 в 22:18
0↑
↓
Зачем Вы указываете кодировку utf8 в заголовке файла? Проект же под третий питон? Не смог прочесть ни одного обзаца целиком: ощущение, что читаю слегка очеловеченный машинный перевод. То, что успел понять, не раскрыло для меня премуществ асинхронности против синхронности.8 декабря 2016 в 22:45
+1↑
↓
Зачем Вы указываете кодировку utf8 в заголовке файла? Проект же под третий питон?
Да, проект написан под 3ку. Относительно наличия «utf8» в заголовке каждого файла могу лишь сказать что это банальная привычка. На работе (наверное, как и многие) все также пишу на 2ой ветке.
8 декабря 2016 в 22:26
+1↑
↓
Нельзя вызывать блокирующий синхронный код (`.save ()` и т.д.) из асинхронного.Точнее, это можно делать пока у вас количество пользователей не больше десятка-двух.
Потом всё начнёт залипать.
8 декабря 2016 в 22:41 (комментарий был изменён)
+1↑
↓
В целом, согласен с вышесказанными. Этот момент реально стоит мне как-то продумать более детально, поскольку мы упираемся в количество имеющихся воркеров.
Были идеи сделать что-то в связке с aiopg, чтобы появилась возможность работы асинхронно с БД (хоть только и PostgreSQL). Правда возникает вполне простой вопрос:, а как мне можно перейти в асинхронный код при работе aiorest-ws с Django, у которого множества различных адаптеров есть под разные базы?
Текущее решение не идеально, на текущий момент, но хоть есть от чего отталкиваться :)8 декабря 2016 в 22:52
0↑
↓
В тему вебсокетов, столкнулся с реализацией клиента на 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()
Или это можно сделать более «красиво»?
8 декабря 2016 в 22:30 (комментарий был изменён)
0↑
↓
4) Здорово, теперь кое что прояснилось.
И после этого читать не получается.)
Потому что не прояснилось и потому что тяжело и лениво вникать.Для кого и для чего материал?
Если это тутор, то на какой уровень подготовки?Если это описание фреймворка, то слишком абстрактный пример и много «воды».
Если это подход к построению фреймворка, то слишком много ненужных в этом случае деталей…8 декабря 2016 в 22:41 (комментарий был изменён)
0↑
↓
Браво! Я просто не могу подобрать слов! Тема 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 вместо нового велосипеда? Неужели требование к минимальности зависимостей настолько строгое?