Привет! Меня зовут Никита Соболев, я core-разработчик языка программирования CPython, а так же core-разработчик фреймворка Litestar, пакета django-stubs и множества других пакетов для Django.

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

Если хотите похоливарить в коментах на тему того, какой фреймворк самый лучший и удобный – залетайте! Обсудим.


Перед стартом

TLDR:

Основные фичи:

  • Скорость! Самый быстрый REST на Django западе: https://django-modern-rest.readthedocs.io/en/latest/pages/deep-dive/performance.html Всего на 30% медленнее FastAPI, без учета походов в базу, что сравняет производительность

  • Поддержка pyndatic2, msgspec, attrs, dataclasses, TypedDict, NamedTuple и множества других типов в качестве моделей. Мы не привязываемся к конкретной библиотеке и сериализатору, а даем пользователям выбор на уровне индивидуального контроллера. А еще – можно писать свои, хоть сделать свой dmr-drf-serializer, который бы использовал существующие сериализаторы от DRF, например, при миграции кода. Ограничений никаких

  • Поддержка sync и async режимов

  • Просто обычная Django. У нас нет никаких уникальных абстракций, а Controller - просто подкласс View. Все существующие плагины должны работать с нашей библиотекой

  • Полная типизация. Типизировано всё и везде. Но типизация не заставляет вас прыгать вокруг неё, а помогает вам писать более корректный и понятный код

  • Content negotiation. Поддерживаем json / msgpack из коробки для обычных ответов и SSE / JsonLines для стриминга

  • Строгая и семантическая схема запросов и ответов. У нас в dev режиме идет строгая валидация ответов: если какого-то статуса, заголовка или поля в спеке нет, то мы падаем с ошибкой валидации. Что позволяет нам всегда держать схему точной и корректной для наших клиентов

  • Поддержка OpenAPI 3.1 и 3.2 из коробки. State of the Art генерация схемы

  • Легко переиспользовать код: мы делаем все на базе контроллеров-классов, архитектура позволяет легко менять форматы ответа и запроса, сериализаторы и другие части классов. Что позволяет с легкостью, удобством и типизацией писать плагины для DMR

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

  • Крутые инструменты для тестирования: мы используем и предоставляем интеграцию со schemathesis, для property-based тестов вашего АПИ. Одного такого теста достаточно, чтобы покрыть 90% вашего кода. Polyfactory позволяет вам с легкостью генерировать тестовые данные для тестирования ручек АПИ, а наш pytest плагин - делает весь процесс очень удобным. А еще мы поддерживаем стандартные практики django.test пакета, если у вас уже так тесты написаны

  • Никакого ИИ слопа. Весь код внимательно прочитан, замерен, типизирован и документирован. Однако, мы позиционируем сам фреймворк как AI-first. Мы представляем множество фичей: llms-full.txt, Context7, DeepWiki, скилы для агентов и плагины для Claude, которые бы помогали нашим пользователям в корректном использовании нашей библиотеки с LLMками, если они ими уже пользуются. Мы даже публикуем все ломающие изменения с промтами по исправлению кода у пользователей

А теперь подробно!

Зачем?

Главный вопрос, который должен волновать каждого хорошего инженера.

Есть несколько главных причин. Давайте разберем все по-порядку.

Первая причина в самой Django. Я пользуюсь ей уже скоро почти 15 лет, знаю каждый модуль, каждую фичу, каждый плагинчик. И оно работает. Работает хорошо, стабильно, держит умеренную нагрузку (для больших нагрузок я бы взял Rust или Elixir). Имеет тысячи плагинов на любой вкус и цвет

У меня есть целый огромный репозиторий с шаблоном идеального Django приложения https://github.com/wemake-services/wemake-django-template на 2200+ звезд, где содержится весь мой наработанный опыт. Там есть буквально всё: от кучи фичей безопасности, до DX инструментов для контроля за N+1 запросами и корректной генерации тестовых данных.

Но чего в Django никогда не было – так нормального REST фреймворка. Я застал еще https://github.com/django-tastypie/django-tastypie, но сейчас есть два основных способа делать REST: django-rest-framework и django-ninja.

