Давно преследует одна и та же боль в любом растущем 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.
Ссылки
-
Документация (EN/RU): https://endocore.readthedocs.io
-
Демки с race-тестами:
demos/в репозитории (kanban, booking, shop) -
Discord: https://discord.gg/jwvGj2M9EX
Буду рад любой обратной связи — особенно если найдёте, где я наврал себе про безопасность или производительность. Личный проект, но отношусь к нему серьёзно.
ссылка на оригинал статьи https://habr.com/ru/articles/1062686/