Skip to main content

DI without the boilerplate. Authorization without the escape hatches.

Project description

endow

endow is a framework-agnostic, opinionated way to structure your backend. It is a small dependency-injection runtime for Python 3.12+ applications. It wires objects from typed attributes, usually builds a shared graph from a backend root, and lets you pass runtime values such as db into the graph when it is created. When needed, you can also build an individual Domain, Service, or other Injectable directly.

Install

uv pip install endow

Example

from endow import BackendBase, Domain, Service


class DB:
    def __init__(self, dsn: str) -> None:
        ...

class Applog(Service):
    db: DB

    def track(self, event: str, **context: object) -> None:
        print(event, context)


class Mailer(Service):
    applog: Applog
    db: DB

    @classmethod
    def from_env(cls, db: DB) -> "Mailer":
        return cls()

    def send(self, recipient: str, subject: str, body: str) -> None:
        self.applog.track(
            "mail.sent",
            recipient=recipient,
            subject=subject,
            body=body,
        )


class Products(Domain):
    mailer: Mailer
    applog: Applog

    def update(self, product_id: int) -> None:
        self.applog.track("products.update.started", product_id=product_id)
        self.mailer.send(
            recipient="ops@example.com",
            subject="Product updated",
            body=f"product_id={product_id}",
        )
        self.applog.track("products.update.finished", product_id=product_id)


class AppBackend(BackendBase):
    products: Products


db = DB("postgresql://user:pass@localhost/app")
backend = AppBackend.with_injected(db=db)
backend.products.update(product_id=7)

How it works

  • Typed attributes are the source of truth for wiring.
  • with_injected(...) usually builds one shared object graph from a backend root, but it can also build a standalone Domain, Service, or other Injectable.
  • Service and Domain are lightweight markers that participate in the graph.
  • Nested from_env(...) hooks can receive runtime inputs from the root call.
  • Cycles in the graph are supported because instances are cached during construction.

Service vs Domain

Use the two markers to communicate architectural intent:

  • Service is for infrastructure and external-facing capabilities, such as logging, mail, persistence, or API clients.
  • Domain is for business logic and application workflows that coordinate those capabilities.

The dependency direction should stay one way:

  • Domain objects may depend on Service objects.
  • Service objects should not depend on Domain objects.

That keeps the graph aligned with layered architecture and one-way data flow: the business layer can use infrastructure, but infrastructure should not reach back into business logic.

By default, that rule is a convention rather than an enforced runtime check. If you want the graph to enforce it, use BackendBase.with_injected_checked(strict, ...):

  • strict=True turns Service-to-Domain dependencies into errors.
  • strict=False leaves the dependency in place but emits a warning.

Use with_injected(...) when you want the current permissive behavior without checking.

Why not make everything a Service

If everything is a Service, the graph stops expressing the difference between business behavior and infrastructure concerns. Keeping Domain separate makes the direction of dependencies visible, which helps prevent business rules from leaking into adapters and makes the architecture easier to read and review.

Policies and authorization

endow.policy helps keep authorization logic out of domain methods.

  • Use BasePolicy.require_authenticated(...) for auth checks.
  • Use BasePolicy.require_allowed(...) for simple yes/no permission checks.
  • Use AuthorizationResult, Allow, and Deny when a policy should return something richer than a boolean.
from endow import Domain
from endow.policy import Allow, AuthorizationResult, BasePolicy, Deny


class AuthContext:  # this is a minimal example
    def can(self, permission: str) -> bool:
        ...


class ProductPolicy(BasePolicy):
    auth: AuthContext

    def request_update(self) -> AuthorizationResult:
        if not self.auth.can("products.update"):
            return Deny("missing products.update permission")

        return Allow(
            apply=lambda query: query.where(
                lambda row: row.owner_id == self.auth.user_id,
            ),
        )

    def require_update(self) -> Allow:
        return self.request_update().require()


class ScopedDomain(Domain):
    policy: ProductPolicy
    table: ...  # ORM-specific query/table object

    def require_update(self):
        authz = self.policy.require_update()
        query = self.table.permissions(update=True)
        return authz(query)


class Products(ScopedDomain):
    def update_name(self, new_name: str):
        return self.require_update().update(name=new_name)

The common pattern is:

  • the shared domain helper sets up the base operation, such as .permissions(update=True)
  • the policy either denies the request or returns an Allow(...) result that applies extra query shaping

That shaping can use .where(...), .select(...), joins, or other ORM-specific operations when needed.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

endow-0.1.2.tar.gz (43.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

endow-0.1.2-py3-none-any.whl (7.4 kB view details)

Uploaded Python 3

File details

Details for the file endow-0.1.2.tar.gz.

File metadata

  • Download URL: endow-0.1.2.tar.gz
  • Upload date:
  • Size: 43.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for endow-0.1.2.tar.gz
Algorithm Hash digest
SHA256 777d582e1ffb3c34f9ce521ea6c5827fb0ec77aa415b9fccbc9b5e09a5986721
MD5 c7fb170346bbb1ec8f5933766e14d614
BLAKE2b-256 6443daff6aecfd40b4a496502df739fc3ca459f7465dc69c4737833f53280f26

See more details on using hashes here.

File details

Details for the file endow-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: endow-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 7.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Linux Mint","version":"22.3","id":"zena","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for endow-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a147db98cf79e2ac6164b212fd5009e0043a47879ef1f226406733160f474065
MD5 0044f391fc91329a64102d0d68f56ca2
BLAKE2b-256 99090b26c941dba3ae68c5212d98867da2d133b1cf56720c3348461b9d1cdfa0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page