Я встречал довольно много людей, кто говорит что очень не любит Django. Мне всегда было интересно: а почему? Три основных пункта, которые мне говорили люди:

  1. Мы настрадались с DRF. И тут их можно понять. Там просто вагон плохих решений. Дизайн с одним сериализатором для ответов и запросов, да, его можно менять через костыли на разные, но все равно больно. Привязка генерации АПИ к полям модели, что приводит к регулярным багам, когда ты добавил внутреннее поле в модель, а оно утекло пользователям. Полное отсутствие типизации: как типизировать request.data? Никак. Нет встроенной OpenAPI спеки. Приходится все время использовать какие-то багованные библиотеки от других людей: то drf-yasg, то drf-spectacular, то еще что-то. Бизнес логика, которая протекает в ModelSerializer.{create, update} Ужас! К сожалению django-ninja не стал нормальной заменой. Во-первых, они решили основывать свой API не на Django, а на FastAPI. И сделали API через функции, что убивает любую возможность переиспользовать код. Ведь функции нельзя наследовать и менять. И еще она очень плохо написана внутри. Ну и конечно, они прибили pydantic гвоздями, не дали нормальной кастомизации OpenAPI схемы, а JWT аутентификация вообще поставляется как форк какой-то древней DRF поделки в виде плагина поверх django-ninja-extra, который почти не поддерживается. Что?

  2. Проблемы с архитектурой. И тут можно частично согласиться. Документация Django писалась в середине и конце нулевых. Тогда было модно делать толстые модели, пихать всю бизнес логику во views.py, сейчас все поменялось. Но не документация. Однако, никто не заставляет вас так делать. Я не пишу логику в моделях и вьюхах. И вы тоже не пишите. И тогда проблем не будет. Посмотрите, как в wemake-django-template организована бизнес логика. Просто и расширяемо до очень больших и концептуально сложных приложений

  3. Django - сложная! Такое я слышал очень много раз. Я просто покажу код:

Минимальное приложение на Django + django-modern-rest
import secrets
import sys
import uuid

import pydantic
from django.conf import settings
from django.core.management import execute_from_command_line
from django.urls import include

from dmr import Body, Controller
from dmr.openapi import build_schema
from dmr.openapi.views import OpenAPIJsonView, SwaggerView
from dmr.plugins.pydantic import PydanticSerializer
from dmr.routing import Router, path

if not settings.configured:
    settings.configure(
        ROOT_URLCONF=__name__,
        ALLOWED_HOSTS='*',
        DEBUG=True,
        INSTALLED_APPS=['dmr', 'django.contrib.staticfiles'],
        STATIC_URL='/static/',
        STATICFILES_FINDERS=[
            'django.contrib.staticfiles.finders.AppDirectoriesFinder',
        ],
        TEMPLATES=[
            {
                'APP_DIRS': True,
                'BACKEND': 'django.template.backends.django.DjangoTemplates',
            },
        ],
        # Secret key for tests, will be new on each run,
        # in production it must be the same token, kept in secret:
        SECRET_KEY=secrets.token_hex(),
    )


class UserCreateModel(pydantic.BaseModel):
    email: str


class UserResponseModel(UserCreateModel):
    uid: uuid.UUID


class UserController(Controller[PydanticSerializer]):
    async def post(
        self,
        parsed_body: Body[UserCreateModel],
    ) -> UserResponseModel:
        return UserResponseModel(uid=uuid.uuid4(), email=parsed_body.email)


router = Router(
    'api/',
    [
        path('user/', UserController.as_view(), name='users'),
    ],
)
schema = build_schema(router)

urlpatterns = [
    path(router.prefix, include((router.urls, 'your_app'), namespace='api')),
    path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
    path('docs/swagger/', SwaggerView.as_view(schema), name='swagger'),
]

if __name__ == '__main__':
    # Use `python THIS_FILE_NAME.py runserver` to run the example.
    # Then visit `http://localhost:8000/docs/swagger` to view the docs.
    execute_from_command_line(sys.argv)

А вот и пример нашего минимального приложения. ^^^

