python-neva 0.6.0.dev4__tar.gz → 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.
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/.pre-commit-config.yaml +5 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/PKG-INFO +2 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/arch/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/arch/application.py +27 -4
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/arch/facade.py +2 -1
- python_neva-1.0.0/neva/arch/faststream.py +76 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/arch/service_provider.py +10 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/config/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/config/base_providers.py +7 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/config/repository.py +1 -1
- python_neva-1.0.0/neva/database/__init__.py +16 -0
- python_neva-1.0.0/neva/database/connection.py +124 -0
- python_neva-1.0.0/neva/database/manager.py +44 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/database/provider.py +3 -0
- python_neva-1.0.0/neva/database/transaction.py +96 -0
- python_neva-1.0.0/neva/events/__init__.py +23 -0
- python_neva-1.0.0/neva/events/dispatcher.py +89 -0
- python_neva-1.0.0/neva/events/event.py +14 -0
- python_neva-1.0.0/neva/events/event_registry.py +44 -0
- python_neva-1.0.0/neva/events/listener.py +70 -0
- python_neva-1.0.0/neva/events/provider.py +27 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/obs/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/obs/logging/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/obs/logging/manager.py +0 -1
- python_neva-1.0.0/neva/obs/middleware/__init__.py +7 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/obs/middleware/profiler.py +4 -4
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/__init__.py +2 -4
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/encryption/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/encryption/encrypter.py +1 -4
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/encryption/protocol.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/__init__.py +1 -0
- python_neva-1.0.0/neva/security/tokens/__init__.py +8 -0
- python_neva-1.0.0/neva/security/tokens/generate_token.py +16 -0
- python_neva-1.0.0/neva/security/tokens/hash_token.py +16 -0
- python_neva-0.6.0.dev4/neva/security/tokens/token_hash.py → python_neva-1.0.0/neva/security/tokens/verify_token.py +2 -14
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/__init__.py +3 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/app.py +1 -1
- python_neva-1.0.0/neva/support/facade/db.py +16 -0
- python_neva-1.0.0/neva/support/facade/db.pyi +39 -0
- python_neva-1.0.0/neva/support/facade/event.py +15 -0
- python_neva-1.0.0/neva/support/facade/event.pyi +22 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/hash.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/hash.pyi +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/testing/__init__.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/testing/http.py +2 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/testing/test_case.py +1 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/pyproject.toml +2 -4
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/ruff.toml +9 -1
- python_neva-1.0.0/tests/arch/__init__.py +1 -0
- {python_neva-0.6.0.dev4/tests → python_neva-1.0.0/tests/arch}/test_scope.py +1 -1
- python_neva-1.0.0/tests/config/__init__.py +1 -0
- python_neva-1.0.0/tests/config/test_loader.py +97 -0
- python_neva-1.0.0/tests/config/test_repository.py +193 -0
- python_neva-1.0.0/tests/database/__init__.py +1 -0
- python_neva-1.0.0/tests/database/conftest.py +30 -0
- python_neva-1.0.0/tests/database/test_connection_manager.py +84 -0
- python_neva-1.0.0/tests/database/test_database_manager.py +59 -0
- python_neva-1.0.0/tests/database/test_edge_cases.py +138 -0
- python_neva-1.0.0/tests/database/test_multi_connection.py +133 -0
- python_neva-1.0.0/tests/database/test_transaction.py +170 -0
- python_neva-1.0.0/tests/database/test_transaction_context.py +54 -0
- python_neva-1.0.0/tests/database/test_transaction_registry.py +42 -0
- python_neva-1.0.0/tests/events/__init__.py +1 -0
- python_neva-1.0.0/tests/events/conftest.py +59 -0
- python_neva-1.0.0/tests/events/test_deferred.py +109 -0
- python_neva-1.0.0/tests/events/test_dispatch.py +95 -0
- python_neva-1.0.0/tests/events/test_function_listener.py +117 -0
- python_neva-1.0.0/tests/events/test_immediate.py +45 -0
- python_neva-1.0.0/tests/obs/__init__.py +1 -0
- python_neva-1.0.0/tests/obs/test_correlation.py +123 -0
- python_neva-1.0.0/tests/obs/test_profiler.py +86 -0
- python_neva-1.0.0/tests/security/__init__.py +1 -0
- python_neva-1.0.0/tests/testing/__init__.py +1 -0
- {python_neva-0.6.0.dev4/tests → python_neva-1.0.0/tests/testing}/test_fixtures.py +0 -1
- {python_neva-0.6.0.dev4/tests → python_neva-1.0.0/tests/testing}/test_test_case.py +3 -2
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/uv.lock +35 -1
- python_neva-0.6.0.dev4/neva/console/__init__.py +0 -1
- python_neva-0.6.0.dev4/neva/console/kernel.py +0 -47
- python_neva-0.6.0.dev4/neva/console/runner.py +0 -26
- python_neva-0.6.0.dev4/neva/database/__init__.py +0 -1
- python_neva-0.6.0.dev4/neva/database/manager.py +0 -15
- python_neva-0.6.0.dev4/neva/database/repository.py +0 -49
- python_neva-0.6.0.dev4/neva/events/__init__.py +0 -1
- python_neva-0.6.0.dev4/neva/events/dispatcher.py +0 -23
- python_neva-0.6.0.dev4/neva/events/event.py +0 -10
- python_neva-0.6.0.dev4/neva/events/event_registry.py +0 -20
- python_neva-0.6.0.dev4/neva/events/interface.py +0 -23
- python_neva-0.6.0.dev4/neva/events/listener.py +0 -21
- python_neva-0.6.0.dev4/neva/obs/middleware/__init__.py +0 -1
- python_neva-0.6.0.dev4/neva/security/tokens/__init__.py +0 -5
- python_neva-0.6.0.dev4/specifications/future_ideas.md +0 -21
- python_neva-0.6.0.dev4/specifications/security.md +0 -801
- python_neva-0.6.0.dev4/tests/test_example_usage.py +0 -175
- python_neva-0.6.0.dev4/wiki/architecture/01-overview.md +0 -54
- python_neva-0.6.0.dev4/wiki/architecture/02-dependency-injection.md +0 -133
- python_neva-0.6.0.dev4/wiki/architecture/03-service-providers.md +0 -217
- python_neva-0.6.0.dev4/wiki/architecture/04-facades.md +0 -199
- python_neva-0.6.0.dev4/wiki/architecture/05-application-lifecycle.md +0 -271
- python_neva-0.6.0.dev4/wiki/architecture/06-result-option.md +0 -277
- python_neva-0.6.0.dev4/wiki/configuration/01-overview.md +0 -59
- python_neva-0.6.0.dev4/wiki/configuration/02-configuration-files.md +0 -178
- python_neva-0.6.0.dev4/wiki/configuration/03-accessing-configuration.md +0 -129
- python_neva-0.6.0.dev4/wiki/configuration/04-config-repository.md +0 -119
- python_neva-0.6.0.dev4/wiki/configuration/05-loading-process.md +0 -154
- python_neva-0.6.0.dev4/wiki/configuration/06-configuration-in-providers.md +0 -174
- python_neva-0.6.0.dev4/wiki/testing/01-introduction.md +0 -68
- python_neva-0.6.0.dev4/wiki/testing/02-test-case.md +0 -87
- python_neva-0.6.0.dev4/wiki/testing/03-fixtures.md +0 -190
- python_neva-0.6.0.dev4/wiki/testing/04-http-testing.md +0 -253
- python_neva-0.6.0.dev4/wiki/testing/05-custom-configuration.md +0 -355
- python_neva-0.6.0.dev4/wiki/testing/06-test-isolation.md +0 -263
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/.envrc +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/.gitignore +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/.python-version +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/README.md +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/arch/app.py +1 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/arch/config.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/config/loader.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/config/provider.py +1 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/database/config.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/obs/logging/provider.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/obs/middleware/correlation.py +1 -1
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/py.typed +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/config.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/security/provider.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/accessors.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/config.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/log.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/results.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/strategy.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/strconv.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/support/time.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/neva/testing/fixtures.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/tests/__init__.py +0 -0
- {python_neva-0.6.0.dev4 → python_neva-1.0.0}/tests/conftest.py +0 -0
- {python_neva-0.6.0.dev4/tests → python_neva-1.0.0/tests/security}/test_encrypter.py +0 -0
- {python_neva-0.6.0.dev4/tests → python_neva-1.0.0/tests/security}/test_hash_manager.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: cryptography>=46.0.3
|
|
7
7
|
Requires-Dist: dishka>=1.7.2
|
|
8
8
|
Requires-Dist: fastapi[all]>=0.124.0
|
|
9
|
+
Requires-Dist: faststream>=0.6.6
|
|
9
10
|
Requires-Dist: flexmock>=0.13.0
|
|
10
11
|
Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
|
|
11
12
|
Requires-Dist: pyinstrument>=5.1.1
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Base application for DI and facade injection."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from collections.abc import AsyncIterator, Iterator
|
|
4
5
|
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
|
5
|
-
import os
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Callable, Self
|
|
8
8
|
|
|
@@ -10,8 +10,8 @@ import dishka
|
|
|
10
10
|
from dishka.integrations.fastapi import FastapiProvider
|
|
11
11
|
|
|
12
12
|
from neva import Err, Ok, Result
|
|
13
|
-
from neva.arch.service_provider import Bootable, ServiceProvider
|
|
14
13
|
from neva.arch.facade import Facade
|
|
14
|
+
from neva.arch.service_provider import Bootable, ServiceProvider
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Application:
|
|
@@ -27,9 +27,9 @@ class Application:
|
|
|
27
27
|
Raises:
|
|
28
28
|
RuntimeError: If the application fails to initialize.
|
|
29
29
|
"""
|
|
30
|
-
from neva.config.repository import ConfigRepository
|
|
31
|
-
from neva.config.provider import ConfigServiceProvider
|
|
32
30
|
from neva.config.base_providers import base_providers
|
|
31
|
+
from neva.config.provider import ConfigServiceProvider
|
|
32
|
+
from neva.config.repository import ConfigRepository
|
|
33
33
|
|
|
34
34
|
self.providers: dict[type, ServiceProvider] = {}
|
|
35
35
|
self.di_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
@@ -65,6 +65,7 @@ class Application:
|
|
|
65
65
|
case Err(e):
|
|
66
66
|
raise RuntimeError(f"Failed to load configuration during boot: {e}")
|
|
67
67
|
|
|
68
|
+
self._bind_event_listeners()
|
|
68
69
|
self.container = dishka.make_container(self.di_provider)
|
|
69
70
|
|
|
70
71
|
def bind_to_fastapi(self) -> None:
|
|
@@ -141,6 +142,28 @@ class Application:
|
|
|
141
142
|
if isinstance(provider, Bootable):
|
|
142
143
|
await stack.enter_async_context(provider.lifespan())
|
|
143
144
|
|
|
145
|
+
self._wire_event_listeners()
|
|
144
146
|
yield
|
|
145
147
|
|
|
146
148
|
Facade.reset_facade_application()
|
|
149
|
+
|
|
150
|
+
def _bind_event_listeners(self) -> None:
|
|
151
|
+
"""Bind all listener classes declared in provider ``listen`` dicts."""
|
|
152
|
+
for provider in self.providers.values():
|
|
153
|
+
for listeners in provider.listen.values():
|
|
154
|
+
for listener_cls in listeners:
|
|
155
|
+
self.bind(listener_cls)
|
|
156
|
+
|
|
157
|
+
def _wire_event_listeners(self) -> None:
|
|
158
|
+
"""Wire event-listener mappings from all providers onto the dispatcher."""
|
|
159
|
+
from neva.events.dispatcher import EventDispatcher
|
|
160
|
+
|
|
161
|
+
result = self.make(EventDispatcher)
|
|
162
|
+
if result.is_err:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
dispatcher = result.unwrap()
|
|
166
|
+
for provider in self.providers.values():
|
|
167
|
+
for event_cls, listeners in provider.listen.items():
|
|
168
|
+
for listener_cls in listeners:
|
|
169
|
+
dispatcher.listen(event_cls, listener_cls)
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, ClassVar
|
|
|
11
11
|
from neva import Option, Result, from_optional
|
|
12
12
|
from neva.support.accessors import get_attr
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from neva.arch.application import Application
|
|
16
17
|
|
|
@@ -91,7 +92,7 @@ class FacadeMeta(ABCMeta):
|
|
|
91
92
|
cls._get_app()
|
|
92
93
|
.ok_or(
|
|
93
94
|
f"A facade root (App instance) has not been set for {cls.__name__}. "
|
|
94
|
-
|
|
95
|
+
"Call Facade.set_facade_application(app) first."
|
|
95
96
|
)
|
|
96
97
|
.and_then(
|
|
97
98
|
lambda x: cls._resolve_facade_instance(
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""FastStream wrapper for Neva."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import dishka
|
|
8
|
+
import faststream
|
|
9
|
+
from faststream._internal.broker import BrokerUsecase
|
|
10
|
+
from faststream._internal.configs import BrokerConfig
|
|
11
|
+
from starlette.types import StatelessLifespan
|
|
12
|
+
|
|
13
|
+
from neva import Result
|
|
14
|
+
from neva.arch import Application, ServiceProvider
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FastStream(faststream.FastStream):
|
|
18
|
+
"""FastStream wrapper for Neva."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
broker: BrokerUsecase[Any, Any, BrokerConfig],
|
|
23
|
+
config_path: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Initialize the FastStream wrapper."""
|
|
26
|
+
self.application: Application = Application(config_path=config_path)
|
|
27
|
+
super().__init__(broker, lifespan=self._create_lifespan())
|
|
28
|
+
|
|
29
|
+
def register(
|
|
30
|
+
self,
|
|
31
|
+
provider: type[ServiceProvider],
|
|
32
|
+
) -> Result[ServiceProvider, str]:
|
|
33
|
+
"""Registers a service provider with the application.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Result containing the registered provider instance or an error message.
|
|
37
|
+
"""
|
|
38
|
+
return self.application.register(provider=provider)
|
|
39
|
+
|
|
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
|
+
@asynccontextmanager
|
|
63
|
+
async def lifespan(self) -> AsyncGenerator[None, None]:
|
|
64
|
+
"""Async context manager for the application lifespan."""
|
|
65
|
+
async with self.application.lifespan():
|
|
66
|
+
yield
|
|
67
|
+
|
|
68
|
+
def _create_lifespan(
|
|
69
|
+
self,
|
|
70
|
+
) -> StatelessLifespan["FastStream"]:
|
|
71
|
+
@asynccontextmanager
|
|
72
|
+
async def composed_lifespan(app: faststream.FastStream) -> AsyncIterator[None]:
|
|
73
|
+
async with self.lifespan():
|
|
74
|
+
yield
|
|
75
|
+
|
|
76
|
+
return composed_lifespan
|
|
@@ -5,14 +5,19 @@ Service providers are responsible for binding services into the dependency injec
|
|
|
5
5
|
container and optionally managing their lifecycle.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
8
10
|
import abc
|
|
9
11
|
from contextlib import AbstractAsyncContextManager
|
|
10
|
-
from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
|
|
12
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, Self, runtime_checkable
|
|
11
13
|
|
|
12
14
|
from neva import Result
|
|
13
15
|
|
|
16
|
+
|
|
14
17
|
if TYPE_CHECKING:
|
|
15
18
|
from neva.arch.application import Application
|
|
19
|
+
from neva.events.event import Event
|
|
20
|
+
from neva.events.listener import EventListener
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
@runtime_checkable
|
|
@@ -46,10 +51,14 @@ class ServiceProvider(abc.ABC):
|
|
|
46
51
|
|
|
47
52
|
Attributes:
|
|
48
53
|
app: The application instance.
|
|
54
|
+
listen: A mapping of event types to listener classes. Listeners
|
|
55
|
+
declared here are automatically bound into the container during
|
|
56
|
+
registration and wired to the event dispatcher during boot.
|
|
49
57
|
|
|
50
58
|
"""
|
|
51
59
|
|
|
52
60
|
app: "Application"
|
|
61
|
+
listen: ClassVar[dict[type[Event], list[type[EventListener[Any]]]]] = {}
|
|
53
62
|
|
|
54
63
|
def __init__(self, app: "Application") -> None:
|
|
55
64
|
"""Initialize the service provider.
|
|
@@ -6,6 +6,8 @@ to function properly.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from neva.arch import ServiceProvider
|
|
9
|
+
from neva.database import DatabaseServiceProvider
|
|
10
|
+
from neva.events.provider import EventServiceProvider
|
|
9
11
|
from neva.obs import LogServiceProvider
|
|
10
12
|
|
|
11
13
|
|
|
@@ -22,4 +24,8 @@ def base_providers() -> set[type[ServiceProvider]]:
|
|
|
22
24
|
Set of service provider classes to register.
|
|
23
25
|
|
|
24
26
|
"""
|
|
25
|
-
return {
|
|
27
|
+
return {
|
|
28
|
+
DatabaseServiceProvider,
|
|
29
|
+
EventServiceProvider,
|
|
30
|
+
LogServiceProvider,
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Database module."""
|
|
2
|
+
|
|
3
|
+
from neva.database.config import DatabaseConfig
|
|
4
|
+
from neva.database.connection import TransactionContext
|
|
5
|
+
from neva.database.manager import DatabaseManager
|
|
6
|
+
from neva.database.provider import DatabaseServiceProvider
|
|
7
|
+
from neva.database.transaction import Transaction
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DatabaseConfig",
|
|
12
|
+
"DatabaseManager",
|
|
13
|
+
"DatabaseServiceProvider",
|
|
14
|
+
"Transaction",
|
|
15
|
+
"TransactionContext",
|
|
16
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Connection manager."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import final
|
|
8
|
+
|
|
9
|
+
from tortoise.transactions import in_transaction
|
|
10
|
+
|
|
11
|
+
from neva import Nothing, Option, Some, from_optional
|
|
12
|
+
from neva.database.transaction import Transaction, TransactionState
|
|
13
|
+
from neva.obs import LogManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TransactionRegistry:
|
|
18
|
+
"""A registry of ongoing transactions."""
|
|
19
|
+
|
|
20
|
+
by_connection: dict[str, Transaction] = field(default_factory=dict)
|
|
21
|
+
stack: list[Transaction] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
def extend(self, transaction: Transaction) -> "TransactionRegistry":
|
|
24
|
+
"""Adds a new transaction to the registry.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A new registry with the new transaction added.
|
|
28
|
+
"""
|
|
29
|
+
return TransactionRegistry(
|
|
30
|
+
by_connection={**self.by_connection, transaction.conn_name: transaction},
|
|
31
|
+
stack=[*self.stack, transaction],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_tx_registry: ContextVar[TransactionRegistry | None] = ContextVar(
|
|
36
|
+
"_tx_registry",
|
|
37
|
+
default=None,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TransactionContext:
|
|
42
|
+
"""Transaction context."""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
if _tx_registry.get() is None:
|
|
46
|
+
_ = _tx_registry.set(TransactionRegistry())
|
|
47
|
+
|
|
48
|
+
def current(self, connection: str | None = None) -> Option[Transaction]:
|
|
49
|
+
"""Get the current transaction.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The current transaction, if any.
|
|
53
|
+
"""
|
|
54
|
+
registry = _tx_registry.get()
|
|
55
|
+
if registry is None:
|
|
56
|
+
return Nothing()
|
|
57
|
+
|
|
58
|
+
if connection is not None:
|
|
59
|
+
tx = registry.by_connection.get(connection)
|
|
60
|
+
return from_optional(tx)
|
|
61
|
+
|
|
62
|
+
if registry.stack:
|
|
63
|
+
return Some(registry.stack[-1])
|
|
64
|
+
return Nothing()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@final
|
|
68
|
+
class ConnectionManager:
|
|
69
|
+
"""Connection manager."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
tx_context: TransactionContext,
|
|
75
|
+
logger: LogManager | None,
|
|
76
|
+
) -> None:
|
|
77
|
+
self.name = name
|
|
78
|
+
self.tx_context = tx_context
|
|
79
|
+
self.logger = logger
|
|
80
|
+
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def transaction(
|
|
83
|
+
self,
|
|
84
|
+
) -> AsyncIterator[Transaction]:
|
|
85
|
+
"""Open a new transaction.
|
|
86
|
+
|
|
87
|
+
Yields:
|
|
88
|
+
Transaction: The new transaction.
|
|
89
|
+
"""
|
|
90
|
+
parent = self.tx_context.current(self.name)
|
|
91
|
+
tx = Transaction(self.name)
|
|
92
|
+
tx.parent = parent
|
|
93
|
+
old_registry = _tx_registry.get()
|
|
94
|
+
token = (
|
|
95
|
+
_tx_registry.set(old_registry.extend(tx))
|
|
96
|
+
if old_registry is not None
|
|
97
|
+
else _tx_registry.set(TransactionRegistry())
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
async with in_transaction(self.name):
|
|
102
|
+
yield tx
|
|
103
|
+
tx.state = TransactionState.COMMITTED
|
|
104
|
+
if tx.is_root:
|
|
105
|
+
for result in await tx.execute_on_commit_callbacks():
|
|
106
|
+
if result.is_err and self.logger is not None:
|
|
107
|
+
self.logger.error(
|
|
108
|
+
"commit callback failed",
|
|
109
|
+
error=result.err().unwrap(),
|
|
110
|
+
connection=self.name,
|
|
111
|
+
)
|
|
112
|
+
except BaseException:
|
|
113
|
+
tx.state = TransactionState.ROLLED_BACK
|
|
114
|
+
if tx.is_root:
|
|
115
|
+
for result in await tx.execute_on_rollback_callbacks():
|
|
116
|
+
if result.is_err and self.logger is not None:
|
|
117
|
+
self.logger.error(
|
|
118
|
+
"rollback callback failed",
|
|
119
|
+
error=result.err().unwrap(),
|
|
120
|
+
connection=self.name,
|
|
121
|
+
)
|
|
122
|
+
raise
|
|
123
|
+
finally:
|
|
124
|
+
_tx_registry.reset(token)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Database manager."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import final
|
|
6
|
+
|
|
7
|
+
from neva import Option
|
|
8
|
+
from neva.database.connection import ConnectionManager, TransactionContext
|
|
9
|
+
from neva.database.transaction import Transaction
|
|
10
|
+
from neva.obs import LogManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@final
|
|
14
|
+
class DatabaseManager:
|
|
15
|
+
"""Database manager."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, tx_context: TransactionContext, logger: LogManager) -> None:
|
|
18
|
+
self._tx_context = tx_context
|
|
19
|
+
self._logger = logger
|
|
20
|
+
self._connections: dict[str, ConnectionManager] = {}
|
|
21
|
+
|
|
22
|
+
def connection(self, name: str) -> ConnectionManager:
|
|
23
|
+
"""Returns a new connection manager."""
|
|
24
|
+
return self._connections.setdefault(
|
|
25
|
+
name, ConnectionManager(name, self._tx_context, self._logger)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def current(self, connection: str | None = None) -> Option[Transaction]:
|
|
29
|
+
"""Returns the current transaction.
|
|
30
|
+
|
|
31
|
+
If no connection is specified, this will return the most recent transaction on
|
|
32
|
+
any connection. This is something to keep in mind.
|
|
33
|
+
"""
|
|
34
|
+
return self._tx_context.current(connection)
|
|
35
|
+
|
|
36
|
+
@asynccontextmanager
|
|
37
|
+
async def transaction(self, name: str = "default") -> AsyncIterator[Transaction]:
|
|
38
|
+
"""Returns a transaction to the default connection.
|
|
39
|
+
|
|
40
|
+
Yields:
|
|
41
|
+
Transaction: The transaction.
|
|
42
|
+
"""
|
|
43
|
+
async with self.connection(name).transaction() as tx:
|
|
44
|
+
yield tx
|
|
@@ -5,8 +5,10 @@ from contextlib import asynccontextmanager
|
|
|
5
5
|
from typing import Self, override
|
|
6
6
|
|
|
7
7
|
from tortoise import Tortoise
|
|
8
|
+
|
|
8
9
|
from neva import Err, Ok, Result
|
|
9
10
|
from neva.arch import ServiceProvider
|
|
11
|
+
from neva.database.connection import TransactionContext
|
|
10
12
|
from neva.database.manager import DatabaseManager
|
|
11
13
|
from neva.support.facade import Config, Log
|
|
12
14
|
|
|
@@ -16,6 +18,7 @@ class DatabaseServiceProvider(ServiceProvider):
|
|
|
16
18
|
|
|
17
19
|
@override
|
|
18
20
|
def register(self) -> Result[Self, str]:
|
|
21
|
+
self.app.bind(TransactionContext)
|
|
19
22
|
self.app.bind(DatabaseManager)
|
|
20
23
|
return Ok(self)
|
|
21
24
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Transaction management systems."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from typing import Callable, Self
|
|
7
|
+
|
|
8
|
+
from neva import Err, Nothing, Option, Result, Some
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
type TransactionCallback = Callable[[], Awaitable[Result[None, str]]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TransactionState(Enum):
|
|
15
|
+
"""Represents the state of a transaction."""
|
|
16
|
+
|
|
17
|
+
ACTIVE = auto()
|
|
18
|
+
COMMITTED = auto()
|
|
19
|
+
ROLLED_BACK = auto()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Transaction:
|
|
24
|
+
"""Represents a database transaction with callback support."""
|
|
25
|
+
|
|
26
|
+
conn_name: str
|
|
27
|
+
state: TransactionState = field(default=TransactionState.ACTIVE, init=False)
|
|
28
|
+
_on_commit: list[TransactionCallback] = field(default_factory=list, init=False)
|
|
29
|
+
_on_rollback: list[TransactionCallback] = field(default_factory=list, init=False)
|
|
30
|
+
parent: Option["Transaction"] = field(default_factory=Nothing, init=False)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_active(self) -> bool:
|
|
34
|
+
"""Determine if the transaction is active."""
|
|
35
|
+
return self.state == TransactionState.ACTIVE
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_root(self) -> bool:
|
|
39
|
+
"""Determine if the transaction is the root transaction."""
|
|
40
|
+
return self.parent.is_nothing
|
|
41
|
+
|
|
42
|
+
def on_commit(self, callback: TransactionCallback) -> Self:
|
|
43
|
+
"""Register a callback to be called when the root transaction is committed.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Self: The current transaction, for chaining purposes.
|
|
47
|
+
"""
|
|
48
|
+
match self.parent:
|
|
49
|
+
case Some(parent):
|
|
50
|
+
_ = parent.on_commit(callback)
|
|
51
|
+
case Nothing():
|
|
52
|
+
self._on_commit.append(callback)
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def on_rollback(self, callback: TransactionCallback) -> Self:
|
|
56
|
+
"""Register a callback to be called when the root transaction is rolled back.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Self: The current transaction, for chaining purposes.
|
|
60
|
+
"""
|
|
61
|
+
match self.parent:
|
|
62
|
+
case Some(parent):
|
|
63
|
+
_ = parent.on_rollback(callback)
|
|
64
|
+
case Nothing():
|
|
65
|
+
self._on_rollback.append(callback)
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def execute_on_commit_callbacks(self) -> list[Result[None, str]]:
|
|
69
|
+
"""Execute all registered commit callbacks.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A list of results from each callback.
|
|
73
|
+
"""
|
|
74
|
+
results: list[Result[None, str]] = []
|
|
75
|
+
for callback in self._on_commit:
|
|
76
|
+
try:
|
|
77
|
+
results.append(await callback())
|
|
78
|
+
except Exception as e:
|
|
79
|
+
results.append(Err(f"Callback raised: {e}"))
|
|
80
|
+
self._on_commit.clear()
|
|
81
|
+
return results
|
|
82
|
+
|
|
83
|
+
async def execute_on_rollback_callbacks(self) -> list[Result[None, str]]:
|
|
84
|
+
"""Execute all registered rollback callbacks.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A list of results from each callback.
|
|
88
|
+
"""
|
|
89
|
+
results: list[Result[None, str]] = []
|
|
90
|
+
for callback in self._on_rollback:
|
|
91
|
+
try:
|
|
92
|
+
results.append(await callback())
|
|
93
|
+
except Exception as e:
|
|
94
|
+
results.append(Err(f"Callback raised: {e}"))
|
|
95
|
+
self._on_rollback.clear()
|
|
96
|
+
return results
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Defines the events system.
|
|
2
|
+
|
|
3
|
+
This module defines the event system, which is used to dispatch events to listener.
|
|
4
|
+
Three components are of particular interest:
|
|
5
|
+
- The base Event class, which is used to define events.
|
|
6
|
+
- The @listener decorator, which is used to define listeners from handler functions.
|
|
7
|
+
- The EventDispatcher class, which is used to registers listeners and dispatch events.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from neva.events.dispatcher import EventDispatcher
|
|
11
|
+
from neva.events.event import Event
|
|
12
|
+
from neva.events.listener import EventListener, HandlingPolicy, listener
|
|
13
|
+
from neva.events.provider import EventServiceProvider
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Event",
|
|
18
|
+
"EventDispatcher",
|
|
19
|
+
"EventListener",
|
|
20
|
+
"EventServiceProvider",
|
|
21
|
+
"HandlingPolicy",
|
|
22
|
+
"listener",
|
|
23
|
+
]
|