python-neva 3.4.0__tar.gz → 3.5.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-3.4.0 → python_neva-3.5.0}/CHANGELOG.md +16 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/PKG-INFO +1 -1
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/dispatcher.py +4 -5
- python_neva-3.5.0/neva/support/facade/app.pyi +47 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/event.py +2 -2
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/event.pyi +7 -2
- {python_neva-3.4.0 → python_neva-3.5.0}/pyproject.toml +1 -1
- python_neva-3.5.0/tests/events/test_before_dispatch.py +177 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/uv.lock +1 -1
- python_neva-3.4.0/neva/support/facade/app.pyi +0 -25
- {python_neva-3.4.0 → python_neva-3.5.0}/.envrc +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/.gitignore +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/.pre-commit-config.yaml +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/.python-version +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/README.md +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/application.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/facade.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/markers.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/scopes.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/service_provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/base_providers.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/loader.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/repository.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/connection.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/transaction.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/dispatcher.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/event.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/handler.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/listener.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/event.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/event_registry.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/listener.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/policy.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/logging/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/factories.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/persistence.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/provider.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/accessors.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/app.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/config.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/db.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/hash.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/hash.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/log.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/results.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/strategy.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/strconv.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/time.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/fakes.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/fixtures.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/py.typed +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/test_case.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/ruff.toml +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_cache.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_context.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_extends.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_scope.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/config/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/config/test_loader.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/config/test_repository.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/conftest.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_sqlalchemy_integration.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_transaction.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_transaction_context.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/conftest.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_binding.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_deferred.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_dispatch.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_event.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_immediate.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_listen_on_parent_class.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/obs/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/obs/test_correlation.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/obs/test_profiler.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/security/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/security/test_encrypter.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/security/test_hash_manager.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/__init__.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_event_fake.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_fixtures.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_refresh_database.py +0 -0
- {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_test_case.py +0 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 3.5.0 (2026-06-11)
|
|
2
|
+
|
|
3
|
+
### ✨ Features
|
|
4
|
+
|
|
5
|
+
- **arch**: improve app facade stub
|
|
6
|
+
- **events**: make the event facade properly depend on the dispatcher contract
|
|
7
|
+
- **events**: new tests for events
|
|
8
|
+
|
|
9
|
+
### 🐛🚑️ Fixes
|
|
10
|
+
|
|
11
|
+
- **events**: fixing handling of async before-dispatch hooks
|
|
12
|
+
|
|
13
|
+
### 🏷️ Types
|
|
14
|
+
|
|
15
|
+
- **events**: improve event facade stub
|
|
16
|
+
|
|
1
17
|
## 3.4.0 (2026-06-10)
|
|
2
18
|
|
|
3
19
|
### ✨ Features
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Base implementation of the event dispatcher."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
from typing import override
|
|
4
5
|
|
|
5
6
|
from neva.arch.application import Application
|
|
@@ -35,11 +36,9 @@ class EventDispatcher(contracts.EventDispatcher):
|
|
|
35
36
|
event: The event about to be dispatched.
|
|
36
37
|
"""
|
|
37
38
|
for hook in self._before_dispatch_hooks:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
case contracts.SyncBeforeDispatchHook():
|
|
42
|
-
hook(event)
|
|
39
|
+
result = hook(event)
|
|
40
|
+
if inspect.isawaitable(result):
|
|
41
|
+
await result
|
|
43
42
|
|
|
44
43
|
@override
|
|
45
44
|
async def before_dispatch(self, hook: contracts.BeforeDispatchHook) -> None:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Type stub for App facade."""
|
|
2
|
+
|
|
3
|
+
from contextlib import AbstractAsyncContextManager
|
|
4
|
+
from typing import Any, Self, override
|
|
5
|
+
|
|
6
|
+
from neva.arch import Facade
|
|
7
|
+
from neva.arch.scopes import BaseScope
|
|
8
|
+
from neva.support import Result
|
|
9
|
+
|
|
10
|
+
class App(Facade):
|
|
11
|
+
@classmethod
|
|
12
|
+
@override
|
|
13
|
+
def get_facade_accessor(cls) -> type: ...
|
|
14
|
+
@classmethod
|
|
15
|
+
async def make_async[T](cls, interface: type[T]) -> Result[T, str]:
|
|
16
|
+
"""Resolve and instanciate a type from the container.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Result containing the resolved type instance or an error message.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def make[T](cls, interface: type[T]) -> Result[T, str]:
|
|
24
|
+
"""Resolve an interface from the container by its alias.
|
|
25
|
+
|
|
26
|
+
Attempts to retrieve and instantiate an object from the dependency
|
|
27
|
+
injection container.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
interface: The interface to resolve.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Result containing the resolved service instance or an error message.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
async def scope(
|
|
39
|
+
cls,
|
|
40
|
+
scope: BaseScope | None = None,
|
|
41
|
+
context: dict[type, Any] | None = None,
|
|
42
|
+
) -> AbstractAsyncContextManager[Self]:
|
|
43
|
+
"""Enter a new scope.
|
|
44
|
+
|
|
45
|
+
Yields:
|
|
46
|
+
The application instance with the new scope.
|
|
47
|
+
"""
|
|
@@ -5,7 +5,7 @@ from contextlib import contextmanager
|
|
|
5
5
|
from typing import TYPE_CHECKING, override
|
|
6
6
|
|
|
7
7
|
from neva.arch import Facade
|
|
8
|
-
from neva.events import
|
|
8
|
+
from neva.events import contracts
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
@@ -23,7 +23,7 @@ class Event(Facade):
|
|
|
23
23
|
Returns:
|
|
24
24
|
EventDispatcher class.
|
|
25
25
|
"""
|
|
26
|
-
return EventDispatcher
|
|
26
|
+
return contracts.EventDispatcher
|
|
27
27
|
|
|
28
28
|
@classmethod
|
|
29
29
|
def fake(cls) -> "EventFake":
|
|
@@ -13,10 +13,15 @@ class Event(Facade):
|
|
|
13
13
|
@override
|
|
14
14
|
def get_facade_accessor(cls) -> type: ...
|
|
15
15
|
@classmethod
|
|
16
|
-
async def
|
|
16
|
+
async def before_dispatch(cls, hook: contracts.BeforeDispatchHook) -> None:
|
|
17
|
+
"""Register a hook to be called before listeners are invoked."""
|
|
18
|
+
@classmethod
|
|
19
|
+
async def dispatch(cls, event: contracts.Event) -> list[Result[None, str]]:
|
|
20
|
+
"""Dispatch an event to all registered listeners."""
|
|
17
21
|
@classmethod
|
|
18
22
|
def listen[T: contracts.Event](
|
|
19
23
|
cls,
|
|
20
24
|
event_cls: type[T],
|
|
21
25
|
listener_cls: type[contracts.EventListener[T]],
|
|
22
|
-
) -> None:
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Register a listener for an event."""
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Before-dispatch hook tests."""
|
|
2
|
+
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from neva.database.manager import DatabaseManager
|
|
8
|
+
from neva.events import EventDispatcher, EventListener, HandlingPolicy
|
|
9
|
+
from neva.events.contracts.event import Event
|
|
10
|
+
from neva.support import Ok, Result
|
|
11
|
+
from tests.events.conftest import OrderPlaced, UserCreated
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestBeforeDispatchHook:
|
|
15
|
+
async def test_async_hook_is_called_on_dispatch(
|
|
16
|
+
self, dispatcher: EventDispatcher
|
|
17
|
+
) -> None:
|
|
18
|
+
called = False
|
|
19
|
+
|
|
20
|
+
async def hook(context: Event) -> None:
|
|
21
|
+
nonlocal called
|
|
22
|
+
called = True
|
|
23
|
+
|
|
24
|
+
await dispatcher.before_dispatch(hook)
|
|
25
|
+
_ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
26
|
+
|
|
27
|
+
assert called
|
|
28
|
+
|
|
29
|
+
async def test_sync_hook_is_called_on_dispatch(
|
|
30
|
+
self, dispatcher: EventDispatcher
|
|
31
|
+
) -> None:
|
|
32
|
+
called = False
|
|
33
|
+
|
|
34
|
+
def hook(context: Event) -> None:
|
|
35
|
+
nonlocal called
|
|
36
|
+
called = True
|
|
37
|
+
|
|
38
|
+
await dispatcher.before_dispatch(hook)
|
|
39
|
+
_ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
40
|
+
|
|
41
|
+
assert called
|
|
42
|
+
|
|
43
|
+
async def test_hook_receives_the_dispatched_event(
|
|
44
|
+
self, dispatcher: EventDispatcher
|
|
45
|
+
) -> None:
|
|
46
|
+
received: Event | None = None
|
|
47
|
+
|
|
48
|
+
async def hook(context: Event) -> None:
|
|
49
|
+
nonlocal received
|
|
50
|
+
received = context
|
|
51
|
+
|
|
52
|
+
await dispatcher.before_dispatch(hook)
|
|
53
|
+
event = OrderPlaced(event_id=2, order_id=42)
|
|
54
|
+
_ = await dispatcher.dispatch(event)
|
|
55
|
+
|
|
56
|
+
assert received is event
|
|
57
|
+
|
|
58
|
+
async def test_hooks_called_in_registration_order(
|
|
59
|
+
self, dispatcher: EventDispatcher
|
|
60
|
+
) -> None:
|
|
61
|
+
order: list[str] = []
|
|
62
|
+
|
|
63
|
+
async def first(context: Event) -> None:
|
|
64
|
+
order.append("first")
|
|
65
|
+
|
|
66
|
+
def second(context: Event) -> None:
|
|
67
|
+
order.append("second")
|
|
68
|
+
|
|
69
|
+
await dispatcher.before_dispatch(first)
|
|
70
|
+
await dispatcher.before_dispatch(second)
|
|
71
|
+
_ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
72
|
+
|
|
73
|
+
assert order == ["first", "second"]
|
|
74
|
+
|
|
75
|
+
async def test_hook_runs_before_listeners(
|
|
76
|
+
self, dispatcher: EventDispatcher
|
|
77
|
+
) -> None:
|
|
78
|
+
order: list[str] = []
|
|
79
|
+
|
|
80
|
+
async def hook(context: Event) -> None:
|
|
81
|
+
order.append("hook")
|
|
82
|
+
|
|
83
|
+
class OrderPlacedListener(EventListener[OrderPlaced]):
|
|
84
|
+
@override
|
|
85
|
+
async def handle(self, event: OrderPlaced) -> Result[None, str]:
|
|
86
|
+
order.append("listener")
|
|
87
|
+
return Ok(None)
|
|
88
|
+
|
|
89
|
+
await dispatcher.before_dispatch(hook)
|
|
90
|
+
dispatcher.listen(OrderPlaced, OrderPlacedListener)
|
|
91
|
+
_ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
92
|
+
|
|
93
|
+
assert order == ["hook", "listener"]
|
|
94
|
+
|
|
95
|
+
async def test_hook_fires_with_no_listeners(
|
|
96
|
+
self, dispatcher: EventDispatcher
|
|
97
|
+
) -> None:
|
|
98
|
+
called = False
|
|
99
|
+
|
|
100
|
+
async def hook(context: Event) -> None:
|
|
101
|
+
nonlocal called
|
|
102
|
+
called = True
|
|
103
|
+
|
|
104
|
+
await dispatcher.before_dispatch(hook)
|
|
105
|
+
results = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
106
|
+
|
|
107
|
+
assert called
|
|
108
|
+
assert results == []
|
|
109
|
+
|
|
110
|
+
async def test_hook_fires_once_per_dispatch(
|
|
111
|
+
self, dispatcher: EventDispatcher
|
|
112
|
+
) -> None:
|
|
113
|
+
call_count = 0
|
|
114
|
+
|
|
115
|
+
async def hook(context: Event) -> None:
|
|
116
|
+
nonlocal call_count
|
|
117
|
+
call_count += 1
|
|
118
|
+
|
|
119
|
+
await dispatcher.before_dispatch(hook)
|
|
120
|
+
_ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
121
|
+
_ = await dispatcher.dispatch(OrderPlaced(event_id=2, order_id=2))
|
|
122
|
+
|
|
123
|
+
assert call_count == 2
|
|
124
|
+
|
|
125
|
+
async def test_no_hooks_registered_is_a_noop(
|
|
126
|
+
self, dispatcher: EventDispatcher
|
|
127
|
+
) -> None:
|
|
128
|
+
results = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
|
|
129
|
+
|
|
130
|
+
assert results == []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestBeforeDispatchWithTransaction:
|
|
134
|
+
async def test_hook_fires_at_dispatch_time_for_deferred_event(
|
|
135
|
+
self, dispatcher: EventDispatcher, db: DatabaseManager
|
|
136
|
+
) -> None:
|
|
137
|
+
hook_calls: list[str] = []
|
|
138
|
+
|
|
139
|
+
async def hook(context: Event) -> None:
|
|
140
|
+
hook_calls.append("hook")
|
|
141
|
+
|
|
142
|
+
class UserCreatedListener(EventListener[UserCreated]):
|
|
143
|
+
policy = HandlingPolicy.DEFERRED
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
async def handle(self, event: UserCreated) -> Result[None, str]:
|
|
147
|
+
hook_calls.append("listener")
|
|
148
|
+
return Ok(None)
|
|
149
|
+
|
|
150
|
+
await dispatcher.before_dispatch(hook)
|
|
151
|
+
dispatcher.listen(UserCreated, UserCreatedListener)
|
|
152
|
+
|
|
153
|
+
async with db.begin():
|
|
154
|
+
_ = await dispatcher.dispatch(UserCreated(event_id=1, user_id=1))
|
|
155
|
+
# Hook runs eagerly even though the listener is deferred.
|
|
156
|
+
assert hook_calls == ["hook"]
|
|
157
|
+
|
|
158
|
+
assert hook_calls == ["hook", "listener"]
|
|
159
|
+
|
|
160
|
+
async def test_hook_fires_even_when_transaction_rolls_back(
|
|
161
|
+
self, dispatcher: EventDispatcher, db: DatabaseManager
|
|
162
|
+
) -> None:
|
|
163
|
+
called = False
|
|
164
|
+
|
|
165
|
+
async def hook(context: Event) -> None:
|
|
166
|
+
nonlocal called
|
|
167
|
+
called = True
|
|
168
|
+
|
|
169
|
+
await dispatcher.before_dispatch(hook)
|
|
170
|
+
|
|
171
|
+
with pytest.raises(RuntimeError):
|
|
172
|
+
async with db.begin():
|
|
173
|
+
_ = await dispatcher.dispatch(UserCreated(event_id=1, user_id=1))
|
|
174
|
+
msg = "rollback"
|
|
175
|
+
raise RuntimeError(msg)
|
|
176
|
+
|
|
177
|
+
assert called
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"""Type stub for App facade."""
|
|
2
|
-
|
|
3
|
-
from typing import override
|
|
4
|
-
|
|
5
|
-
from neva.arch import Facade
|
|
6
|
-
from neva.support import Result
|
|
7
|
-
|
|
8
|
-
class App(Facade):
|
|
9
|
-
@classmethod
|
|
10
|
-
@override
|
|
11
|
-
def get_facade_accessor(cls) -> type: ...
|
|
12
|
-
@classmethod
|
|
13
|
-
def make[T](cls, interface: type[T]) -> Result[T, str]:
|
|
14
|
-
"""Resolve an interface from the container by its alias.
|
|
15
|
-
|
|
16
|
-
Attempts to retrieve and instantiate an object from the dependency
|
|
17
|
-
injection container.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
interface: The interface to resolve.
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
Result containing the resolved service instance or an error message.
|
|
24
|
-
|
|
25
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|