Можно ли назвать его сложным? Кажется, что - нет. Даже при очень большом желании. За 71 строку мы с вами создали контроллер, модели, роутинг, сгенерировали OpenAPI спеку, добавили 2 вьюхи для OpenAPI, запустили приложение.

Но в отличии от микро-фреймворков вроде FastAPI - вам не нужно думать про расширение приложения в будущем. Потому что все паттерны уже есть, все уже описано. И приложение может расти вместе с потребностями по заранее известным паттернам.

Есть ли у вас другие причины не любить Django? Поделитесь ими в комментах.

Вторая главная причина создания фреймворка в самом Python. Питон меняется. Добавление Free-Threading сделало концепцию asyncio - значительно менее актуальной. А возможно и "устаревшей".

Не верите мне? Вот цитаты уважаемых людей - других core-разрботчиков CPython:

I think that putting asyncio in the stdlib was a mistake. It froze the API and the implementation, making it much harder for alternatives (like Trio or Curio) to compete. If it had been a third-party library, it could have evolved (or died) on its own merits.

— Guido van Rossum

Virtual threads are a better way of doing concurrency than Python’s async and wait. We should add virtual threads to Python.

IMO, virtual threads offer a superior programming model to adding async and await all over your code and having to duplicate all your libraries.

— Mark Shannon, https://discuss.python.org/t/add-virtual-threads-to-python/91403

If we take a step back, it seems pretty clear to me that we have veered off course by adopting async/await in languages that have real threads. Innovations like Java’s Project Loom feel like the right fit here. Virtual threads can yield when they need to, switch contexts when blocked, and even work with message-passing systems that make concurrency feel natural. If we free ourselves from the idea that the functional, promise system has figured out all the problems we can look at threads properly again.

— Armin Ronacher, https://lucumr.pocoo.org/2024/11/18/threads-beat-async-await

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

Пример

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

Давайте начнем от "противного". Вот так FastAPI огранизует парсинг данных из запроса, официальный пример из их документации:

from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str

    
@app.put("/items/{item_id}")
async def update_item(
    item_id: int,
    q: str | None = None,
    item: Item | None = None,
) -> dict[str, Any]:
    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    if item:
        results.update({"item": item})
    return results

Что здесь что? Откуда берется q? Откуда item_id? Почему item резко появляется из .body, а не из path или query как раньше? Как нам парсить Headers? А как Cookies? Да, вы правильно поняли, что там везде разный набор правил и разная семантика.

Сравним с:

from typing import Any

from dmr import Body, Controller, Path, Query
from dmr.plugins.pydantic import PydanticSerializer
from pydantic import BaseModel


class BodyItem(BaseModel):
    name: str


class PathItem(BaseModel):
    item_id: int


class QueryItem(BaseModel):
    q: str | None = None


class ItemController(Controller[PydanticSerializer]):
    async def put(
        self,
        parsed_body: Body[BodyItem | None],
        parsed_query: Query[QueryItem],
        parsed_path: Path[PathItem],
    ) -> dict[str, Any]:
        results = {'item_id': parsed_path.item_id}
        if parsed_query.q:
            results.update({'q': parsed_query.q})
        if parsed_body.item:
            results.update({'item': parsed_body.item})
        return results

Всё четко и сразу понятно: какие компоненты парсят что, откуда, в какую модель. Более того, мы сразу выносим всю возможную валидацию из самого метода в модели. Например: если нам нужно сказать, что нам может прийти или q или v в качестве query. Сразу делаем в модели. А как парсить заголовки и cookies? Да, вы угадали: parsed_headers: Headers[HeaderModel] и parsed_cookies: Cookies[CookieModel]. Можно писать свои компоненты для парсинга и наследовать существующие (например для полной кастомизации OpenAPI схемы).

Как оно работает?

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

