neva-fastapi 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export VIRTUAL_ENV="$PWD/.venv"
2
+ layout python
@@ -0,0 +1,21 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[ocd]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Tests / Coverage / Benchmarks
13
+ htmlcov/
14
+ .coverage
15
+ .pytest_cache/
16
+ .benchmarks/
17
+ profiles/
18
+
19
+ # LLM Contexts
20
+ llm-context.md
21
+ CLAUDE.md
@@ -0,0 +1,31 @@
1
+ repos:
2
+ - repo: https://github.com/gitleaks/gitleaks
3
+ rev: v8.30.1
4
+ hooks:
5
+ - id: gitleaks
6
+
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v6.0.0
9
+ hooks:
10
+ - id: trailing-whitespace
11
+ - id: end-of-file-fixer
12
+ - id: check-yaml
13
+ - id: check-added-large-files
14
+ - id: check-case-conflict
15
+ - id: check-merge-conflict
16
+ - id: debug-statements
17
+
18
+ - repo: https://github.com/astral-sh/ruff-pre-commit
19
+ rev: v0.15.6
20
+ hooks:
21
+ - id: ruff
22
+ args: [--fix, --exit-non-zero-on-fix]
23
+ - id: ruff-format
24
+
25
+ - repo: https://github.com/pre-commit/mirrors-mypy
26
+ rev: v1.19.1
27
+ hooks:
28
+ - id: mypy
29
+ args: [--enable-incomplete-feature=TypeForm]
30
+ additional_dependencies:
31
+ [dishka>=1.10.0, "fastapi[all]>=0.129.0", pytest>=9.0.2]
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,24 @@
1
+ ## 1.0.0 (2026-05-11)
2
+
3
+ ### 📌➕⬇️➖⬆️ Dependencies
4
+
5
+ - update neva dep
6
+
7
+ ## 0.2.1 (2026-05-11)
8
+
9
+ ### 🐛🚑️ Fixes
10
+
11
+ - remove obsolete parameter in rouetr
12
+
13
+ ### 🏷️ Types
14
+
15
+ - stub + re-exports
16
+
17
+ ## 0.2.0 (2026-05-06)
18
+
19
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
20
+
21
+ - update perms on build script
22
+ - config commitizen
23
+
24
+ ## 0.1.0 (2026-05-05)
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: neva-fastapi
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: python-neva>=3.1.0
File without changes
@@ -0,0 +1,7 @@
1
+ """FastAPI integration for Neva."""
2
+
3
+ from neva.fastapi.app import App, Inject
4
+ from neva.fastapi.router import APIRouter
5
+
6
+
7
+ __all__ = ["APIRouter", "App", "Inject"]
@@ -0,0 +1,102 @@
1
+ """Main application class.
2
+
3
+ This module provides the core App class that extends FastAPI with dependency
4
+ injection. DI is handled by the dependency injection container provided by
5
+ the Application class.
6
+ """
7
+
8
+ from collections.abc import AsyncIterator, Mapping, Sequence
9
+ from contextlib import asynccontextmanager
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from dishka import FromDishka
14
+ from dishka.integrations.fastapi import FastapiProvider, setup_dishka
15
+ from neva.arch.application import Application
16
+ from neva.arch.service_provider import ServiceProvider
17
+ from neva.support import Result
18
+ from starlette.middleware import Middleware
19
+ from starlette.routing import BaseRoute
20
+ from starlette.types import Lifespan, StatefulLifespan
21
+
22
+ import fastapi
23
+
24
+
25
+ class App(fastapi.FastAPI):
26
+ """Main application class extending FastAPI."""
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ routes: list[BaseRoute] | None = None,
32
+ middlewares: Sequence[Middleware] | None = None,
33
+ lifespan: Lifespan["App"] | None = None,
34
+ config_path: str | Path | None = None,
35
+ ) -> None:
36
+ """Initialize the application.
37
+
38
+ Args:
39
+ routes: List of routes to register with the application.
40
+ middlewares: Sequence of middleware to apply to the application.
41
+ lifespan: Custom lifespan context manager for application lifecycle.
42
+ config_path: Path to the configuration directory. Defaults to "./config"
43
+ relative to the current working directory.
44
+
45
+ """
46
+ self.application: Application = Application(config_path=config_path)
47
+ self.application.build_container(FastapiProvider())
48
+
49
+ config = self.application.config
50
+
51
+ super().__init__(
52
+ debug=config.get("app.debug", type_=bool).unwrap_or(False), # noqa: FBT003
53
+ routes=routes,
54
+ title=config.get("app.title", type_=str).unwrap_or("Neva Application"),
55
+ version=config.get("app.version", type_=str).unwrap_or("0.1.0"),
56
+ openapi_url=config.get("app.openapi_url", type_=str).unwrap_or(
57
+ "/openapi.json"
58
+ ),
59
+ docs_url=config.get("app.docs_url", type_=str).unwrap_or("/docs"),
60
+ redoc_url=config.get("app.redoc_url", type_=str).unwrap_or("/redoc"),
61
+ lifespan=self.build_lifespan(custom_lifespan=lifespan),
62
+ middleware=middlewares,
63
+ )
64
+
65
+ setup_dishka(self.application.container, app=self)
66
+
67
+ def register(
68
+ self,
69
+ provider: type[ServiceProvider],
70
+ ) -> Result[ServiceProvider, str]:
71
+ """Registers a service provider with the application.
72
+
73
+ Returns:
74
+ Result containing the registered provider instance or an error message.
75
+ """
76
+ return self.application.register(provider=provider)
77
+
78
+ def build_lifespan(
79
+ self,
80
+ custom_lifespan: Lifespan["App"] | None = None,
81
+ ) -> StatefulLifespan["App"]:
82
+ """Builds a lifespan context manager for the application.
83
+
84
+ Returns:
85
+ the lifespan context manager.
86
+ """
87
+
88
+ @asynccontextmanager
89
+ async def lifespan(
90
+ app: "App",
91
+ ) -> AsyncIterator[Mapping[str, Any]]:
92
+ async with self.application.lifespan():
93
+ if custom_lifespan is None:
94
+ yield {}
95
+ else:
96
+ async with custom_lifespan(app) as state:
97
+ yield state if state is not None else {}
98
+
99
+ return lifespan
100
+
101
+
102
+ Inject = FromDishka
@@ -0,0 +1,31 @@
1
+ from collections.abc import AsyncIterator
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from httpx import ASGITransport, AsyncClient
6
+
7
+ from neva.fastapi.app import App
8
+
9
+
10
+ @pytest.fixture
11
+ def webapp(test_config: Path) -> App:
12
+ """Pytest fixture for the HTTP Neva app.
13
+
14
+ Returns:
15
+ App: The Neva application instance.
16
+ """
17
+ return App(config_path=test_config)
18
+
19
+
20
+ @pytest.fixture
21
+ async def http_client(webapp: App) -> AsyncIterator[AsyncClient]:
22
+ """An async httpx client to test the application.
23
+
24
+ Yields:
25
+ An async httpx client.
26
+ """
27
+ async with AsyncClient(
28
+ transport=ASGITransport(webapp),
29
+ base_url="http://localhost:8000",
30
+ ) as client:
31
+ yield client
File without changes
@@ -0,0 +1,234 @@
1
+ # ruff: noqa: B008
2
+ from collections.abc import Sequence
3
+ from enum import Enum
4
+ from typing import Annotated, Any, Callable, override
5
+
6
+ from annotated_doc import Doc
7
+ from dishka.integrations.fastapi import DishkaRoute
8
+ from starlette.routing import BaseRoute
9
+ from starlette.types import ASGIApp, Lifespan
10
+ from typing_extensions import deprecated
11
+
12
+ import fastapi
13
+ from fastapi import Response, params
14
+ from fastapi.datastructures import Default
15
+ from fastapi.responses import JSONResponse
16
+ from fastapi.routing import APIRoute
17
+ from fastapi.utils import generate_unique_id
18
+
19
+
20
+ class APIRouter(fastapi.APIRouter):
21
+ """A custom router that integrates dependency injection utilities."""
22
+
23
+ @override
24
+ def __init__(
25
+ self,
26
+ *,
27
+ prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "",
28
+ tags: Annotated[
29
+ list[str | Enum] | None,
30
+ Doc("""
31
+ A list of tags to be applied to all the *path operations* in this
32
+ router.
33
+
34
+ It will be added to the generated OpenAPI (e.g. visible at `/docs`).
35
+
36
+ Read more about it in the
37
+ [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).
38
+ """),
39
+ ] = None,
40
+ dependencies: Annotated[
41
+ Sequence[params.Depends] | None,
42
+ Doc("""
43
+ A list of dependencies (using `Depends()`) to be applied to all the
44
+ *path operations* in this router.
45
+
46
+ Read more about it in the
47
+ [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies).
48
+ """),
49
+ ] = None,
50
+ default_response_class: Annotated[
51
+ type[Response],
52
+ Doc("""
53
+ The default response class to be used.
54
+
55
+ Read more in the
56
+ [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class).
57
+ """),
58
+ ] = Default(JSONResponse),
59
+ responses: Annotated[
60
+ dict[int | str, dict[str, Any]] | None,
61
+ Doc("""
62
+ Additional responses to be shown in OpenAPI.
63
+
64
+ It will be added to the generated OpenAPI (e.g. visible at `/docs`).
65
+
66
+ Read more about it in the
67
+ [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/).
68
+
69
+ And in the
70
+ [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies).
71
+ """),
72
+ ] = None,
73
+ callbacks: Annotated[
74
+ list[BaseRoute] | None,
75
+ Doc("""
76
+ OpenAPI callbacks that should apply to all *path operations* in this
77
+ router.
78
+
79
+ It will be added to the generated OpenAPI (e.g. visible at `/docs`).
80
+
81
+ Read more about it in the
82
+ [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/).
83
+ """),
84
+ ] = None,
85
+ routes: Annotated[
86
+ list[BaseRoute] | None,
87
+ Doc("""
88
+ **Note**: you probably shouldn't use this parameter, it is inherited
89
+ from Starlette and supported for compatibility.
90
+
91
+ ---
92
+
93
+ A list of routes to serve incoming HTTP and WebSocket requests.
94
+ """),
95
+ deprecated("""
96
+ You normally wouldn't use this parameter with FastAPI, it is
97
+ inherited from Starlette and supported for compatibility.
98
+
99
+ In FastAPI, you normally would use the *path operation methods*,
100
+ like `router.get()`, `router.post()`, etc.
101
+ """),
102
+ ] = None,
103
+ redirect_slashes: Annotated[
104
+ bool,
105
+ Doc("""
106
+ Whether to detect and redirect slashes in URLs when the
107
+ client inherited doesn't use the same format.
108
+ """),
109
+ ] = True,
110
+ default: Annotated[
111
+ ASGIApp | None,
112
+ Doc("""
113
+ Default function handler for this router. Used to handle
114
+ 404 Not Found errors.
115
+ """),
116
+ ] = None,
117
+ dependency_overrides_provider: Annotated[
118
+ Any | None,
119
+ Doc("""
120
+ Only used internally by FastAPI to handle dependency overrides.
121
+
122
+ You shouldn't need to use it. It normally points to the `FastAPI`
123
+ app object.
124
+ """),
125
+ ] = None,
126
+ on_startup: Annotated[
127
+ Sequence[Callable[[], Any]] | None,
128
+ Doc("""
129
+ A list of startup event handler functions.
130
+
131
+ You should instead use the `lifespan` handlers.
132
+
133
+ Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/).
134
+ """),
135
+ ] = None,
136
+ on_shutdown: Annotated[
137
+ Sequence[Callable[[], Any]] | None,
138
+ Doc("""
139
+ A list of shutdown event handler functions.
140
+
141
+ You should instead use the `lifespan` handlers.
142
+
143
+ Read more in the
144
+ [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/).
145
+ """),
146
+ ] = None,
147
+ lifespan: Annotated[
148
+ Lifespan[Any] | None,
149
+ Doc("""
150
+ A `Lifespan` context manager handler. This replaces `startup` and
151
+ `shutdown` functions with a single context manager.
152
+
153
+ Read more in the
154
+ [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/).
155
+ """),
156
+ ] = None,
157
+ deprecated: Annotated[
158
+ bool | None,
159
+ Doc("""
160
+ Mark all *path operations* in this router as deprecated.
161
+
162
+ It will be added to the generated OpenAPI (e.g. visible at `/docs`).
163
+
164
+ Read more about it in the
165
+ [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/).
166
+ """),
167
+ ] = None,
168
+ include_in_schema: Annotated[
169
+ bool,
170
+ Doc("""
171
+ To include (or not) all the *path operations* in this router in the
172
+ generated OpenAPI.
173
+
174
+ This affects the generated OpenAPI (e.g. visible at `/docs`).
175
+
176
+ Read more about it in the
177
+ [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-parameters-from-openapi).
178
+ """),
179
+ ] = True,
180
+ generate_unique_id_function: Annotated[
181
+ Callable[[APIRoute], str],
182
+ Doc("""
183
+ Customize the function used to generate unique IDs for the *path
184
+ operations* shown in the generated OpenAPI.
185
+
186
+ This is particularly useful when automatically generating clients or
187
+ SDKs for your API.
188
+
189
+ Read more about it in the
190
+ [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function).
191
+ """),
192
+ ] = Default(generate_unique_id),
193
+ strict_content_type: Annotated[
194
+ bool,
195
+ Doc("""
196
+ Enable strict checking for request Content-Type headers.
197
+
198
+ When `True` (the default), requests with a body that do not include
199
+ a `Content-Type` header will **not** be parsed as JSON.
200
+
201
+ This prevents potential cross-site request forgery (CSRF) attacks
202
+ that exploit the browser's ability to send requests without a
203
+ Content-Type header, bypassing CORS preflight checks. In particular
204
+ applicable for apps that need to be run locally (in localhost).
205
+
206
+ When `False`, requests without a `Content-Type` header will have
207
+ their body parsed as JSON, which maintains compatibility with
208
+ certain clients that don't send `Content-Type` headers.
209
+
210
+ Read more about it in the
211
+ [FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
212
+ """),
213
+ ] = Default(True), # noqa: FBT003 This is inherited from FastAPI
214
+ ) -> None:
215
+ super().__init__(
216
+ prefix=prefix,
217
+ tags=tags,
218
+ dependencies=dependencies,
219
+ default_response_class=default_response_class,
220
+ responses=responses,
221
+ callbacks=callbacks,
222
+ routes=routes,
223
+ redirect_slashes=redirect_slashes,
224
+ default=default,
225
+ dependency_overrides_provider=dependency_overrides_provider,
226
+ route_class=DishkaRoute,
227
+ on_startup=on_startup,
228
+ on_shutdown=on_shutdown,
229
+ lifespan=lifespan,
230
+ deprecated=deprecated,
231
+ include_in_schema=include_in_schema,
232
+ generate_unique_id_function=generate_unique_id_function,
233
+ # strict_content_type=strict_content_type,
234
+ )
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "neva-fastapi"
3
+ dynamic = ["version"]
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = ["python-neva>=3.1.0"]
8
+
9
+ [dependency-groups]
10
+ dev = [
11
+ "bandit>=1.9.2",
12
+ "commitizen>=4.15.0",
13
+ "cz-conventional-gitmoji>=0.7.0",
14
+ "mypy>=1.19.1",
15
+ "poethepoet>=0.38.0",
16
+ "polyfactory>=3.1.0",
17
+ "pre-commit>=4.5.0",
18
+ "pytest>=9.0.2",
19
+ "pytest-asyncio>=0.25.3",
20
+ "pytest-benchmark>=5.2.3",
21
+ "pytest-cov>=7.0.0",
22
+ "ruff>=0.15.6",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["hatchling", "versioningit"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["neva"]
31
+
32
+ [tool.hatch.version]
33
+ source = "versioningit"
34
+
35
+ [tool.hatch.version.format]
36
+ distance = "{base_version}.dev{distance}+{vcs}{rev}"
37
+ dirty = "{version}+dirty"
38
+ distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.dirty"
39
+
40
+ [tool.commitizen]
41
+ name = "cz_gitmoji"
42
+ version_provider = "scm"
43
+ tag_format = "$version"
44
+ version_scheme = "pep440"
45
+ update_changelog_on_bump = true
46
+ annotated_tag = true
47
+ post_bump_hooks = ["scripts/retag-with-changelog.sh"]
48
+
49
+ [tool.uv.sources]
50
+ python-neva = { path = "../neva", editable = true }
51
+
52
+ [tool.basedpyright]
53
+ enableExperimentalFeatures = true
54
+
55
+ [tool.mypy]
56
+ enable_incomplete_feature = ["TypeForm"]
57
+ plugins = ["pydantic.mypy"]
58
+
59
+ [tool.pytest.ini_options]
60
+ asyncio_mode = "auto"
61
+ asyncio_default_fixture_loop_scope = "function"
62
+ testpaths = ["tests"]
63
+
64
+
65
+ [tool.poe.tasks]
66
+ # Code quality
67
+ ruff = "uv run ruff"
68
+ mypy = "uv run mypy"
69
+ lint = "poe ruff check"
70
+ fmt = "poe ruff format"
71
+ tc = "poe mypy ."
72
+
73
+ # Testing
74
+ test = "pytest"
75
+ test-cov = "poe test --cov=neva --cov-report=term-missing"
76
+ test-full = "poe test-cov tests/"
@@ -0,0 +1,42 @@
1
+ target-version = "py312"
2
+ line-length = 88
3
+
4
+ [lint]
5
+ select = [
6
+ "FAST",
7
+ "ANN",
8
+ "ASYNC",
9
+ "S",
10
+ "FBT",
11
+ "B",
12
+ "A",
13
+ "C4",
14
+ "DTZ",
15
+ "ISC",
16
+ "ICN",
17
+ "SIM",
18
+ "SLOT",
19
+ "I",
20
+ "E",
21
+ "W",
22
+ "DOC",
23
+ "D",
24
+ "F",
25
+ "RUF",
26
+ ]
27
+ preview = true
28
+ ignore = [
29
+ "D100", # Allow missing docstrings in public modules
30
+ "D107", # Allow missing docstrings in __init__
31
+ "RUF029",
32
+ ]
33
+
34
+ [lint.per-file-ignores]
35
+ "tests/*" = ["D", "S101"]
36
+
37
+ [lint.isort]
38
+ force-single-line = false
39
+ lines-after-imports = 2
40
+
41
+ [lint.pydocstyle]
42
+ convention = "google"
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ TAG="${CZ_POST_CURRENT_VERSION}"
5
+
6
+ # Generate the changelog section for this version only
7
+ CHANGELOG=$(cz changelog "${CZ_POST_CURRENT_VERSION}" --dry-run |
8
+ sed 's/^### *//' |
9
+ sed 's/^## *//' |
10
+ sed 's/\*\*//g' |
11
+ sed 's/`//g' |
12
+ sed 's/\[.\+\](\(.\+\))/\1/g')
13
+
14
+ # Replace the tag cz just created with an annotated one carrying the changelog
15
+ git tag -d "$TAG"
16
+ git tag -a "$TAG" -m "$CHANGELOG"
File without changes
File without changes
@@ -0,0 +1,6 @@
1
+ config = {
2
+ "title": "TestApp",
3
+ "openapi_url": None,
4
+ "debug": 0,
5
+ "environment": "testing",
6
+ }
@@ -0,0 +1 @@
1
+ pytest_plugins = ["neva.fastapi.fixtures"]
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+
3
+ from neva.testing import TestCase
4
+ from pytest import fixture
5
+
6
+ from neva.fastapi.app import App
7
+
8
+
9
+ class TestWebApp(TestCase):
10
+ @fixture
11
+ def test_config(self, tmp_path: Path) -> Path:
12
+ return Path(__file__).parent / "config"
13
+
14
+ def test_webapp(self, webapp: App) -> None:
15
+ assert isinstance(webapp, App)