Metadata-Version: 2.5
Name: neva-fastapi
Version: 2.1.0
Summary: Add your description here
Requires-Python: >=3.12
Requires-Dist: annotated-doc>=0.0.4
Requires-Dist: dishka>=1.10.0
Requires-Dist: fastapi>=0.141.1
Requires-Dist: pydantic>=2.0
Requires-Dist: python-neva>=5.4.1
Requires-Dist: starlette>=1.6
Requires-Dist: typing-extensions>=4.5
Provides-Extra: testing
Requires-Dist: httpx>=0.27; extra == 'testing'
Requires-Dist: python-neva[testing]; extra == 'testing'
Description-Content-Type: text/markdown

# neva-fastapi

The **FastAPI integration** for the [Neva](https://pypi.org/project/python-neva/)
framework. It marries FastAPI to neva's dishka-based dependency-injection
container so that route handlers resolve injected services, while keeping all
HTTP concerns out of the framework-agnostic `python-neva` core.

Most consumers pull this in transitively via the `python-neva[fastapi]` extra
rather than depending on it directly.

## What it provides

Everything is re-exported from `neva.fastapi`:

- **`App`** — a subclass of `fastapi.FastAPI` that owns a `neva.arch.Application`
  (the DI container) and drives it: builds the container, reads app metadata
  from neva config (`app.*`), wires neva providers' `lifespan()` hooks, and
  installs per-request DI scoping middleware.
- **`APIRouter`** — a subclass of `fastapi.APIRouter` that forces
  `route_class=DishkaRoute` so injection works inside handlers.
- **`Inject`** — a re-export of `dishka.FromDishka`. Annotate handler params as
  `Inject[SomeService]`. **`Inject` only works on this package's `APIRouter`
  (or `App`'s routes)** — a vanilla `fastapi.APIRouter` lacks `DishkaRoute` and
  injection silently won't happen.
- **`route_template`** — `(scope) -> str | None`, the matched route's path
  template with router prefixes and mount points applied
  (`"/api/v1/users/{id}"`), or `None` when nothing matched. A pure function
  over the ASGI scope, so it works with a vanilla `fastapi.APIRouter` too.
  Written for `neva-asgi[otel]`'s tracing middleware, which takes it as an
  injected callable — a tracer needs the template rather than the resolved
  path, or every request becomes its own operation and no endpoint can be
  aggregated. This package depends on no OpenTelemetry to provide it, not even
  optionally.
- **`HttpAppConfig`** — a `TypedDict` naming the `app.*` config keys this
  package reads, so a type checker catches the typos that a `Result`-based
  lookup with a default would otherwise swallow.

`neva` is a **namespace package** (no top-level `neva/__init__.py`); this repo
owns `neva/fastapi/` and shares the `neva.*` namespace with `python-neva`.

## Application metadata

The OpenAPI document's title and version, the debug flag and the three
documentation URLs come from the `app` config namespace rather than the
constructor:

```python
# src/config/app.py
config = {
    "title": "Billing",
    "version": "2.1.0",
    "docs_url": "/documentation",
}
```

`HttpAppConfig` declares those six keys — `title`, `debug`, `version`,
`openapi_url`, `docs_url`, `redoc_url` — and nothing else. The `app` namespace
is shared, and each package declares only what it reads: the core owns `key`,
`previous_keys` and `providers`. A `TypedDict` is closed, so annotate a real
`config/app.py` with a shape that inherits from each package's:

```python
from neva.arch import AppConfig
from neva.fastapi import HttpAppConfig


class ServiceAppConfig(HttpAppConfig, AppConfig): ...


config: ServiceAppConfig = {"title": "Billing", "providers": [...]}
```

Annotating with `HttpAppConfig` alone rejects `providers`; that is the
ownership rule working, not a defect. Unset keys fall back to
`Neva Application`, `0.1.0`, debug off, `/openapi.json`, `/docs` and `/redoc`.

## Testing helpers

`neva.fastapi.testing.HttpTestCase` is a `neva.testing.TestCase` whose
application *is* the one your `App` owns, booted through the `App`'s own
lifespan, with an `http_client` fixture over it.

```python
from neva.fastapi.testing import HttpTestCase


class TestActors(HttpTestCase):
    @override
    @classmethod
    def create_app(cls, config_path: Path) -> App:
        app = App(config_path=config_path)
        _ = app.register(ActorProvider)
        app.include_router(router)
        return app

    async def test_it_lists(self, http_client: AsyncClient) -> None:
        assert (await http_client.get("/actors")).status_code == 200
```

Register providers and include routers in `create_app`: it runs before the boot,
and `Application.register` refuses once the application is booted.

The `webapp` / `http_client` fixtures and the `neva.fastapi.fixtures` plugin were
removed in 2.0.0. `webapp` was a synchronous fixture, so nothing ever entered the
app's lifespan — it handed back an unbooted app with no `state.dishka_container`
and no provider hooks run.

## Develop

```bash
uv sync          # install/refresh deps
poe lint         # ruff check
poe fmt          # ruff format
poe tc           # pyrefly check
poe test         # pytest
poe test-cov     # pytest --cov=neva --cov-report=term-missing
```

`asyncio_mode = "auto"` is set, so async tests need no `@pytest.mark.asyncio`.

## Contributing

**Commits** follow [Conventional Commits](https://www.conventionalcommits.org/)
with [gitmoji](https://gitmoji.dev/) prefixes, enforced by
[`cz_gitmoji`](https://github.com/ljnsn/cz-conventional-gitmoji). Commitizen is
a dev dependency — run `cz commit` for the guided wizard, or format manually as
`:gitmoji: type(scope): subject`.

**Releases** are cut per-repo with commitizen from the repo root:

```bash
cz bump                          # bump version in pyproject, write CHANGELOG, tag v<version>
git push --follow-tags origin main
uv build && uv publish           # build + publish the wheel/sdist
```

`cz bump` derives the level from the commits since the last tag, updates
`CHANGELOG.md`, and runs `scripts/retag-with-changelog.sh` to rewrite the new
tag with the rendered changelog as its annotation.