В основе django-modern-rest лежит несколько простых идей:

  1. Мы даем людям возможность выдрать сериализацию из общего слоя приложения в отдельный класс с простым API, чтобы можно было использовать любые библиотеки для парсинга байтов в питоновские примитивы, а потом в какую-то модель (или сразу в модель, если хотите). Пример сериализатора. За счет чего получаем большую гибкость

  2. Мы никак не лезем в модели. Мы не знаем про них ничего. Ни как они определяются, ни как получать из них OpenAPI схему. Только ваш текущий сериализатор знает, как работать с вашими моделями. Мы не лезем в strict=True в pydantic, мы не добавляем никаких дополнительных абстракций. За счет чего получаем большую понятность

  3. Мы делаем все подготовительные действия из возможных в import-time. Например: построение TypeProvider для валидации, проверка корретности определения самих контроллеров, создание всех промежуточных объектов и тд. В runtime мы только получаем данные, парсим и валидируем их, возвращаем ответ. Пример прогрева кеша перед запуском. За счет чего мы получаем скорость

  4. Все должно быть в спеке. Все возможные коды ошибок, все возможные ответы, все возможные запросы. Я слишком долго в своей жизни правил баги, когда мы держали какие-то части АПИ без спецификации, а потом они внезапно переставали работать. Спека – источник истины. Для того мы и валидируем в dev/test режиме все ответы от нашего АПИ. Но мы часто можем не описать данные ответы руками. Если мы знаем, что auth может упасть с 401, то мы просто добавляем в компонент auth данный код. А потом его добавляем в спеку. За счет чего мы получаем корректность

  5. Django должна оставаться Django. Все наши классы - просто обычные Django View классы. С ними работает все, что работает с Django View. Даже роутинг мы используем стандартный. Только у нас есть собственный вариант замены (с полным сохранением API), который работает в 51 раз быстрее. Такой дизайн позволяет полностью переиспользовать весь тот богатый мир Django плагинов. За счет чего мы получаем совместимость и целый мир готовых библиотек

Что еще есть интересное?

Переиспользование кода

Мы все 1000 раз копировали код из проектов типа https://indominusbyte.github.io/fastapi-jwt-auth/usage/basic/, где нужно буквально влить себе в проект тонну бойлерплейта, просто чтобы получить несчастный JWT токен. А еще мы все страдали от кода вида https://github.com/jazzband/djangorestframework-simplejwt/blob/35dd45723d6bd057fd57ebb2d12e124b44a875c1/rest_framework_simplejwt/serializers.py#L121-L150 где ехал import_string через import_string, которые выполняются прямо в рантайме. Хотим мы такого? Нет.

There must be a better way! (c) Raymond Hettinger

В django-modern-rest контроллер считается абстрактным, пока в него не прокинули все необходимые типовые переменные. Что дает нам возможность писать Generic заготовки вроде:

class ObtainTokensAsyncController(
    _BaseTokenController[_SerializerT],
    Generic[_SerializerT, _ObtainTokensT, _TokensResponseT],
):
    responses = (
        ResponseSpec(
            return_type=ErrorModel,
            status_code=HTTPStatus.UNAUTHORIZED,
        ),
    )

    @modify(status_code=HTTPStatus.OK)
    async def post(self, parsed_body: Body[_ObtainTokensT]) -> _TokensResponseT:
        return await self.login(parsed_body)

    async def login(self, parsed_body: _ObtainTokensT) -> _TokensResponseT:
        user = await aauthenticate(
            self.request,
            **(await self.convert_auth_payload(parsed_body)),
        )
        if user is None:
            raise NotAuthenticatedError
        self.request.user = user
        return await self.make_api_response()

    @abstractmethod
    async def convert_auth_payload(
        self,
        payload: _ObtainTokensT,
    ) -> ObtainTokensPayload:
        raise NotImplementedError

    @abstractmethod
    async def make_api_response(self) -> _TokensResponseT:
        raise NotImplementedError

Что тут происходит? Мы объявляем Generic контроллер, в котором есть 3 типовые переменные:

  • _SerializerT для конкретнного класса сериализатора

  • _ObtainTokensT для типа входных данных, они могут быть разными. Например: {"username": "str", "password": "str"} или {"email": "str", "token": "str"}

  • _TokensResponseT для формата ответа: он тоже может быть любой, например: {"access": "str", "refresh": "str"}

