python-neva 2.4.1__tar.gz → 3.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.
- {python_neva-2.4.1 → python_neva-3.0.0}/.pre-commit-config.yaml +16 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/PKG-INFO +2 -2
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/arch/__init__.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/arch/application.py +40 -20
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/arch/facade.py +3 -3
- python_neva-3.0.0/neva/arch/integrations/__init__.py +1 -0
- python_neva-2.4.1/neva/arch/app.py → python_neva-3.0.0/neva/arch/integrations/fastapi.py +14 -31
- {python_neva-2.4.1/neva/arch → python_neva-3.0.0/neva/arch/integrations}/faststream.py +2 -25
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/arch/service_provider.py +26 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/config/repository.py +8 -14
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/database/provider.py +8 -8
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/events/dispatcher.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/events/provider.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/logging/provider.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/encryption/encrypter.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/hash_manager.py +10 -10
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/provider.py +2 -2
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/testing/test_case.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/pyproject.toml +4 -3
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/arch/test_scope.py +19 -19
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/config/test_repository.py +4 -15
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/test_dispatch.py +3 -3
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/security/test_encrypter.py +6 -5
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/security/test_hash_manager.py +1 -1
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/testing/test_fixtures.py +8 -8
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/testing/test_refresh_database.py +7 -7
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/testing/test_test_case.py +11 -11
- {python_neva-2.4.1 → python_neva-3.0.0}/uv.lock +5 -5
- {python_neva-2.4.1 → python_neva-3.0.0}/.envrc +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/.gitignore +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/.python-version +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/README.md +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/arch/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/config/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/config/base_providers.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/config/loader.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/database/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/database/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/database/connection.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/database/manager.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/database/transaction.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/events/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/events/event.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/events/event_registry.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/events/listener.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/accessors.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/app.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/db.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/event.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/event.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/hash.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/hash.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/log.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/results.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/strategy.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/strconv.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/support/time.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/testing/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/testing/fakes.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/testing/fixtures.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/neva/testing/http.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/ruff.toml +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/arch/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/config/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/config/test_loader.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/conftest.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/conftest.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_sqlalchemy_integration.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_transaction.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_transaction_context.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/conftest.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/test_deferred.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/test_event.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/test_event_registry.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/events/test_immediate.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/obs/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/obs/test_correlation.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/obs/test_profiler.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/security/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/testing/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/testing/test_event_fake.py +0 -0
- {python_neva-2.4.1 → python_neva-3.0.0}/tests/testing/test_facade_restore.py +0 -0
|
@@ -27,3 +27,19 @@ repos:
|
|
|
27
27
|
hooks:
|
|
28
28
|
- id: mypy
|
|
29
29
|
args: [--enable-incomplete-feature=TypeForm]
|
|
30
|
+
additional_dependencies:
|
|
31
|
+
[
|
|
32
|
+
cryptography>=46.0.3,
|
|
33
|
+
dishka>=1.10.0,
|
|
34
|
+
"fastapi[all]>=0.129.0",
|
|
35
|
+
faststream>=0.6.6,
|
|
36
|
+
"pwdlib[argon2,bcrypt]>=0.3.0",
|
|
37
|
+
pyinstrument>=5.1.1,
|
|
38
|
+
pytest>=9.0.2,
|
|
39
|
+
structlog>=25.5.0,
|
|
40
|
+
"sqlalchemy[asyncio]>=2.0.0",
|
|
41
|
+
asyncpg>=0.30.0,
|
|
42
|
+
aiosqlite>=0.20.0,
|
|
43
|
+
typer>=0.21.1,
|
|
44
|
+
opentelemetry-instrumentation-sqlalchemy>=0.62b0,
|
|
45
|
+
]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: aiosqlite>=0.20.0
|
|
7
7
|
Requires-Dist: asyncpg>=0.30.0
|
|
8
8
|
Requires-Dist: cryptography>=46.0.3
|
|
9
|
-
Requires-Dist: dishka>=1.
|
|
9
|
+
Requires-Dist: dishka>=1.10.0
|
|
10
10
|
Requires-Dist: fastapi[all]>=0.129.0
|
|
11
11
|
Requires-Dist: faststream>=0.6.6
|
|
12
12
|
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
|
|
@@ -4,9 +4,9 @@ This module contains the main application class, service provider pattern,
|
|
|
4
4
|
and facade implementations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from neva.arch.app import App
|
|
8
7
|
from neva.arch.application import Application
|
|
9
8
|
from neva.arch.facade import Facade
|
|
9
|
+
from neva.arch.integrations.fastapi import App
|
|
10
10
|
from neva.arch.service_provider import (
|
|
11
11
|
Bootable,
|
|
12
12
|
ServiceProvider,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Base application for DI and facade injection."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from collections.abc import AsyncIterator
|
|
5
|
-
from contextlib import AsyncExitStack, asynccontextmanager
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Callable, Self
|
|
8
8
|
|
|
9
9
|
import dishka
|
|
10
|
-
from dishka.
|
|
10
|
+
from dishka.provider import BaseProvider
|
|
11
|
+
from typing_extensions import deprecated
|
|
11
12
|
|
|
12
13
|
from neva import Err, Ok, Result
|
|
13
14
|
from neva.arch.facade import Facade
|
|
@@ -33,7 +34,7 @@ class Application:
|
|
|
33
34
|
|
|
34
35
|
self.config: ConfigRepository = ConfigRepository()
|
|
35
36
|
self.providers: dict[type, ServiceProvider] = {}
|
|
36
|
-
self.
|
|
37
|
+
self.root_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
37
38
|
|
|
38
39
|
configuration_path = (
|
|
39
40
|
config_path
|
|
@@ -50,7 +51,9 @@ class Application:
|
|
|
50
51
|
):
|
|
51
52
|
raise RuntimeError("Failed to register config")
|
|
52
53
|
|
|
53
|
-
self.
|
|
54
|
+
_ = self.root_provider.provide(
|
|
55
|
+
lambda: self.config, provides=ConfigRepository
|
|
56
|
+
)
|
|
54
57
|
|
|
55
58
|
providers_from_file: list[type[ServiceProvider]] = self.config.get(
|
|
56
59
|
"providers.providers", type_=list[type[ServiceProvider]]
|
|
@@ -61,14 +64,18 @@ class Application:
|
|
|
61
64
|
providers: set[type[ServiceProvider]] = set(providers_from_file).union(
|
|
62
65
|
set(providers_from_app)
|
|
63
66
|
)
|
|
67
|
+
_ = self.root_provider.provide(source=lambda: self, provides=Application)
|
|
64
68
|
self.register_providers(base_providers().union(providers))
|
|
65
|
-
self.bind(source=lambda: self, interface=Application)
|
|
66
69
|
self._bind_event_listeners()
|
|
67
|
-
self.
|
|
68
|
-
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
71
|
-
self.container = dishka.
|
|
70
|
+
self.build_container(self.root_provider)
|
|
71
|
+
|
|
72
|
+
def build_container(self, *providers: BaseProvider) -> None:
|
|
73
|
+
"""Build the application container."""
|
|
74
|
+
self.container: dishka.AsyncContainer = dishka.make_async_container(
|
|
75
|
+
self.root_provider,
|
|
76
|
+
*[p.provider for p in self.providers.values()],
|
|
77
|
+
*providers,
|
|
78
|
+
)
|
|
72
79
|
|
|
73
80
|
def register(self, provider: type[ServiceProvider]) -> Result[ServiceProvider, str]:
|
|
74
81
|
"""Registers a service provider with the application.
|
|
@@ -90,6 +97,7 @@ class Application:
|
|
|
90
97
|
for provider in providers:
|
|
91
98
|
_ = self.register(provider)
|
|
92
99
|
|
|
100
|
+
@deprecated("Use the new bind() method directly with a Service Provider.")
|
|
93
101
|
def bind(
|
|
94
102
|
self,
|
|
95
103
|
source: type | Callable[..., Any],
|
|
@@ -98,32 +106,43 @@ class Application:
|
|
|
98
106
|
scope: dishka.BaseScope | None = None,
|
|
99
107
|
) -> None:
|
|
100
108
|
"""Binds a source to the container."""
|
|
101
|
-
_ = self.
|
|
109
|
+
_ = self.root_provider.provide(
|
|
102
110
|
source=source,
|
|
103
111
|
scope=scope,
|
|
104
112
|
provides=interface,
|
|
105
113
|
)
|
|
106
114
|
|
|
107
|
-
def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
115
|
+
async def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
108
116
|
"""Resolve and instanciate a type from the container.
|
|
109
117
|
|
|
110
118
|
Returns:
|
|
111
119
|
Result containing the resolved type instance or an error message.
|
|
112
120
|
"""
|
|
113
121
|
try:
|
|
114
|
-
return Ok(self.container.get(interface))
|
|
122
|
+
return Ok(await self.container.get(interface))
|
|
115
123
|
except Exception as e:
|
|
116
124
|
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
def make_sync[T](self, interface: type[T]) -> Result[T, str]:
|
|
127
|
+
"""Synchronous version of make.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Result containing the resolved type instance or an error message.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
return Ok(self.container.get_sync(interface))
|
|
134
|
+
except Exception as e:
|
|
135
|
+
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
136
|
+
|
|
137
|
+
@asynccontextmanager
|
|
138
|
+
async def scope(self, scope: dishka.BaseScope | None = None) -> AsyncIterator[Self]:
|
|
120
139
|
"""Enter a new scope.
|
|
121
140
|
|
|
122
141
|
Yields:
|
|
123
142
|
The application instance with the new scope.
|
|
124
143
|
"""
|
|
125
144
|
parent = self.container
|
|
126
|
-
with self.container(scope=scope) as container:
|
|
145
|
+
async with self.container(scope=scope) as container:
|
|
127
146
|
self.container = container
|
|
128
147
|
try:
|
|
129
148
|
yield self
|
|
@@ -140,9 +159,10 @@ class Application:
|
|
|
140
159
|
if isinstance(provider, Bootable):
|
|
141
160
|
await stack.enter_async_context(provider.lifespan())
|
|
142
161
|
|
|
143
|
-
self._wire_event_listeners()
|
|
162
|
+
await self._wire_event_listeners()
|
|
144
163
|
yield
|
|
145
164
|
|
|
165
|
+
await self.container.close()
|
|
146
166
|
Facade.reset_facade_application()
|
|
147
167
|
|
|
148
168
|
def _bind_event_listeners(self) -> None:
|
|
@@ -152,11 +172,11 @@ class Application:
|
|
|
152
172
|
for listener_cls in listeners:
|
|
153
173
|
self.bind(listener_cls)
|
|
154
174
|
|
|
155
|
-
def _wire_event_listeners(self) -> None:
|
|
175
|
+
async def _wire_event_listeners(self) -> None:
|
|
156
176
|
"""Wire event-listener mappings from all providers onto the dispatcher."""
|
|
157
177
|
from neva.events.dispatcher import EventDispatcher
|
|
158
178
|
|
|
159
|
-
result = self.make(EventDispatcher)
|
|
179
|
+
result = await self.make(EventDispatcher)
|
|
160
180
|
if result.is_err:
|
|
161
181
|
return
|
|
162
182
|
|
|
@@ -153,7 +153,7 @@ class FacadeMeta(ABCMeta):
|
|
|
153
153
|
real: object = (
|
|
154
154
|
cls._get_app()
|
|
155
155
|
.ok_or(f"Cannot spy on {cls.__name__}: no application set.")
|
|
156
|
-
.and_then(lambda a: a.
|
|
156
|
+
.and_then(lambda a: a.make_sync(cls.get_facade_accessor()))
|
|
157
157
|
.unwrap()
|
|
158
158
|
)
|
|
159
159
|
spy_obj = MagicMock(spec=type(real), wraps=real)
|
|
@@ -162,7 +162,7 @@ class FacadeMeta(ABCMeta):
|
|
|
162
162
|
|
|
163
163
|
def restore(cls) -> None:
|
|
164
164
|
"""Restore the facade to its real service, removing any fake or spy."""
|
|
165
|
-
FacadeMeta._fake_instances.pop(cls, None)
|
|
165
|
+
_ = FacadeMeta._fake_instances.pop(cls, None)
|
|
166
166
|
|
|
167
167
|
@contextmanager
|
|
168
168
|
def faking(cls) -> Iterator[AsyncMock]:
|
|
@@ -198,7 +198,7 @@ class FacadeMeta(ABCMeta):
|
|
|
198
198
|
"""
|
|
199
199
|
if cls in FacadeMeta._fake_instances:
|
|
200
200
|
return Ok(cast(T, FacadeMeta._fake_instances[cls]))
|
|
201
|
-
return app.
|
|
201
|
+
return app.make_sync(interface)
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
class Facade(ABC, metaclass=FacadeMeta):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Integrations module providing integrations with external frameworks."""
|
|
@@ -8,11 +8,10 @@ the Application class.
|
|
|
8
8
|
from collections.abc import AsyncGenerator, AsyncIterator, Sequence
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Callable
|
|
12
11
|
|
|
13
|
-
import dishka
|
|
14
12
|
import fastapi
|
|
15
|
-
from dishka
|
|
13
|
+
from dishka import FromDishka
|
|
14
|
+
from dishka.integrations.fastapi import FastapiProvider, setup_dishka
|
|
16
15
|
from starlette.middleware import Middleware
|
|
17
16
|
from starlette.routing import BaseRoute
|
|
18
17
|
from starlette.types import StatefulLifespan, StatelessLifespan
|
|
@@ -44,6 +43,7 @@ class App(fastapi.FastAPI):
|
|
|
44
43
|
|
|
45
44
|
"""
|
|
46
45
|
self.application: Application = Application(config_path=config_path)
|
|
46
|
+
self.application.build_container(FastapiProvider())
|
|
47
47
|
|
|
48
48
|
self._lifespan: StatelessLifespan["App"] | StatefulLifespan["App"] | None = (
|
|
49
49
|
lifespan
|
|
@@ -51,13 +51,15 @@ class App(fastapi.FastAPI):
|
|
|
51
51
|
config = self.application.config
|
|
52
52
|
|
|
53
53
|
super().__init__(
|
|
54
|
-
debug=config.get("app.debug",
|
|
54
|
+
debug=config.get("app.debug", type_=bool).unwrap_or(False), # noqa: FBT003
|
|
55
55
|
routes=routes,
|
|
56
|
-
title=config.get("app.title",
|
|
57
|
-
version=config.get("app.version",
|
|
58
|
-
openapi_url=config.get("app.openapi_url",
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
title=config.get("app.title", type_=str).unwrap_or("Neva Application"),
|
|
57
|
+
version=config.get("app.version", type_=str).unwrap_or("0.1.0"),
|
|
58
|
+
openapi_url=config.get("app.openapi_url", type_=str).unwrap_or(
|
|
59
|
+
"/openapi.json"
|
|
60
|
+
),
|
|
61
|
+
docs_url=config.get("app.docs_url", type_=str).unwrap_or("/docs"),
|
|
62
|
+
redoc_url=config.get("app.redoc_url", type_=str).unwrap_or("/redoc"),
|
|
61
63
|
lifespan=self._create_lifespan(),
|
|
62
64
|
middleware=middlewares,
|
|
63
65
|
)
|
|
@@ -75,28 +77,6 @@ class App(fastapi.FastAPI):
|
|
|
75
77
|
"""
|
|
76
78
|
return self.application.register(provider=provider)
|
|
77
79
|
|
|
78
|
-
def bind(
|
|
79
|
-
self,
|
|
80
|
-
source: type | Callable[..., Any],
|
|
81
|
-
*,
|
|
82
|
-
interface: type | None = None,
|
|
83
|
-
scope: dishka.BaseScope | None = None,
|
|
84
|
-
) -> None:
|
|
85
|
-
"""Binds a source to the container."""
|
|
86
|
-
self.application.bind(
|
|
87
|
-
source=source,
|
|
88
|
-
interface=interface,
|
|
89
|
-
scope=scope,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
93
|
-
"""Resolve and instanciate a type from the container.
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
Result containing the resolved type instance or an error message.
|
|
97
|
-
"""
|
|
98
|
-
return self.application.make(interface=interface)
|
|
99
|
-
|
|
100
80
|
@asynccontextmanager
|
|
101
81
|
async def lifespan(self) -> AsyncGenerator[None, None]:
|
|
102
82
|
"""Manage the lifecycle of the application."""
|
|
@@ -127,3 +107,6 @@ class App(fastapi.FastAPI):
|
|
|
127
107
|
yield
|
|
128
108
|
|
|
129
109
|
return composed_lifespan
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
Inject = FromDishka
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
|
-
import dishka
|
|
8
7
|
import faststream
|
|
9
8
|
from faststream._internal.broker import BrokerUsecase
|
|
10
9
|
from faststream._internal.configs import BrokerConfig
|
|
@@ -37,28 +36,6 @@ class FastStream(faststream.FastStream):
|
|
|
37
36
|
"""
|
|
38
37
|
return self.application.register(provider=provider)
|
|
39
38
|
|
|
40
|
-
def bind(
|
|
41
|
-
self,
|
|
42
|
-
source: type | Callable[..., Any],
|
|
43
|
-
*,
|
|
44
|
-
interface: type | None = None,
|
|
45
|
-
scope: dishka.BaseScope | None = None,
|
|
46
|
-
) -> None:
|
|
47
|
-
"""Binds a source to the container."""
|
|
48
|
-
self.application.bind(
|
|
49
|
-
source=source,
|
|
50
|
-
interface=interface,
|
|
51
|
-
scope=scope,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
55
|
-
"""Resolve and instanciate a type from the container.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Result containing the resolved type instance or an error message.
|
|
59
|
-
"""
|
|
60
|
-
return self.application.make(interface=interface)
|
|
61
|
-
|
|
62
39
|
@asynccontextmanager
|
|
63
40
|
async def lifespan(self) -> AsyncGenerator[None, None]:
|
|
64
41
|
"""Async context manager for the application lifespan."""
|
|
@@ -69,7 +46,7 @@ class FastStream(faststream.FastStream):
|
|
|
69
46
|
self,
|
|
70
47
|
) -> StatelessLifespan["FastStream"]:
|
|
71
48
|
@asynccontextmanager
|
|
72
|
-
async def composed_lifespan(
|
|
49
|
+
async def composed_lifespan(_: faststream.FastStream) -> AsyncIterator[None]:
|
|
73
50
|
async with self.lifespan():
|
|
74
51
|
yield
|
|
75
52
|
|
|
@@ -9,7 +9,17 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import abc
|
|
11
11
|
from contextlib import AbstractAsyncContextManager
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
TYPE_CHECKING,
|
|
14
|
+
Any,
|
|
15
|
+
Callable,
|
|
16
|
+
ClassVar,
|
|
17
|
+
Protocol,
|
|
18
|
+
Self,
|
|
19
|
+
runtime_checkable,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
import dishka
|
|
13
23
|
|
|
14
24
|
from neva import Result
|
|
15
25
|
|
|
@@ -67,8 +77,23 @@ class ServiceProvider(abc.ABC):
|
|
|
67
77
|
app: The application instance.
|
|
68
78
|
|
|
69
79
|
"""
|
|
80
|
+
self.provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
70
81
|
self.app = app
|
|
71
82
|
|
|
83
|
+
def bind(
|
|
84
|
+
self,
|
|
85
|
+
source: type | Callable[..., Any],
|
|
86
|
+
*,
|
|
87
|
+
interface: type | None = None,
|
|
88
|
+
scope: dishka.BaseScope | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Binds a source to the container."""
|
|
91
|
+
_ = self.provider.provide(
|
|
92
|
+
source=source,
|
|
93
|
+
scope=scope,
|
|
94
|
+
provides=interface,
|
|
95
|
+
)
|
|
96
|
+
|
|
72
97
|
@abc.abstractmethod
|
|
73
98
|
def register(self) -> Result[Self, str]:
|
|
74
99
|
"""Register services into the application container.
|
|
@@ -5,7 +5,7 @@ store for all application configuration values. It supports dot notation for
|
|
|
5
5
|
nested access and can be frozen to prevent modifications after initialization.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
from typing_extensions import TypeForm
|
|
11
11
|
|
|
@@ -65,14 +65,12 @@ class ConfigRepository:
|
|
|
65
65
|
def get[T](
|
|
66
66
|
self,
|
|
67
67
|
key: str,
|
|
68
|
-
default: T | None = None,
|
|
69
68
|
type_: TypeForm[T] | None = None,
|
|
70
69
|
) -> Result[T, str]:
|
|
71
70
|
"""Get a configuration value using dot notation.
|
|
72
71
|
|
|
73
72
|
Args:
|
|
74
73
|
key: Dot-notated key path (e.g., "database.host").
|
|
75
|
-
default: Default value to return if key is not found.
|
|
76
74
|
type_: Type to cast the value to.
|
|
77
75
|
|
|
78
76
|
Returns:
|
|
@@ -82,18 +80,14 @@ class ConfigRepository:
|
|
|
82
80
|
keys = key.split(".")
|
|
83
81
|
current = self._items
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return Ok(default)
|
|
90
|
-
return Err(f"Config key '{key}' not found")
|
|
83
|
+
for k in keys:
|
|
84
|
+
if not isinstance(current, dict):
|
|
85
|
+
return Err(f"Config key '{key}' not found")
|
|
86
|
+
try:
|
|
91
87
|
current = current[k]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return Ok(default)
|
|
96
|
-
return Err(f"Config key '{key}' not found")
|
|
88
|
+
except KeyError:
|
|
89
|
+
return Err(f"Config key '{key}' not found")
|
|
90
|
+
return Ok(cast(T, current))
|
|
97
91
|
|
|
98
92
|
def has(self, key: str) -> bool:
|
|
99
93
|
"""Check if a configuration key exists.
|
|
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
8
8
|
|
|
9
9
|
from neva import Err, Ok, Result
|
|
10
10
|
from neva.arch import ServiceProvider
|
|
11
|
+
from neva.database.config import ConnectionConfig, DatabaseConfig
|
|
11
12
|
from neva.database.connection import TransactionContext
|
|
12
13
|
from neva.database.manager import DatabaseManager
|
|
13
14
|
from neva.obs import LogManager
|
|
@@ -18,22 +19,21 @@ class DatabaseServiceProvider(ServiceProvider):
|
|
|
18
19
|
|
|
19
20
|
@override
|
|
20
21
|
def register(self) -> Result[Self, str]:
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
22
|
+
self.bind(TransactionContext)
|
|
23
|
+
self.bind(DatabaseManager)
|
|
23
24
|
return Ok(self)
|
|
24
25
|
|
|
25
26
|
@asynccontextmanager
|
|
26
27
|
async def lifespan(self) -> AsyncIterator[None]:
|
|
27
28
|
"""Initialize and cleanup database connections."""
|
|
28
|
-
logger: LogManager = self.app.make(LogManager).unwrap()
|
|
29
|
-
db: DatabaseManager = self.app.make(DatabaseManager).unwrap()
|
|
29
|
+
logger: LogManager = (await self.app.make(LogManager)).unwrap()
|
|
30
|
+
db: DatabaseManager = (await self.app.make(DatabaseManager)).unwrap()
|
|
30
31
|
logger.info("Beginning SQLAlchemy initialization...")
|
|
31
|
-
match self.app.config.get("database"):
|
|
32
|
+
match self.app.config.get("database", type_=DatabaseConfig):
|
|
32
33
|
case Ok(config):
|
|
33
|
-
connections: dict = config
|
|
34
|
+
connections: dict[str, ConnectionConfig] = config["connections"]
|
|
34
35
|
for name, conn_config in connections.items():
|
|
35
|
-
|
|
36
|
-
engine = create_async_engine(url, **conn_config)
|
|
36
|
+
engine = create_async_engine(**conn_config)
|
|
37
37
|
db.register_engine(name, engine)
|
|
38
38
|
logger.info(f"Registered engine for connection '{name}'.")
|
|
39
39
|
logger.info("SQLAlchemy initialization complete.")
|
|
@@ -127,7 +127,7 @@ class AesEncrypter:
|
|
|
127
127
|
ValueError: If no encryption key is configured.
|
|
128
128
|
"""
|
|
129
129
|
previous_keys = self._app.config.get(
|
|
130
|
-
"app.previous_keys",
|
|
130
|
+
"app.previous_keys", type_=list[str]
|
|
131
131
|
).unwrap_or([])
|
|
132
132
|
|
|
133
133
|
match self._app.config.get("app.key", type_=str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Hash manager for managing password hashing strategies."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import override
|
|
4
4
|
|
|
5
5
|
from neva import Option
|
|
6
6
|
from neva.arch import Application
|
|
@@ -108,15 +108,15 @@ class HashManager(StrategyResolver[Hasher]):
|
|
|
108
108
|
Configured Argon2Hasher instance.
|
|
109
109
|
"""
|
|
110
110
|
argon_config = manager.app.config.get(
|
|
111
|
-
"hashing.argon",
|
|
111
|
+
"hashing.argon", type_=Argon2Config
|
|
112
112
|
).unwrap()
|
|
113
113
|
|
|
114
114
|
return Argon2Hasher(
|
|
115
|
-
time_cost=
|
|
116
|
-
memory_cost=
|
|
117
|
-
parallelism=
|
|
118
|
-
hash_len=
|
|
119
|
-
salt_len=
|
|
115
|
+
time_cost=argon_config.get("time_cost", 2),
|
|
116
|
+
memory_cost=argon_config.get("memory_cost", 102400),
|
|
117
|
+
parallelism=argon_config.get("parallelism", 8),
|
|
118
|
+
hash_len=argon_config.get("hash_len", 16),
|
|
119
|
+
salt_len=argon_config.get("salt_len", 16),
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
def _create_bcrypt_hasher(self, manager: StrategyResolver[Hasher]) -> BcryptHasher:
|
|
@@ -129,10 +129,10 @@ class HashManager(StrategyResolver[Hasher]):
|
|
|
129
129
|
Configured BcryptHasher instance.
|
|
130
130
|
"""
|
|
131
131
|
bcrypt_config = manager.app.config.get(
|
|
132
|
-
"hashing.bcrypt",
|
|
132
|
+
"hashing.bcrypt", type_=BcryptConfig
|
|
133
133
|
).unwrap()
|
|
134
134
|
|
|
135
135
|
return BcryptHasher(
|
|
136
|
-
rounds=
|
|
137
|
-
prefix=
|
|
136
|
+
rounds=bcrypt_config.get("rounds", 12),
|
|
137
|
+
prefix=bcrypt_config.get("prefix", "2b"),
|
|
138
138
|
)
|
|
@@ -12,6 +12,6 @@ class SecurityProvider(arch.ServiceProvider):
|
|
|
12
12
|
|
|
13
13
|
@override
|
|
14
14
|
def register(self) -> Result[Self, str]:
|
|
15
|
-
self.
|
|
16
|
-
self.
|
|
15
|
+
self.bind(HashManager, interface=HashManager)
|
|
16
|
+
self.bind(AesEncrypter, interface=Encrypter)
|
|
17
17
|
return Ok(self)
|
|
@@ -95,7 +95,7 @@ class RefreshDatabase:
|
|
|
95
95
|
_setup_database_engine: None, # ordering dependency: runs after engine setup
|
|
96
96
|
) -> AsyncIterator[None]:
|
|
97
97
|
"""Wrap the test in a DB transaction and roll it back on completion."""
|
|
98
|
-
db = _test_case_application.make(DatabaseManager).unwrap()
|
|
98
|
+
db = (await _test_case_application.make(DatabaseManager)).unwrap()
|
|
99
99
|
async with db.begin() as tx:
|
|
100
100
|
tx.share()
|
|
101
101
|
yield
|
|
@@ -7,13 +7,13 @@ packages = ["neva"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "python-neva"
|
|
10
|
-
version = "
|
|
10
|
+
version = "3.0.0"
|
|
11
11
|
description = "Add your description here"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"cryptography>=46.0.3",
|
|
16
|
-
"dishka>=1.
|
|
16
|
+
"dishka>=1.10.0",
|
|
17
17
|
"fastapi[all]>=0.129.0",
|
|
18
18
|
"faststream>=0.6.6",
|
|
19
19
|
"pwdlib[argon2,bcrypt]>=0.3.0",
|
|
@@ -47,7 +47,8 @@ dev = [
|
|
|
47
47
|
enableExperimentalFeatures = true
|
|
48
48
|
|
|
49
49
|
[tool.mypy]
|
|
50
|
-
enable_incomplete_feature = [
|
|
50
|
+
enable_incomplete_feature = ["TypeForm"]
|
|
51
|
+
plugins = ["pydantic.mypy"]
|
|
51
52
|
|
|
52
53
|
[tool.pytest.ini_options]
|
|
53
54
|
asyncio_mode = "auto"
|