nodstar
nodnod integration for Litestar. Declare dependency lifetimes on nodes, inject into handlers by type.
Install
uv add nodstar
Requires Python 3.14+.
Usage
from nodnod import scalar_node
from litestar import Litestar, get
from nodstar import NodstarPlugin, Node, global_node, request
Define nodes
Decorate with a lifetime (@global_node, @request, @per_call) and @scalar_node:
@global_node
@scalar_node
class DatabasePool:
@classmethod
async def __compose__(cls) -> AsyncEngine:
engine = create_async_engine(DATABASE_URL)
yield engine
await engine.dispose()
@request
@scalar_node
class DbSession:
@classmethod
async def __compose__(cls, pool: DatabasePool) -> AsyncSession:
async with AsyncSession(pool) as session:
yield session
Inject into handlers
Annotate a handler parameter with a node type — nodnod resolves the dependency graph, Litestar injects the value. Injection is by type, so the parameter can be named anything:
@get("/users")
async def get_users(session: DbSession) -> list[User]:
return await session.scalars(select(User))
Optionally wrap the type in Node[T] for precise static typing — it resolves to
T for the type checker (nodes are otherwise seen as type[T]):
@get("/users")
async def get_users(session: Node[DbSession]) -> list[User]:
return await session.scalars(select(User))
Both forms are equivalent at runtime.
Wire up
app = Litestar(
route_handlers=[get_users],
plugins=[NodstarPlugin()],
)
That's it. No dependencies={...}, no manual Provide(), no container configuration.
Nodes that need the request
A node can depend on the live connection with Connection, so authentication and
anything else derived from headers is an ordinary node rather than a hand-written
Provide:
from nodstar import Connection
@request
@scalar_node
class Principal:
@classmethod
async def __compose__(cls, connection: Connection, session: DbSession) -> Principal:
token = connection.headers.get("Authorization")
...
@get("/me")
async def me(principal: Node[Principal]) -> UserRead: ...
The injected value is Litestar's own connection object. Composing such a node outside a request raises, since there is nothing to inject.
Reaching nodes from guards and hooks
Litestar calls guards, before_request hooks and exception handlers with a fixed
signature, so they cannot take Node[...] parameters. from_connection is the way in:
from nodstar import from_connection
async def authenticated(connection: ASGIConnection, _: BaseRouteHandler) -> None:
principal = await from_connection(connection, Principal)
if not principal.allows(...):
raise PermissionDeniedException
Guards run before handler DI, but composition is shared per request: the handler's
Node[Principal] reuses what the guard already composed.
Lifetimes
| Decorator | Scope | Created | Destroyed |
|---|---|---|---|
@global_node |
App | On startup | On shutdown |
@request |
Request | On first use in a request | After response |
@per_call |
Call | On every resolution | After response |
Nodes declare their own lifetime. The dependency graph is resolved automatically — a @request node can depend on a @global_node, and nodnod will pull the value from the parent scope.
Composition is lazy: a request pays only for the nodes it actually asks for, so a
handler that touches no node never opens a session, and an authentication node never
runs on a public route. Within one request each @request node is composed once and
shared by every consumer, including concurrently resolved parameters.
How it works
- Lifetime decorators register nodes in a global registry
- On app init,
NodstarPluginwalks every route handler (including those onControllers andRouters) and inspects its type hints - For each parameter whose type is a registered node, the plugin binds a
Provideto that handler under the parameter's own name and marks itskip_validation=True, so matching is by type, not by parameter name - On startup,
@global_nodenodes are composed into an app-wide scope, and an agent is built (not run) per request-scoped node so a broken graph fails at startup - Per request, a middleware creates a child scope but composes nothing. A provider
composes its node's subtree on first use, into the child scope for
@requestand into a throwaway grandchild for@per_call; both are closed after the response Node[T]is an optional type-level alias that resolves toTfor type checkers; at runtime it is justAnnotated[T, Dependency(skip_validation=True)]and is treated identically to a bareTannotation
Generator lifecycle
Use yield in __compose__ for setup/teardown:
@request
@scalar_node
class DbSession:
@classmethod
async def __compose__(cls, pool: DatabasePool) -> AsyncSession:
async with AsyncSession(pool) as session:
yield session
# teardown runs when request scope closes
License
MIT
Release files for nodstar 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nodstar-0.2.0.tar.gz | 26.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nodstar-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.1 kB
Release files / nodstar-0.2.0.tar.gz
| Download URL | nodstar-0.2.0.tar.gz |
|---|---|
| Size | 26.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a7fb27f855d41d0c26f77734b31292d6d164a28bf86b143c4e20ecd321e35f44
|
|
BLAKE2b-256 checksum How to use checksums |
0e4498fefae9b2da3ef667f3af9a66aff76d47bc244d6ec12321a63a81f337e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / nodstar-0.2.0-py3-none-any.whl
| Download URL | nodstar-0.2.0-py3-none-any.whl |
|---|---|
| Size | 10.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a52fc7d12a08e219915b5049d89d42dcb1ce7763a4e4bfc9b4852abe24b977ac
|
|
BLAKE2b-256 checksum How to use checksums |
8065f10664b0e7c237f6886380acd3f6dfed1ba1d4ac02542416351808e4c3ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|