А дальше нам просто можно воспользоваться данным абстрактным контроллером для нашей собственной реализации:

from dmr.security.jwt.views import ObtainTokensSyncController

class ObtainTokensPayload(TypedDict):
    username: str
    password: str

class ObtainTokensResponse(TypedDict):
    access_token: str
    refresh_token: str

class ObtainAccessAndRefreshSyncController(
    ObtainTokensSyncController[
        PydanticSerializer,
        ObtainTokensPayload,
        ObtainTokensResponse,
    ],
):
    @override
    def convert_auth_payload(
        self,
        payload: ObtainTokensPayload,
    ) -> ObtainTokensPayload:
        return payload

    @override
    def make_api_response(self) -> ObtainTokensResponse:
        now = dt.datetime.now(dt.UTC)
        return {
            'access_token': self.create_jwt_token(
                expiration=now + self.jwt_expiration,
                token_type='access',  # noqa: S106
            ),
            'refresh_token': self.create_jwt_token(
                expiration=now + self.jwt_refresh_expiration,
                token_type='refresh',  # noqa: S106
            ),
        }

Что мы тут делаем?

  1. Определяем входные и выходные модели

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

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

Готово! Мы не написали ни строчки boilerplate кода, только то, что нам реально нужно.

А в итоге получили красивый, быстрый, типизированный и расширяемый код.

Стримим SSE или JsonLines

Сейчас в разработке никуда без стриминга событий. LLMки их используют, метрики, логи, трейсы, финансы, гео-события, и т.д.

Конечно же нам нужны такие возможности. Выглядит оно вот так:

import dataclasses
from collections.abc import AsyncIterator

from dmr.plugins.msgspec import MsgspecSerializer
from dmr.streaming.sse import SSEController, SSEvent


@dataclasses.dataclass(frozen=True, slots=True)
class _User:
    email: str


class UserEventsController(SSEController[MsgspecSerializer]):
    async def get(self) -> AsyncIterator[SSEvent[_User]]:
        return self.produce_user_events()

    async def produce_user_events(self) -> AsyncIterator[SSEvent[_User]]:
        # You can send any complex data that can be serialized
        # by the controller's serializer,
        # all SSEvent fields can be customized:
        yield SSEvent(
            _User(email='first@example.com'),
            event='user',
        )
        yield SSEvent(
            _User(email='second@example.com'),
            event='user',
        )

Чтобы стримить что-то, нам потребуется:

  • Отнаследоваться от нужного контроллера: SSEController

  • Определить источник событий: produce_user_events

  • Вернуть из него AsyncIterator

  • Вернуть источник событий из нашей ручки АПИ

  • Мы великолепны!

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

Современные подходы к тестам

Скажу честно, что я украл данный подход из Litestar. Мне очень нравится библиотека polyfactory. Она позволяет почти для любых типов моделей генерировать тестовые данные.

Если у нас есть вот такой контроллер:

class UserCreateModel(pydantic.BaseModel):
    email: str
    age: int


class UserModel(UserCreateModel):
    uid: uuid.UUID


class UserController(Controller[PydanticSerializer]):
    def post(self, parsed_body: Body[UserCreateModel]) -> UserModel:
        return UserModel(
            uid=uuid.uuid4(),
            age=parsed_body.age,
            email=parsed_body.email,
        )

То мы можем вот так его протестировать:

import json
from http import HTTPStatus

from dirty_equals import IsUUID
from django.http import HttpResponse
from polyfactory.factories.pydantic_factory import ModelFactory

from dmr.test import DMRClient
from examples.testing.pydantic_controller import UserCreateModel


class UserCreateModelFactory(ModelFactory[UserCreateModel]):
    """Will create structured random request data for you."""

    __check_model__ = True


def test_create_user(dmr_client: DMRClient) -> None:
    request_data = UserCreateModelFactory.build().model_dump(mode='json')

    request = dmr_client.post('/url/', data=request_data)

    assert response.status_code == HTTPStatus.CREATED
    assert response.content.json() == {
        'uid': IsUUID,
        **request_data,
    }

