Перейти к содержанию

Главная

archtool
Собирается как Lego.
Работает как молоток.
Держит как фундамент.

разработано·Чудайкин Александр·Бюро автоматизации процессов

PyPI CI Python MIT Coverage Sponsor


Кто ты?

Команда растёт. Каждый сервис подключён по-своему. Новый разработчик изобретает колесо заново. Архитектурная документация давно не совпадает с кодом.

archtool даёт всей команде один стандарт: объявляй интерфейс, пиши реализацию, archtool подключит сам. Нарушения слоёв ловятся при старте — не на разборе инцидента в три ночи. Архитектура становится чем-то, что можно проверить, а не просто задокументировать.

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

archtool берёт на себя бойлерплейт — ты фокусируешься на продукте. Добавил модуль, зарегистрировал, готово. Архитектура растёт вместе с командой — никакого переписывания на десятом разработчике.

fractal_chunks (в разработке) — проверенные боем модули для auth, users, платежей, уведомлений. Подключаешь нужное; пропускаешь лишнее.

Ты генерируешь код быстрее, чем интегрируешь его. Узкое место — разводка.

archtool убирает этот шаг. ИИ пишет сервис, ты объявляешь интерфейс — archtool подключает всё при старте. Никакого бойлерплейта. Никаких цепочек импортов.

fractal_chunks (в разработке) — растущий каталог production-модулей, каждый как кубик Lego. Добавь users, auth-jwt, notifications в новый проект за минуты, а не дни.

Ты уже чистил чужие service-locator'ы. Ты знаешь, что бывает, когда DI неформальный.

archtool делает правильные паттерны единственным путём. Интерфейсы — единственный контракт. Реализации обнаруживаются, не регистрируются. Нарушения слоёв падают быстро при старте. Clean Architecture — без лишних церемоний.


Проблема, которую рано или поздно встречает каждый Python-проект

Начинаешь чисто. Один сервис, один репозиторий, может быть контроллер. Проект растёт.

Где-то на пятом модуле появляется это:

# entrypoints/run.py — кладбище благих намерений
import sys
sys.path.insert(0, "..")   # ← зачем это вообще здесь?

from app.users.repos import UserRepo
from app.users.services import UserService
from app.orders.repos import OrderRepo
from app.orders.services import OrderService
from app.payments.repos import PaymentRepo
from app.payments.services import PaymentService
from app.notifications.services import NotificationService

user_repo = UserRepo()
order_repo = OrderRepo()
payment_repo = PaymentRepo()
user_service = UserService()
user_service.repo = user_repo                          # ← не забыть это
order_service = OrderService()
order_service.repo = order_repo
order_service.user_service = user_service              # ← и это
payment_service = PaymentService()
payment_service.repo = payment_repo
payment_service.order_service = order_service          # ← и это
notification_service = NotificationService()
notification_service.payment_service = payment_service # ← и это
# ... ещё 40 строк ...

Именно это archtool заменяет — полностью.


Решение

# entrypoints/run.py с archtool
from pathlib import Path
from archtool.dependency_injector import DependencyInjector
from archtool.global_types import AppModule

injector = DependencyInjector(
    modules_list=[
        AppModule("app.users"),
        AppModule("app.orders"),
        AppModule("app.payments"),
        AppModule("app.notifications"),
    ],
    project_root=Path(__file__).parent.parent,
)
injector.inject()

archtool сканирует твои модули, обнаруживает каждую пару интерфейс–реализация, инстанциирует их в порядке зависимостей и связывает всё вместе. Никакого бойлерплейта регистрации. Никаких sys.path-хаков. Никаких скрытых багов проводки.


Что ты получаешь

Объяви зависимость как аннотацию класса на конкретном классе. archtool прочитает её и вызовет setattr при сборке.

# services.py
class OrderService(OrderServiceABC):
    repo: OrderRepoABC             # archtool установит это
    user_service: UserServiceABC   # и это

    def place(self, items: list) -> None:
        user = self.user_service.get_current()
        self.repo.save({"user": user, "items": items})

Нарушения границ слоёв — сервис импортирует напрямую из репо, контроллер лезет во внутренности домена — выявляются при старте, а не в продакшене в 3 ночи.

injector = DependencyInjector(
    modules_list=[...],
    layers=[InfrastructureLayer, DomainLayer, ApplicationLayer],
)
injector.inject()  # бросает TopLevelLayerUsingException при нарушении

Встроенные слои следуют Clean Architecture. Но ты определяешь свои:

class IntegrationsLayer(Layer):
    depends_on = InfrastructureLayer
    class Components:
        clients = ComponentPattern("clients", superclass=ABCClient)

Любое имя файла, любой базовый класс — archtool адаптируется к твоей архитектуре, а не наоборот.

  • S — каждый AppModule владеет одним ограниченным контекстом
  • O — добавляешь модуль, ничего больше не меняется
  • L — заменяешь репо на заглушку, потребители не знают об этом
  • I — интерфейсы остаются минимальными и сфокусированными
  • D — всё зависит от абстракций, никогда от конкреций

Посмотри как работает

# app/users/services.py

from .interfaces import UserServiceABC, UserRepoABC

class UserService(UserServiceABC):
    repo: UserRepoABC  # ← archtool подставит UserRepo сюда

    def get_name(self) -> str:
        return self.repo.find_all()[0]


# entrypoints/run.py

injector = DependencyInjector(
    modules_list=[AppModule("app.users")],
    project_root=Path(__file__).parent.parent,
)
injector.inject()

svc = injector.get_dependency(UserServiceABC)
print(svc.get_name())  # → "alice"
# app/orders/services.py

from app.users.interfaces import UserServiceABC  # кросс-модульная зависимость
from .interfaces import OrderServiceABC, OrderRepoABC

class OrderService(OrderServiceABC):
    repo:     OrderRepoABC
    user_svc: UserServiceABC  # ← подтянется из app.users автоматически

    def place(self, user_id: int, items: list) -> dict:
        user = self.user_svc.get_name()
        return {"user": user, "items": items, "status": "placed"}


# entrypoints/run.py

injector = DependencyInjector(
    modules_list=[
        AppModule("app.users"),
        AppModule("app.orders"),  # кросс-модульная зависимость резолвится
    ],
    project_root=ROOT,
)
injector.inject()
# app/fraud/services.py  ←  Domain layer

from .interfaces import FraudServiceABC, FraudControllerABC

class FraudService(FraudServiceABC):
    # Domain зависит от Application — нарушение!
    controller: FraudControllerABC


# entrypoints/run.py

injector = DependencyInjector(
    modules_list=[AppModule("app.fraud")],
    layers=default_layers,          # контроль слоёв включён
    project_root=ROOT,
)
injector.inject()  # ← падает здесь, а не в 3 ночи в проде
output
Нажми ▶ Run чтобы запустить

Установка

pip install archtool

Поддерживается Python 3.10 · 3.11 · 3.12 · 3.13.


Старт за пять минут

Быстрый старт — рабочий проект с нуля.

Зачем archtool? — полное описание проблемы и как мы её решаем.

Как это работает — алгоритм двухпроходной инъекции объяснён.