Skip to main content

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

  1. Lifetime decorators register nodes in a global registry
  2. On app init, NodstarPlugin walks every route handler (including those on Controllers and Routers) and inspects its type hints
  3. For each parameter whose type is a registered node, the plugin binds a Provide to that handler under the parameter's own name and marks it skip_validation=True, so matching is by type, not by parameter name
  4. On startup, @global_node nodes 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
  5. 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 @request and into a throwaway grandchild for @per_call; both are closed after the response
  6. Node[T] is an optional type-level alias that resolves to T for type checkers; at runtime it is just Annotated[T, Dependency(skip_validation=True)] and is treated identically to a bare T annotation

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)

Source distribution for nodstar 0.2.0
File Size Uploaded
nodstar-0.2.0.tar.gz 26.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nodstar 0.2.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page