Если у нас таких фабрик становится много, то они легко превращаются в pytest фикстуры.

Тесты получаются удобными, простыми, достаточно быстрыми и расширяемыми. А штуки вроде pytest-randomly позволяют сделать их еще и воспроизводимыми.

Считаем OpenAPI coverage

Во всех моих проектах всегда требование 100% покрытия кода. Однако, что делать дальше? Если ты уже все покрыл? Дальше остается выдумывать новые полезные метрики. Такие как мутационное тестирование.

Или такие как tracecov или "Покрытие OpenAPI спеки". Выглядит оно вот так:

tracecov
tracecov

Что оно показывает?

  1. Какие операции мы дергали во время тестирования приложения?

  2. Какие параметры мы отправляли из тех, что разрешает спецификация?

  3. Сколько разных примеров протестировали, из тех, что указаны в спецификации?

  4. Сколько видов ответов получили из возможных?

Данная фича - очень крутая! Потому что в отличии от "покрытия кода", она показывает реально важную для ваших клиентов метрику: а сколько АПИ запросов у вас реально работают так, как нужно?

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

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

# Tracecov:
"--tracecov-format=text,html,markdown",
"--tracecov-fail-under-operations=100",
"--tracecov-fail-under-examples=100",
# TODO: set value to 100
"--tracecov-fail-under-parameters=90",
"--tracecov-fail-under-keywords=90",
"--tracecov-fail-under-responses=50",

Чтобы тесты автоматически падали, если ваше покрытие спеки будет падать.

Использование ИИ

Я не большой поклонник ИИ. ИИ очень много ошибается, требует очень много сюсюкания, а самое главное - лишает меня удовольствия писать код самому.

Но, я прекрасно понимаю, что многие люди любят агентов и используют их в работе и в своих хобби проектах. Вот почему мы позиционируем django-modern-rest как AI-First фреймворк.

Давайте покажу на примере. Есть классическая задача для REST фреймворков. Дана OpenAPI спека, нужно сгенерить код; раньше все писали такие генераторы руками. Они быстро устаревали. Вечно ломались, дорабатывать их было очень неудобно. Сейчас все изменилось. Теперь агент может сделать такое полностью сам, нам только нужно предложить для него контекст и скилы.

Для контекста мы используем:

  • llms.txt или llms-full.txt - даешь одну ссылку ЛЛМ, и она знает кунг-фу django-modern-rest

  • Context7 - как еще один формат. Там же можно бесплатно початиться с ЛЛМ про фреймворк, попросить написать какие-то примеры кода. Удобно. Там же можно найти все наши скилы: https://context7.com/wemake-services/django-modern-rest?tab=skills

А еще мы заморочились с DeepWiki: https://deepwiki.com/wemake-services/django-modern-rest

На мой вкус – она отлично помогает дополнить документацию. А на вопросы "почему так?" - вообще дает очень полные и подробные ответы со ссылками на документацию и исходники. Регулярно пользуюсь для других проектов

Каждую страницу доки можно сразу скинуть как контекст в ChatGPT или Claude:

Вжух и контекст для ЛЛМ установлен
Вжух и контекст для ЛЛМ установлен

Мы еще находимся в альфе. И кое-что ломаем. Все ломающие изменения мы публикуем в двух форматах: текстовом (для людей) и в виде промпта для агентов. Пример последнего релиза: https://github.com/wemake-services/django-modern-rest/releases/tag/0.4.0

Копируешь промпт - агент за тебя меняет все части проекта, которые мы сломали. Очень удобно, пользователи почти не страдают.

Ну и конечно же мы подготовили два очень важных скила:

То есть буквально: берете ваш старый код на django-ninja, добавляете скилл, говорите: "мигрировай!". И оно мигрирует.

Давайте покажу. У меня как раз есть проектик старый на django-ninja.

Спор о лучших агентах можно вести долго, я для демо возьму https://gitverse.ru/features/gigacode/install/, потому что он доступен бесплатно, без КВНа в России, есть плагинчик на пичарм (который самый популярные редактор / ide по моему опыту). Давайте его и возьмем для примера.

