Skip to content

Home

archtool
Assembles like Lego.
Works like a hammer.
Holds like a foundation.

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

PyPI CI Python MIT Coverage Sponsor


Who are you?

Your team grows. Each new service is wired differently. Every new hire reinvents the plumbing. Architecture docs drift from reality.

archtool gives the whole team one standard: declare an interface, write a concrete class, archtool wires it. Layer violations are caught at startup — not in a 3 AM incident. Architecture becomes something you can actually enforce, not just document.

You ship fast. But every shortcut in the foundation costs twice as much when you scale.

archtool handles the boilerplate so you focus on the product. Add a module, register it, done. The architecture scales with you — no rewrite when you hire engineer #10.

fractal_chunks (coming soon) — battle-tested modules for auth, users, payments, notifications. Plug in what you need; skip what you don't.

You generate code faster than you integrate it. The bottleneck is wiring.

archtool removes that step. AI writes the service, you declare the interface — archtool connects everything at startup. No boilerplate. No import chains to untangle.

fractal_chunks (coming soon) — a growing catalog of production-grade modules, each a Lego brick. Drop users, auth-jwt, notifications into a new project in minutes, not days.

You've cleaned up enough service-locator messes. You know what happens when DI is informal.

archtool makes the right patterns the only path. Interfaces are the only contract. Implementations are discovered, not registered. Layer violations fail fast at startup. Clean Architecture — without the ceremony.


The problem every Python project hits

You start clean. A service, a repo, maybe a controller. Then the project grows.

Somewhere around module 5, this happens:

# entrypoints/run.py — the graveyard of good intentions
import sys
sys.path.insert(0, "..")   # ← why is this here again?

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                          # ← don't forget this
order_service = OrderService()
order_service.repo = order_repo
order_service.user_service = user_service              # ← or this
payment_service = PaymentService()
payment_service.repo = payment_repo
payment_service.order_service = order_service          # ← or this
notification_service = NotificationService()
notification_service.payment_service = payment_service # ← or this
# ... 40 more lines ...

This is what archtool replaces — entirely.


The solution

# entrypoints/run.py with 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 scans your modules, discovers every interface–implementation pair, instantiates them in dependency order, and wires everything together. No registration boilerplate. No sys.path hacks. No hidden wiring bugs.


What you get

Declare a dependency as a class annotation on your concrete class. archtool reads it and calls setattr at assembly time.

# services.py
class OrderService(OrderServiceABC):
    repo: OrderRepoABC          # archtool sets this
    user_service: UserServiceABC  # and this

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

Layer violations — a service depending on a controller, a controller reaching into domain internals — are caught at startup, not in production at 3 AM.

injector = DependencyInjector(
    modules_list=[...],
    layers=[InfrastructureLayer, DomainLayer, ApplicationLayer],
)
injector.inject()  # raises TopLevelLayerUsingException if boundaries are crossed

The built-in layers follow Clean Architecture. But you define your own:

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

Any filename, any base class — archtool adapts to your architecture, not the other way around.

  • S — each AppModule owns one bounded context
  • O — add a module, nothing else changes
  • L — swap a repo for a stub, consumers don't know
  • I — interfaces stay minimal and focused
  • D — everything depends on abstractions, never concretions

See it in action

# app/users/services.py

from .interfaces import UserServiceABC, UserRepoABC

class UserService(UserServiceABC):
    repo: UserRepoABC  # ← archtool injects UserRepo here

    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  # cross-module
from .interfaces import OrderServiceABC, OrderRepoABC

class OrderService(OrderServiceABC):
    repo:     OrderRepoABC
    user_svc: UserServiceABC  # ← wired from app.users automatically

    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"),  # cross-module dep resolved
    ],
    project_root=ROOT,
)
injector.inject()
# app/fraud/services.py  ←  Domain layer

from .interfaces import FraudServiceABC, FraudControllerABC

class FraudService(FraudServiceABC):
    # Domain depending on Application layer — violation!
    controller: FraudControllerABC


# entrypoints/run.py

injector = DependencyInjector(
    modules_list=[AppModule("app.fraud")],
    layers=default_layers,          # enforcement enabled
    project_root=ROOT,
)
injector.inject()  # ← raises here, not at 3 AM in production
output
Click ▶ Run to execute

Install

pip install archtool

Supports Python 3.10 · 3.11 · 3.12 · 3.13.


Five-minute start

Quickstart — working project from scratch.

Why archtool? — the full problem statement and how we address it.

How it works — the two-pass injection algorithm explained.