Я сделал ASGI-фреймворк, где дерево папок — и есть API

от автора

Давно преследует одна и та же боль в любом растущем API-проекте на Python: таблица маршрутов начинает жить своей жизнью. @app.post("/v2/user/role") лежит в файле, который импортируется через два уровня include_router, реальная бизнес-логика — в третьем файле, а понять «какой версии принадлежит этот эндпоинт» превращается в археологию по grep’у.

Я решил проверить гипотезу: а что если убрать таблицу маршрутов вообще? Не «сделать её удобнее» — а убрать как отдельную сущность. Пусть структура каталогов и будет таблицей маршрутов.

Так получился EndoCore — ASGI-фреймворк, который я пишу как pet-проект последние несколько месяцев. Дошёл до состояния «можно показывать людям»: 1679 тестов, PyPI, MkDocs-сайт на двух языках. Делюсь — что получилось, что скучно, и где я сам не уверен, что это хорошая идея.

Идея

Api/v1/User/[id]/Get.py    ->  GET  /v1/user/42        (id="42")Api/v1/User/Role/Post.py   ->  POST /v1/user/roleApi/v2/User/[id]/Get.py    ->  GET  /v2/user/42         (v1 продолжает работать)

Кладёшь файл в нужную папку — эндпоинт существует. Роутинг, версионирование, end routes для интроспекции — всё это операции над одним и тем же деревом каталогов, а не три разные подсистемы, которые могут разъехаться.

Сам файл — просто функция:

# Api/v1/User/Role/Post.py -> POST /v1/user/rolefrom endocore import Request, Responseasync def handler(request: Request) -> Response:    data = await request.json()    return Response.json({"created": data["name"]}, status=201)

Никакого app = FastAPI(), никакого декоратора, никакой регистрации руками. Удалил файл — эндпоинта нет. end routes не парсит декораторы в поисках роутов — он просто печатает дерево, потому что дерево уже и есть ответ.

Версионирование — это copytree, а не конфиг

end version create 2   # Api/v1 -> Api/v2, endpoints + локальные сервисы

v1 физически не может измениться, когда меняется v2 — это не два роутера, разделяющих состояние, а две независимые копии поддерева. Никаких if version == 2 веток, гниющих внутри одного хендлера годами.

ORM: безопасность — не опция, а единственный путь

Я довольно много времени убил именно на ORM, потому что для меня “фреймворк ради фреймворка” неинтересен — интересно, когда каждый примитив спроектирован так, что нельзя выстрелить себе в ногу случайно.

from endocore.orm import Model, fields, configure, create_all, Q, Fclass User(Model):    name   = fields.CharField(max_length=100)    age    = fields.IntegerField(default=0)    active = fields.BooleanField(default=True)configure(backend="sqlite", database="app.db")  # или postgres, pool_size=10, ...create_all(User)User.objects.filter(age__gte=18).order_by("-age")User.objects.filter(Q(age__lt=18) | Q(name__icontains="a"))User.objects.filter(pk=1).update(age=F("age") + 1)          # атомарный F()# неблокирующе для ASGI:async with endocore.orm.aatomic():    user = await User.objects.aget(pk=1)    await user.asave()

Значения всегда идут через биндинг драйвера — никогда не через f-строку. Идентификаторы (таблицы/колонки) валидируются регэкспом и квотятся. Лукапы — строгий whitelist, неизвестный лукап падает с ошибкой, а не тихо проваливается в сырой SQL. Это не «слой безопасности, который можно выключить» — это единственный способ, которым ORM вообще умеет строить запрос.

Есть connection pooling (для PostgreSQL — реальный параллелизм транзакций, не один лок на всех), миграции с откатом, шифрование файлов (AES-256-GCM, если утечёт диск — без ключа файлы бесполезны), встроенные сессии на HMAC-подписанных куках и scrypt для паролей — без сторонних auth-библиотек.

Проверка боем: не просто демки, а гоночные тесты

Написать «у меня асинхронно и с транзакциями» — легко. Доказать, что это реально работает под конкурентностью — совсем другое дело. Поэтому вместо одного тривиального демо-приложения я сделал три, и специально проверил их race conditions:

  • Kanban-борда с живыми обновлениями по WebSocket (pub/sub комнаты).

  • Бронирование слотов — тест на 8 одновременных запросов на один и тот же слот: ровно один получает 201, остальные 409, в базе ровно одна запись.

  • Магазин с идемпотентными покупками и вебхуком платёжного шлюза — 6 одновременных дублирующихся запросов не списывают деньги дважды, retry вебхука после сбоя — тоже.

Отдельно есть тестовый сьют, который гоняет пул соединений против реального PostgreSQL (tests/orm/test_postgres_pool.py), а не только против SQLite в памяти — потому что конкурентность на SQLite и на настоящей БД с несколькими воркерами это буквально разные миры, и я не хотел врать себе, что «раз тесты зелёные — значит работает везде».

Цифры (с честными оговорками)

Python 3.14, static GET:  EndoCore 23 800 req/s vs FastAPI 11 000 req/s  (2.2×)Python 3.14, dynamic GET: EndoCore 30 800 req/s vs FastAPI  8 600 req/s  (3.6×)

Но! Это чистый оверхед диспетчеризации — вызов ASGI-callable напрямую, без сети, без базы. FastAPI платит эту цену за pydantic-валидацию и сериализацию, которые включены всегда, даже если конкретному эндпоинту они не нужны. В реальном приложении с базой данных эта разница тонет в latency одного SQL-запроса. Я не хочу продавать бенчмарк как причину выбрать фреймворк — это просто честные цифры, полная методология в доках.

Чего в EndoCore нет и, скорее всего, не будет

  • Нет своей экосистемы плагинов размером с FastAPI/Django — это моложе на порядки.

  • Нет ORM-миграций для сложных data-трансформаций — только структура таблиц, для сложного переноса данных пишешь скрипт руками.

  • WebSocket pub/sub — однопроцессный; для fan-out между воркерами нужен Redis поверх (описано в доках, но это ваша забота, не встроенное).

  • Это по-прежнему бета (0.7.0b2), не 1.0. Я использую его сам, но не претендую, что это готово для чужого продакшена без вдумчивого чтения кода — благо, кода мало и он читается за вечер.

Как попробовать

pip install "endocore[watch]"end new blog && cd blogend dev   # http://127.0.0.1:8000/docs

end — зарезервированное слово в PowerShell, там алиас endo.

Ссылки

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

ссылка на оригинал статьи https://habr.com/ru/articles/1062686/