Промптинг по шагам:

Прочитай эти два источника и только подтверди, что прочитал: 1. Документация: https://django-modern-rest.readthedocs.io/llms-full.txt 2. Скил миграции: https://github.com/wemake-services/django-modern-rest/blob/master/.agents/skills/dmr-from-django-ninja/SKILL.md После прочтения выведи: - Краткое summary документации (5-7 ключевых концептов) - Список всех правил из скила (нумерованный список, дословно) - Напиши "Готов к анализу кода" Больше ничего не делай.

Сначала добавляем контекст и смотрим, что он нас понял верно
Сначала добавляем контекст и смотрим, что он нас понял верно

А затем в отдельном шаге:

Ты мигрируешь API с django-ninja на django-modern-rest. ПРАВИЛА: - Используй ТОЛЬКО то, что есть в документации и скиле (ты их уже прочитал) - Если чего-то не знаешь или потерял контекст — СТОП, напиши "Мне нужно уточнение: [вопрос]" - Никогда не угадывай API — только документация - После каждого шага жди моего подтверждения "продолжай" ПРОЦЕСС: Шаг 1: Проанализируй мой код и составь список всех django-ninja конструкций, которые нужно заменить. Выведи таблицу: | django-ninja | django-modern-rest аналог | уверенность (есть в доках / не нашёл) | Шаг 2 (после моего ок): Составь пошаговый план миграции — каждый пункт это один атомарный файл или блок Шаг 3+ (по одному шагу за раз): Реализуй один пункт плана → покажи diff → жди "продолжай"

Агент уничтожает ужасное ЛЕГАСИ, добавляя в ваш проект МОДЕРНОВОСТИ
Агент уничтожает ужасное ЛЕГАСИ, добавляя в ваш проект МОДЕРНОВОСТИ

Тут нужно будет особо внимательно читать то, что он предлагает. Поправить его сейчас сильно проще, чем потом!

Что дальше:

  • Потребуется какое-то время, пока агент работает, процесс далеко не мгновенный

  • Ему нужно много внимания, все промпты про то, чтобы он не делал фигню сам, а спрашивал вас, если что-то не знает / потерял контекст

  • Каждый diff все равно надо ревьюить глазками, в вашем коде никто не освобождал вас от ответственности за него

  • Потом применить тулинг сверху: всякие ruff, black, тд

  • До состояния самолета придется доработать поезд напильником

Теперь у вас нет отговорок про легаси! Бегом мигрировать на новые модные фреймворки! 🌚️️(шутка, обязательно оценивайте технические решения с холодным расчетом, взвешивайте риски, спрашивайте свою команду)

Вместо завершения

Как-то не поднимается рука назвать данную главу "Завершение", потому что у нас столько всего впереди!

  • Продолжим писать примеры, интеграции, и дорабатывать документацию

  • Продолжим улучшать OpenAPI спецификацию

  • Попробуем сделать быстрее саму Django в некоторых местах

  • Ускорим и улучшим парсинг форм через multipart, как опциональная зависимость или конфигурация

  • Мы хотим компилировать некоторые части фреймворка с mypyc, чтобы добиться бесплатного прироста производительности

  • Возможно какие-то критичные части вынесем вообще в Rust

  • Подумаем как сделать новые WebSocket для Django вместе с бывшими участниками команды django-channels

Надеюсь, что вам было интересно. Потому что мне - было! Работа над данным фреймворком принесла мне кучу положительных моментов.

Что можно сделать для поддержки проекта?

  • Поставить звездочку https://github.com/wemake-services/django-modern-rest/ Давайте добьем до 1000!

  • Вступить в наше сообщество в ТГ: https://t.me/opensource_findings

  • Закинуть мне на пиво за 4+ месяцев фултайм работы над проектом, донат начинается от всего 100 рублей: https://boosty.to/sobolevn

  • Открывать задачки на гитхабе с обратной связью, багами, идеями для фичей. У нас пока альфа, все будем оперативно править / дорабатывать

Давайте в комментах выясним, какие фреймворки (на питоне и не только) - самые лучшие!