Metadata-Version: 2.4
Name: nodstar
Version: 0.1.0
Summary: nodnod integration for Litestar — declare dependency lifetimes on nodes, inject into handlers by type
Author-email: univied <amerfoe@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: litestar>=2.24.0
Requires-Dist: nodnod>=1.1.0
Description-Content-Type: text/markdown

# nodstar

[nodnod](https://github.com/timoniq/nodnod) integration for [Litestar](https://litestar.dev). Declare dependency lifetimes on nodes, inject into handlers by type.

## Install

```bash
uv add nodstar
```

Requires Python 3.14+.

## Usage

```python
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`:

```python
@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

Use `Node[T]` in handler signature — nodnod resolves the dependency graph, Litestar injects the value:

```python
@get("/users")
async def get_users(db_session: Node[DbSession]) -> list[User]:
    return await db_session.scalars(select(User))
```

`db_session` is fully typed as `AsyncSession` by the type checker.

### Wire up

```python
app = Litestar(
    route_handlers=[get_users],
    plugins=[NodstarPlugin()],
)
```

That's it. No `dependencies={...}`, no manual `Provide()`, no container configuration.

## Lifetimes

| Decorator | Scope | Created | Destroyed |
|-----------|-------|---------|-----------|
| `@global_node` | App | On startup | On shutdown |
| `@request` | Request | Per HTTP request | After response |
| `@per_call` | Call | Per handler invocation | After handler |

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.

## How it works

1. Lifetime decorators register nodes in a global registry
2. `NodstarPlugin` collects all registered nodes on app init
3. On startup, `@global_node` nodes are composed into an app-wide scope
4. Per request, a child scope is created and `@request`/`@per_call` nodes are composed
5. `Node[T]` is a type-level alias that resolves to `T` for type checkers, and at runtime produces `Annotated[T, Dependency(skip_validation=True)]` so Litestar skips msgspec validation and pulls the value from the nodnod scope

## Generator lifecycle

Use `yield` in `__compose__` for setup/teardown:

```python
@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
