Кракс! Миллениалы изобрели Python фреймворк

    Пролог


    Здравствуй, Хабр! Данная статья посвящена разбору плюсов и минусов очередного Python фреймворка, который увидел свет около недели назад.

    Итак, небольшое лирическое отступление. Во время всем известных событий, когда нас чуть-чуть самоизолировали, у нас появилось немножко больше свободного времени. Кто-то добрался до списка литературы, отложенной к прочтению, кто-то начал изучать ещё один иностранный язык, кто-то продолжал жать в дотан и не обратил внимание на перемены. Я же (простите, в этой статье будет очень много «Я», и мне немного стыдно) решился и попробовал сделать что-то полезное. Впрочем, о полезности можно поспорить. Очевидные вопросы, которые скорее всего возникнут у читателя в первую очередь: «Эм, Python framework? Ещё один? Простите, но зачем? Мы же не JavaScript, в конце концов!»

    Собственно, именно об этом и пойдёт речь в данной статье: Нужно ли это? Если нужно, то кому? В чём отличие от того, что уже есть? Чем это может быть привлекательно и почему, например, это можно похоронить, не дожидаясь первого дня рождения. В статье не планируется много кода — примеры написания приложения и использования отдельных частей можно найти в документации (там кода значительно больше ;) ). Данная статья носит скорее обзорный характер.

    Кому это нужно?


    Несколько эгоистичный ответ на этот вопрос — в первую очередь, разумеется, мне самому. Я имею определённый опыт в построении веб приложений с использованием существующих фреймворков и регулярно ловлю себя на мысли: «Да, всё классно, но вот если бы тут было вот так…. А тут вот эдак… .». Большинство из нас так или иначе рано или поздно сталкиваются с тем, что некоторые вещи не нравятся и хотелось бы (а то и придётся) их изменить. Я попробовал собрать вместе то, что мне нравится из инструментов, которые я использовал. Я надеюсь, что я не одинок в своих предпочтениях, и что найдутся люди, которым эти идеи покажутся близкими. Главная идея Crax — это то, что он максимально не навязывает какой то определённый стиль разработки. Например, нам не нужны неймспейсы, мы не хотим делить логику на приложения, мы хотим быстро развернуть два роута и погонять реквесты и респонсы. Ок, в этом случае мы можем просто создать single file application и получить желаемое. Но возможна и обратная ситуация, и это тоже не будет проблемой. Второе, что пропагандирует Crax — это простота. Минимум кода и минимум чтения документации для старта. Если с фреймворком планирует работать человек, который только начинает изучать Python, он должен быть в состоянии безболезненно преодолеть порог вхождения.

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

    Почему не использовать уже существующие инструменты?


    А почему бы и нет? Если Вы точно знаете какой инструмент использовать, что наиболее подходит для Вашей текущей задачи, да ещё и Вы работали с этим инструментом и знаете все нюансы. Разумеется, Вы выберете то, что Вам известно и подходит. Нет (и не будет) цели позиционировать Crax как «Убийца %framework_name%». Не будет агитации типа: «Выкиньте срочно %framework_name%, перепишите всё на Crax и сразу же заметно увеличится член количество продаж». Ничего подобного. Просто можно отметить для себя, что у Вас в наборе инструментов неделю назад появился ещё один. Использовать его, или нет — Ваше дело. Однако, почему стоит попробовать.

    Во-первых, он достаточно быстрый. Он написан с использованием интерфейса ASGI (читаем спецификацию тут) и он гораздо быстрее Flask или Django 1.*, 2.*. Но Crax, разумеется, не единственный Python framework, использующий ASGI, и предварительные тесты показывают, что он уверенно соревнуется с прочими фреймворками, использующими данную технологию. Для сравнения использовались тесты TechEmpower Performance Rating К сожалению, Crax как и прочие фреймворки, добавленные в середине текущего раунда, попадут только в следующий, и тогда можно будет наблюдать результаты в графической выдаче. Однако, после каждого пулл реквеста Travis прогоняет тесты и можно посмотреть сравнительную характеристику фреймворков в логе Travis. Ниже по ссылке длинная портянка лога Travis для Python фреймворков с названиями в алфавитном порядке от A до F Вот тут. Можно попробовать почитать лог и сравнить Crax, например, с apidaora, получится достаточно неплохо. Ниже на графике текущее положение вещей в Раунде 19 тестов.



    Разумеется, реальные результаты и реальную выдачу мы сможем посмотреть только в следующем раунде, но тем не менее.

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

    Допустим, Starlette или FastApi. Это совершенно потрясающие фреймворки с большим коммьюнити, которое заинтересовано в развитии этих продуктов. Стоит отметить, что Crax наиболее похож на Starlette или FastAPI по своей идеологии, а некоторые идеи были украдены подсмотрены в Starlette (например Response Middleware). Тем не менее, есть ряд моментов, которые могут понравиться в Crax, и заставить задуматься: «Может попробовать его для следующего проекта». Например файл конфигурации. Разумеется, у Starlette тоже есть возможность создания конфигурационного файла, но он несколько сложноват для начинающего, и в итоге его суть сводится к тому, что все переменные конфигурации в итоге всё равно передаются в инициализатор класса приложения. В случае, если собрать ВСЕ возможные переменные, например, настройку логгера, миддлвари, CORS и прочее, то получится многовато. В Crax все переменные объявлены в главном (конфигурационном) файле (на манер Django), и передавать их никуда не нужно. Более того, ко всем переменным, объявленным в конфигурационном файле, всегда есть возможность получить доступ в рантайме (как у работающего приложения, так и со стороны, привет Django).

    from crax.utils import get_settings_variable
    base_url = get_settings_variable('BASE_URL')
    

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

    Следующая важная деталь, о которой хотелось бы поговорить — это организация структуры приложения. Когда у Вас маленький проект, всю логику которого можно поместить в один файл — это одно. Но когда Вы пишете что то более глобальное, Вы, возможно, хотели бы разделить представления, модели, описания роутов и прочее, согласно их логике. В этом контексте нам приходят в голову прекрасные Flask blueprints или Django applications. Crax рассуждает в этом смысле о неймспейсах. Изначально задумано, что Ваше приложение — это

    набор python packages, которые подключены в основном файле проекта. Кстати, неймспейсы (ваши части приложения) могут быть рекурсивно вложенные (привет, Flask), а наименования файлов в них не имеют значения. Зачем так делать? И что нам это даёт?

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

    from crax.urls import Route, Url, include
    
    url_list = [
        Route(Url('/'), Home),
        Route(Url('/guest_book'), guest_view_coroutine),
        include('second_app.urls'),
        include('second_app.nested.urls'),
        include('third_app.urls')
    ]
    

    Замените точки на слэши и Вы получите uri до Вашего неймспейса (естественно, добавив конечный хендлер). Раз уж мы упомянули роутинг, то остановимся на нём чуть подробнее.
    Crax предлагает пару любопытных возможностей, помимо привычной работы с регулярными выражениями или работы via Django path.

    # URL defined as regex with one floating (optional) parameter
    Url(r"/cabinet/(?P<username>\w{0,30})/(?:(?P<optional>\w+))?", type="re_path")
    # General way to define URL
    Url("/v1/customer/<customer_id>/<discount_name>/")
    

    Однако, существует возможность привязать к одному хендлеру несколько Url.

    from crax.urls import Route, Url
    
    class APIView(TemplateView):
        template = "index.html"
    
    urls = [
        Route(
            urls=(
                Url("/"),
                Url("/v1/customers"),
                Url("/v1/discounts"),
                Url("/v1/cart"),
                Url("/v1/customer/<customer_id:int>"),
                Url("/v1/discount/<discount_id:int>/<optional:str>/"),
            ),
            handler=APIView)
        ]
    

    Вы сами можете придумать, где Вам это может быть полезно. А так же, есть режим работы резолвера в режиме «маскарадинга». Например, Вы желаете просто раздавать какую-то директорию с шаблонами, и не желаете ничего более. Возможно, это документация Sphinx, или что-то подобное. Вы всегда можете сделать так:

    import os
    from crax.urls import Url, Route
    
    class Docs(TemplateView):
        template = 'index.html'
        scope = os.listdir('docs/templates')
    
    URL_PATTERNS = [
        Route(urls=(
            Url('/documentation', masquerade=True),
            handler=Docs),
    ]
    

    Отлично, теперь все шаблоны, которые находятся в каталоге docs/templates, будут успешно отрендерены с использованием одного хендлера. Пытливый читатель скажет, что тут вообще не нужен никакой питон, и можно это всё сделать только силами условного Nginx. Абсолютно согласен, ровно до той поры пока не придётся, например, раздавать эти шаблоны по ролям или где-то сбоку не потребуется дополнительная логика.

    Однако, вернёмся к нашим баранам неймспейсам. Было бы очень грустно, если бы неймспейсы (пусть и вложенные) были бы нужны только для того чтобы организовать url resolving. Разумеется, назначение неймспейсов чуть шире. Например, работа с моделями баз данных и миграциями.

    В Crax нет ORM. И не предполагается. Во всяком случае до тех пор, пока SQLAlchemy не предложит асинхронных решений. Тем не менее, работа с базами данных (Postgres, MySQL и SQLite) заявлена. Это значит, что есть возможность писать свои модели на основе Crax BaseTable. Под капотом — это очень тоненькая обёртка над SQLAlchemy Core Table, и умеет всё, что умеет Core Table. Для чего она может быть нужна. Возможно, чтобы делать что-то похожее.

    from crax.database.model import BaseTable
    import sqlalchemy as sa
    
    class BaseModelOne(BaseTable):
        # This model just passes it's fields to the child
        # Will not be created in database because the abstract is defined
        parent_one = sa.Column(sa.String(length=50), nullable=False)
    
        class Meta:
            abstract = True
    
    class BaseModelTwo(BaseTable):
        # Also passes it's fields to the child
        # Will be created in database
        parent_two = sa.Column(sa.String(length=50), nullable=False)
    
    class MyModel(BaseModelOne, BaseModelTwo):
        name = sa.Column(sa.String(length=50), nullable=False)
    
    print([y.name for x in MyModel.metadata.sorted_tables for y in x._columns])
    # Let's check our fields ['name', 'id', 'parent_one', 'parent_two']
    

    И для того чтобы иметь возможность работать с миграциями. Миграции Crax — это немного кода поверх SQLAlchemy Alembic. Раз уж мы говорим о неймспейсах и разделении логики, то,
    очевидно, хотелось бы хранить миграции в том же пакете, что и прочая логика данного нэймспейса. Именно так и работают миграции Crax. Все миграции будут распределены согласно их неймспейса, а если в данном неймспейсе подразумевается работа с различными базами данных, то внутри каталога миграций будет разделение на каталоги соответствующих баз. Это же касается и миграций в оффлайн режиме — все *.sql файлы будут разделены согласно неймспейса и базы данных модели. Не буду здесь расписывать про составление запросов — это есть в документации, скажу только, что Вы по прежнему продолжаете работать с SQLAlchemy Core.

    Опять же, неймспейсы подразумевают удобное хранение шаблонов (наследование и прочие Jinja2 возможности поддерживаются + пара приятностей в виде уже готовых CSRF токенов или генерации url). То есть, все Ваши шаблоны структурированы. Ну, конечно же, я не застрял в славном 2007 году, я понимаю, что шаблоны (пусть даже которые рендерятся асинхронно) будут мало востребованы в 2020-м. И что, скорее всего, Вы изволите разделить логику frontend и backend. Crax отлично справляется с этой задачей, результаты можно посмотреть на Github.
    Здесь в качестве фронтенда использован VueJs. А раз у нас есть какой то API, вероятно мы захотим сделать интерактивную документацию. Crax умеет из коробки строить OpenAPI (Swagger) документацию, основанную на Ваших списках роутов и докстрингах Ваших хендлеров. Все примеры, разумеется, есть в документации.

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

    Естественно, режим отладки — это когда ошибку и полный трейс можно читать прямо в браузере, на странице, где случилось несчастье. Режим отладки можно отключить, и кастомизировать нескучными обоями своими хендлерами. Вывод уникального представления для каждого http status code. Делается это очень просто, впрочем, как и всё в Crax.

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

    Два типа предустановленных миддлварей. Первый отрабатывается ДО того, как реквест будет обработан приложением, а второй ПОСЛЕ.

    Встроенная поддержка CORS headers. Нужно только в конфиге объявить правила CORS.
    Возможность определять доступные для каждого хендлера методы непосредственно на месте. Каждый хендлер будет работать со списком HTTP методов, который задан (+ HEAD и OPTIONS), либо только с GET, HEAD и OPTIONS.

    Возможность указания, что данный хендлер доступен только для авторизованных пользователей либо только пользователей из группы администраторы, либо только для членов роли superuser.
    Есть авторизация на HMAC подписанных сессиях, за которыми не нужно лезть в базу данных и ряд инструментов создания и управления пользователями. Можно включить поддержку бэкенда авторизации и получить предустановленного пользователя и ряд инструментов для работы. Впрочем, как и большинство инструментов Crax, это можно не включать, не использовать и писать своё. Можно не использовать авторизацию, базы данных, модели, миграции, представления и полностью писать свои кастомные решения. Для этого не нужно прилагать никаких усилий, Вы это не включили — этого нет.

    Есть несколько типов Response и несколько типов Class Based хендлеров, которые помогут писать приложения быстрее и лаконичнее. При этом будут работать и Ваши собственные, которые не наследуются от встроенных.

    from crax.views import BaseView
    
    # Written your own stuff
    class CustomView:
        methods = ['GET', 'POST']
        def __init__(self, request):
            self.request = request
        async def __call__(self, scope, receive, send):
            if self.request.method == 'GET':
                response = TextResponse(self.request, "Hello world")
                await response(scope, receive, send)
            elif self.request.method == 'POST':
                response = JSONResponse(self.request, {"Hello": "world"})
                await response(scope, receive, send)
    
    # Crax based stuff
    class CustomView(BaseView):
        methods = ['GET', 'POST']
        async def get(self):
            response = TextResponse(self.request, "Hello world")
            return response
    
        async def post(self):
            response = JSONResponse(self.request, {"Hello": "world"})
            return response
    
    class CustomersList(TemplateView):
        template = 'second.html'
    
        # No need return anything in case if it is TemplateView.
        # Template will be rendered with params
        async def get(self):
            self.context['params'] = self.request.params
    

    Поддержка CSRF protection. Генерация токенов, проверка наличия токена в теле запроса,
    отключение проверки для конкретных хендлеров.

    Поддержка ClickJacking protection (Политики отрисовки frame, iframe, embed...)

    Поддержка проверки максимально допустимого размера body запроса ДО того, как приложение его начнёт обрабатывать.

    Нативная поддержка вебсокетов. Давайте возьмём пример из документации и напишем простенькое приложение, которое может отправлять вебсокет сообщения бродкастом, по группам пользователей или сообщения конкретному пользователю. Предположим, у нас есть группы «мальчишки» и «девочки» (есть возможность добавить группу «родители»). Мы можем написать для примера (разумеется, это не продуктовый код) что то похожее.

    #app.py
    
    import asyncio
    import json
    import os
    from base64 import b64decode
    from functools import reduce
    
    from crax.auth import login
    from crax.auth.authentication import create_session_signer
    from crax.auth.models import Group, UserGroup
    from crax.response_types import JSONResponse
    from crax.urls import Route, Url
    from crax.views import TemplateView, WsView
    from sqlalchemy import and_, select
    from websockets import ConnectionClosedOK
    
    BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    SECRET_KEY = "SuperSecret"
    MIDDLEWARE = [
        "crax.auth.middleware.AuthMiddleware",
        "crax.auth.middleware.SessionMiddleware",
    ]
    
    APPLICATIONS = ["ws_app"]
    CLIENTS = {'boys': [], 'girls': []}
    
    
    class Home(TemplateView):
        template = "index.html"
        login_required = True
    
    
    class Login(TemplateView):
        template = "login.html"
        methods = ["GET", "POST"]
    
        async def post(self):
            credentials = json.loads(self.request.post)
            try:
                await login(self.request, **credentials)
                if hasattr(self.request.user, "first_name"):
                    context = {'success': f"Welcome back, {self.request.user.username}"}
                    status_code = 200
                else:
                    context = {'error': f"User or password wrong"}
                    status_code = 401
            except Exception as e:
                context = {'error': str(e)}
                status_code = 500
            response = JSONResponse(self.request, context)
            response.status_code = status_code
            return response
    
    
    class WebSocketsHome(WsView):
    
        def __init__(self, request):
            super(WebSocketsHome, self).__init__(request)
            self.group_name = None
    
        async def on_connect(self, scope, receive, send):
            # This coroutine will be called every time a client connects.
            # So at this point we can do some useful things when we find a new connection.
    
            await super(WebSocketsHome, self).on_connect(scope, receive, send)
            if self.request.user.username:
                cookies = self.request.cookies
                # In our example, we want to check a group and store the user in the desired location.
    
                query = select([Group.c.name]).where(
                    and_(UserGroup.c.user_id == self.request.user.pk, Group.c.id == UserGroup.c.group_id)
                )
                group = await Group.query.fetch_one(query=query)
                self.group_name = group['name']
    
                # We also want to get the username from the user's session key for future access via direct messaging
    
                exists = any(x for x in CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0])
                signer, max_age, _, _ = create_session_signer()
                session_cookie = b64decode(cookies['session_id'])
                user = signer.unsign(session_cookie, max_age=max_age)
                user = user.decode("utf-8")
                username = user.split(":")[0]
                val = {f"{cookies['session_id']}:{cookies['ws_secret']}:{username}": receive.__self__}
    
                # Since we have all the information we need, we can save the user
                # The key will be session: ws_cookie: username and the value will be an instance of uvicorn.WebSocketProtocol
    
                if not exists:
                    CLIENTS[self.group_name].append(val)
                else:
                    # We should clean up our storage to prevent existence of the same clients.
                    # For example due to page reloading
                    [
                        CLIENTS[self.group_name].remove(x) for x in
                        CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0]
                    ]
                    CLIENTS[self.group_name].append(val)
    
        async def on_disconnect(self, scope, receive, send):
            # This coroutine will be called every time a client disconnects.
            # So at this point we can do some useful things when we find a client disconnects.
            # We remove the client from the storage
    
            cookies = self.request.cookies
            if self.group_name:
                try:
                    [
                        CLIENTS[self.group_name].remove(x) for x in
                        CLIENTS[self.group_name] if cookies['session_id'] in list(x)[0]
                    ]
                except ValueError:
                    pass
    
        async def on_receive(self, scope, receive, send):
            # This coroutine will be called every time we receive a new incoming websocket message.
            # Check the type of message received and send a response according to the message type.
    
            if "text" in self.kwargs:
                message = json.loads(self.kwargs["text"])
                message_text = message["text"]
                clients = []
                if message["type"] == 'BroadCast':
                    clients = reduce(lambda x, y: x + y, CLIENTS.values())
    
                elif message["type"] == 'Group':
                    clients = CLIENTS[message['group']]
    
                elif message["type"] == 'Direct':
                    username = message["user_name"]
                    client_list = reduce(lambda x, y: x + y, CLIENTS.values())
                    clients = [client for client in client_list if username.lower() in list(client)[0]]
                for client in clients:
                    if isinstance(client, dict):
                        client = list(client.values())[0]
                        try:
                            await client.send(message_text)
                        except (ConnectionClosedOK, asyncio.streams.IncompleteReadError):
                            await client.close()
                            clients.remove(client)
    
    
    URL_PATTERNS = [Route(Url("/"), Home), Route(Url("/", scheme="websocket"), WebSocketsHome), Route(Url("/login"), Login)]
    DATABASES = {
            "default": {
                "driver": "sqlite",
                "name": f"/{BASE_URL}/ws_crax.sqlite",
            },
        }
    app = Crax('ws_app.app')
    
    if __name__ == "__main__":
        if sys.argv:
            from_shell(sys.argv, app.settings)
    


    <!-- index.html -->
    
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Crax Websockets</title>
        </head>
        <body>
            <div id="wsText"></div>
            <form>
                <input id="messageText"><br>
                <select id="targetGroup">
                    <option>boys</option>
                    <option>girls</option>
                </select>
                <select id="messageType">
                    <option>BroadCast</option>
                    <option>Group</option>
                    <option>Direct</option>
                </select>
                <select id="userNames">
                    <option>Greg</option>
                    <option>Chuck</option>
                    <option>Mike</option>
                    <option>Amanda</option>
                    <option>Lisa</option>
                    <option>Anny</option>
                </select>
            </form>
            <a href="#" id="sendWs">Send Message</a>
            <script>
                var wsText = document.getElementById("wsText")
                var messageType = document.getElementById("messageType")
                var messageText = document.getElementById("messageText")
                var targetGroup = document.getElementById("targetGroup")
                var userName = document.getElementById("userNames")
                var sendButton = document.getElementById("sendWs")
                ws = new WebSocket("ws://127.0.0.1:8000")
                ws.onmessage = function(e){
                    wsText.innerHTML+=e.data
                }
    
                sendButton.addEventListener("click", function (e) {
                    e.preventDefault()
                    var message = {type: messageType.value, text: messageText.value}
                    var data
                    if (messageText.value !== "") {
                        if (messageType.value === "BroadCast"){
                            // send broadcast message
                            data = message
                        }
                        else if (messageType.value === "Group"){
                            // send message to group
                            data = Object.assign(message, {group: targetGroup.value})
                        }
                        else if (messageType.value === "Direct"){
                            // send message to certain user
                            data = Object.assign(message, {user_name: userName.value})
                        }
                        ws.send(JSON.stringify(data))
                    }
                })
            </script>
        </body>
        </html>
    

    <!-- login.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Crax Websockets</title>
    </head>
    <body>
        <form>
            <input id="username">
            <input id="password" type="password">
        </form>
        <div id="loginResults"></div>
        <a href="#" id="sendLogin">Login</a>
    
        <script>
            var loginButton = document.getElementById("sendLogin")
            var loginResults = document.getElementById("loginResults")
            var username = document.getElementById("username")
            var password = document.getElementById("password")
            loginButton.addEventListener("click", function (e) {
                e.preventDefault()
                if (username.value !== "" && password.value !== "") {
                    var xhr = new XMLHttpRequest()
                    xhr.overrideMimeType("application/json")
                    xhr.open("POST", "/login")
                    xhr.send(JSON.stringify({username: username.value, password: password.value}))
                    xhr.onload = function () {
                        var result = JSON.parse(xhr.responseText)
                        if ("success" in result){
                            loginResults.innerHTML+="<h5 style='color: green'>"+result.success+ "</h5>"
                        }
                        else if ("error" in result) {
                            loginResults.innerHTML+="<h5 style='color: red'>"+result.error+ "</h5>"
                        }
                    }
                }
            })
        </script>
    </body>
    </html>
    

    Полный код можно посмотреть в документации Crax.

    Ну и настало время самого интересного в этой статье.

    Почему это не нужно?


    Во-первых, как уже говорилось выше, существует несколько фреймворков, делающих то же самое, и обладающих уже сформировавшимся коммьюнити. В то время как Crax — это младенец, которому неделя от роду. Single man army — это почти гарантия того, что рано или поздно проект будет заброшен. Печально, но факт, что работать в стол, выпуская релизы и обновления только для себя и Василия из Сыктывкара — это значительно дольше, чем когда над проектом работает коммьюнити. Тем временем, в проекте нет ряда фич, которые маст хэв в 2020 году. Например: нет поддержки JWT(JOSE). Нет поддержки из коробки инструментов для работы с OAuth2. Нет поддержки GraphQL. Понятно, что это можно написать самому для своего проекта, но в Starlette или FastAPI это уже есть. Мне же только предстоит это писать (да, это есть в планах). О планах будет немного в заключении.

    О FastAPI пишут разработчики Netflix и Microsoft. О Crax пишет нонейм, не известно откуда появившийся, и неизвестно куда способный в аккурат послезавтра пропасть.

    Моим именем идиотским не назовут парохода
    Моя мама ночами плачет, ведь она родила урода…
    (с)

    Это важно. Это называется — репутация и экосистема. У Crax нет ни того ни другого. Без этих важных вещей проект гарантировано отправится на свалку, так и не родившись.

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

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

    The Goals


    Первое, что я планирую сделать — это, конечно же, дописать маст хэв вещи, такие как поддержку JWT(JOSE), OAuth2 и GraphQL. Это то, что позволит работать мне и заинтересованным людям проще. А это, собственно, главная цель Crax — сделать кому-то работу чуть проще. Возможно, к тому времени начнётся новый раунд на TechEmpower и бенчмарки станут более очевидными. Возможно даже, что после этого появится определённый интерес у сообщества.
    Существует идея на базе Crax написать CMS.
    Если я не ошибаюсь (если ошибаюсь — поправьте), у нас пока нет в инструментарии ни одной асинхронной CMS на питоне. Я могу передумать и решить написать какое-то e-commerce решение. Но, очевидно, что для того, чтобы Crax не утонул, не доплыв до буйков, нужно сделать на его базе что-то интересное. Возможно, этим заинтересуются энтузиасты. Энтузиасты — это когда бесплатно. Потому что денег тут нет и, скорее всего, не будет. Crax — это совершенно бесплатно для всех и я за эту работу не получил ни цента. Таким образом, разработка планируется «долгими зимними вечерами» и, возможно, в грядущем году что-то интересное появится на свет.

    Заключение


    Я размышлял о том, в какую группу отнести данную статью (это, кстати, моя первая публикация на ресурс). Может даже стоило это разместить под тегом «Я пиарюсь». Что заставило передумать: в первую очередь то, что это не имеет рекламного характера чего бы то ни было.

    Здесь нет призыва «Мальчики, срочно записываемся на пулл реквестики». Здесь нет идеи найти спонсора. Здесь даже нет идеи о том, что я принёс Вам то, чего Вы никогда не видели (естественно, видели). Можно абстрагироваться от мысли, что я автор и статьи и этого инструмента, и воспринимать написанное как обзор. И, да, так будет лучше всего. Для меня будет прекрасным результатом если Вы просто будете иметь в виду, что это есть.
    На этом, пожалуй, всё.

    — Так… пора сматывать удочки.
    — Почему?
    — Красная кепка Харриса распугала всю рыбу.
    (с)

    Код на GitHub
    Документация

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 12

      0
      Take a look at a simple bash script below.

      А почему не сразу на питоне скриптовать? Да и в принципе какой-нибудь простенький менеджер всего этого безобразия консольный написать. crax init, crax serve, crax migrate и прочее в том же духе. Батарейки в комплекте обычно добавляют плюс к размеру комьюнити.


      __init__.py

      Я же правильно понимаю что проект завязан на Python 3+. Оно вроде с третьей версии не требует скрипта инициализации, если это не фолбэк во вторую версию. Поправьте если не прав.

        0
        Есть инструменты работы с командной строкой. Об этом рассказывается в соответствующих главах документации. Но вызов происходит путём python app.py command_name. В основном команды касаются работы с бд. Например python app.py makemigrations, python app.py migrate и прочее. Скриптов создания пустого проекта или неймспейса пока нет. Нет, Вы совершенно правы, начиная с 3.3 пакеты можно без init.py
          0
          Оно вроде с третьей версии не требует скрипта инициализации, если это не фолбэк во вторую версию. Поправьте если не прав.

          Зависит от того, что вы хотите получить. Разница между namespace и regular packages всё же есть. См. PEP-0420


          Пустые __init__ действительно можно выкинуть.

            0
            Забавно, что некоторые модули до сих пор требуют init. Например, unittest
            python3 -m unittest discover
            не находит тесты, если на пути до них не будет везде __init__.py
            +3

            Посмотрел исходники.
            Вы никогда не обращали внимание на то, что в других, "взрослых" фреймворках HTTP-заголовки представлены не совсем обычным питонячим словарём? Обычно это нечто называемое, как минимум, MultiDict (а в идеале там ещё и case-insensetive сравнение ключей).
            И ведь не просто так ASGI передаёт приложению заголовки в виде списка тюплов. А вы довольно смело берёте и превращаете этот список в самый обычный словарик.
            Думается мне, что вас ждёт ещё много открытий на вашем пути. И с каждым новым открытием ваш фреймворк скорее всего будет становиться медленнее и медленнее.

              0
              Спасибо, что интересуетесь.
              Положа руку на сердце, в проекте достаточно «детских» решений, которые ещё придётся изменять и дополнять.
              Касаемо преобразования list of tuples в словарик. Мы, естественно, преобразуем ASGI scope
              (https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope). Я, разумеется, читал (https://github.com/encode/starlette/blob/c566fc6c819f0d565f8cff432351fe009e83d866/starlette/datastructures.py#L487). И, несомненно, у этого есть высокая цель. Я даже подозреваю какая: Объект заголовков должен быть неизменяемым на момент всего выполнения запроса каким бы долгим он ни был — это первое. Второе — «Duplicates are possible and must be preserved in the message as received». Именно поэтому Ваше сообщение вызывает уважение. Я размышлял очень много над объектом реквеста (он практически неизменен с первых строк проекта), и я хочу что-то делать не потому что «А у взрослых дяденек так», а потому что так действительно стоит делать. В данном конкретном случае ТАК действительно нужно делать только потому, что так говорит ASGI спецификация. Как именно это выполнить, я пока не знаю.
              Что я имею в виду: Объект реквеста, с предоставляемыми атрибутами может быть изменён приложением во время выполнения. При этом объект ASGI scope останется неизменным. Опасность заключается в том, что эти объекты могут не совпадать. Я понимаю риск. Я понимаю, что объект реквеста, который передаётся в течение всего цикла запроса есть желание изменить. Для этих целей, например, у Starlette есть State.
              Я не уверен, как делать это в Crax.
                +4

                Если что, то это не ASGI говорит, это спека на HTTP говорит, что может быть несколько заголовков с одинаковым именем. И это нормальная ситуация.

                  –1
                  Совершенно верно. Значения заголовков с одинаковыми названиями МОГУТ быть объединены в один. В том случае, если значения представляют собой список значений, разделённый запятой. Во всех остальных случаях последнее значение «перекрывает» предыдущее. Текущая ситуация может породить похожий тред:
                  github.com/nodejs/node/issues/3591
              0
              При разговоре про конфигурацию хочется сослаться на pydantic.
                0
                Нам точно нужна +1 зависимость чтобы писать конфиг?
                  0

                  Иногда да, иногда нет. Я в последнее время использовал fastapi, а с ним pydantic. Конфиги пришли приятным бонусом.
                  А до этого мы в проектах использовали envparse для чтения переменных окружения (я знаю про os.environ), так что все равно была +1 зависимость.

                +1
                Single man army — это почти гарантия того, что рано или поздно проект будет заброшен.


                Мне нравится «почти». «Можете со мной поспорить и переспорить, но это не будет считаться, потому что я сказал волшебное слово „почти“. „

                И вывод странный. Все до одного фреймворки в любом языке когда-то были single man army. А вы как хотели, чтобы сразу сообщество выросло в первый месяц-год?

                Что-то забросили, а что-то выросло.

                Only users with full accounts can post comments. Log in, please.