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 standaloneDomain,Service, or otherInjectable.ServiceandDomainare 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:
Serviceis for infrastructure and external-facing capabilities, such as logging, mail, persistence, or API clients.Domainis for business logic and application workflows that coordinate those capabilities.
The dependency direction should stay one way:
Domainobjects may depend onServiceobjects.Serviceobjects should not depend onDomainobjects.
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=TrueturnsService-to-Domaindependencies into errors.strict=Falseleaves 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, andDenywhen 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
777d582e1ffb3c34f9ce521ea6c5827fb0ec77aa415b9fccbc9b5e09a5986721
|
|
| MD5 |
c7fb170346bbb1ec8f5933766e14d614
|
|
| BLAKE2b-256 |
6443daff6aecfd40b4a496502df739fc3ca459f7465dc69c4737833f53280f26
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a147db98cf79e2ac6164b212fd5009e0043a47879ef1f226406733160f474065
|
|
| MD5 |
0044f391fc91329a64102d0d68f56ca2
|
|
| BLAKE2b-256 |
99090b26c941dba3ae68c5212d98867da2d133b1cf56720c3348461b9d1cdfa0
|