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.
Files changed (146) hide show
  1. {python_neva-3.4.0 → python_neva-3.5.0}/CHANGELOG.md +16 -0
  2. {python_neva-3.4.0 → python_neva-3.5.0}/PKG-INFO +1 -1
  3. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/dispatcher.py +4 -5
  4. python_neva-3.5.0/neva/support/facade/app.pyi +47 -0
  5. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/event.py +2 -2
  6. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/event.pyi +7 -2
  7. {python_neva-3.4.0 → python_neva-3.5.0}/pyproject.toml +1 -1
  8. python_neva-3.5.0/tests/events/test_before_dispatch.py +177 -0
  9. {python_neva-3.4.0 → python_neva-3.5.0}/uv.lock +1 -1
  10. python_neva-3.4.0/neva/support/facade/app.pyi +0 -25
  11. {python_neva-3.4.0 → python_neva-3.5.0}/.envrc +0 -0
  12. {python_neva-3.4.0 → python_neva-3.5.0}/.gitignore +0 -0
  13. {python_neva-3.4.0 → python_neva-3.5.0}/.pre-commit-config.yaml +0 -0
  14. {python_neva-3.4.0 → python_neva-3.5.0}/.python-version +0 -0
  15. {python_neva-3.4.0 → python_neva-3.5.0}/README.md +0 -0
  16. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/__init__.py +0 -0
  17. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/application.py +0 -0
  18. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/config.py +0 -0
  19. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/facade.py +0 -0
  20. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/integrations/__init__.py +0 -0
  21. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/integrations/faststream.py +0 -0
  22. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/markers.py +0 -0
  23. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/py.typed +0 -0
  24. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/scopes.py +0 -0
  25. {python_neva-3.4.0 → python_neva-3.5.0}/neva/arch/service_provider.py +0 -0
  26. {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/__init__.py +0 -0
  27. {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/base_providers.py +0 -0
  28. {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/loader.py +0 -0
  29. {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/py.typed +0 -0
  30. {python_neva-3.4.0 → python_neva-3.5.0}/neva/config/repository.py +0 -0
  31. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/__init__.py +0 -0
  32. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/config.py +0 -0
  33. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/connection.py +0 -0
  34. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/manager.py +0 -0
  35. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/provider.py +0 -0
  36. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/py.typed +0 -0
  37. {python_neva-3.4.0 → python_neva-3.5.0}/neva/database/transaction.py +0 -0
  38. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/__init__.py +0 -0
  39. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/__init__.py +0 -0
  40. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/dispatcher.py +0 -0
  41. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/event.py +0 -0
  42. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/handler.py +0 -0
  43. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/contracts/listener.py +0 -0
  44. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/event.py +0 -0
  45. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/event_registry.py +0 -0
  46. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/listener.py +0 -0
  47. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/policy.py +0 -0
  48. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/provider.py +0 -0
  49. {python_neva-3.4.0 → python_neva-3.5.0}/neva/events/py.typed +0 -0
  50. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/__init__.py +0 -0
  51. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/instrumentation/__init__.py +0 -0
  52. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  53. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/logging/__init__.py +0 -0
  54. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/logging/manager.py +0 -0
  55. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/logging/provider.py +0 -0
  56. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/middleware/__init__.py +0 -0
  57. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/middleware/correlation.py +0 -0
  58. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/middleware/profiler.py +0 -0
  59. {python_neva-3.4.0 → python_neva-3.5.0}/neva/obs/py.typed +0 -0
  60. {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/__init__.py +0 -0
  61. {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/factories.py +0 -0
  62. {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/persistence.py +0 -0
  63. {python_neva-3.4.0 → python_neva-3.5.0}/neva/polyfactory/py.typed +0 -0
  64. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/__init__.py +0 -0
  65. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/encryption/__init__.py +0 -0
  66. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/encryption/encrypter.py +0 -0
  67. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/encryption/protocol.py +0 -0
  68. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/__init__.py +0 -0
  69. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/config.py +0 -0
  70. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hash_manager.py +0 -0
  71. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/__init__.py +0 -0
  72. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/argon2.py +0 -0
  73. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  74. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/hashing/hashers/protocol.py +0 -0
  75. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/provider.py +0 -0
  76. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/py.typed +0 -0
  77. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/__init__.py +0 -0
  78. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/generate_token.py +0 -0
  79. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/hash_token.py +0 -0
  80. {python_neva-3.4.0 → python_neva-3.5.0}/neva/security/tokens/verify_token.py +0 -0
  81. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/__init__.py +0 -0
  82. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/accessors.py +0 -0
  83. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/__init__.py +0 -0
  84. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/app.py +0 -0
  85. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/config.py +0 -0
  86. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/config.pyi +0 -0
  87. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/crypt.py +0 -0
  88. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/crypt.pyi +0 -0
  89. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/db.py +0 -0
  90. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/db.pyi +0 -0
  91. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/hash.py +0 -0
  92. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/hash.pyi +0 -0
  93. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/log.py +0 -0
  94. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/facade/log.pyi +0 -0
  95. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/py.typed +0 -0
  96. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/results.py +0 -0
  97. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/strategy.py +0 -0
  98. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/strconv.py +0 -0
  99. {python_neva-3.4.0 → python_neva-3.5.0}/neva/support/time.py +0 -0
  100. {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/__init__.py +0 -0
  101. {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/fakes.py +0 -0
  102. {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/fixtures.py +0 -0
  103. {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/py.typed +0 -0
  104. {python_neva-3.4.0 → python_neva-3.5.0}/neva/testing/test_case.py +0 -0
  105. {python_neva-3.4.0 → python_neva-3.5.0}/ruff.toml +0 -0
  106. {python_neva-3.4.0 → python_neva-3.5.0}/scripts/retag-with-changelog.sh +0 -0
  107. {python_neva-3.4.0 → python_neva-3.5.0}/tests/__init__.py +0 -0
  108. {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/__init__.py +0 -0
  109. {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_cache.py +0 -0
  110. {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_context.py +0 -0
  111. {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_extends.py +0 -0
  112. {python_neva-3.4.0 → python_neva-3.5.0}/tests/arch/test_scope.py +0 -0
  113. {python_neva-3.4.0 → python_neva-3.5.0}/tests/config/__init__.py +0 -0
  114. {python_neva-3.4.0 → python_neva-3.5.0}/tests/config/test_loader.py +0 -0
  115. {python_neva-3.4.0 → python_neva-3.5.0}/tests/config/test_repository.py +0 -0
  116. {python_neva-3.4.0 → python_neva-3.5.0}/tests/conftest.py +0 -0
  117. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/__init__.py +0 -0
  118. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_connection_manager.py +0 -0
  119. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_database_manager.py +0 -0
  120. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_edge_cases.py +0 -0
  121. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_multi_connection.py +0 -0
  122. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  123. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_transaction.py +0 -0
  124. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_transaction_context.py +0 -0
  125. {python_neva-3.4.0 → python_neva-3.5.0}/tests/database/test_transaction_registry.py +0 -0
  126. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/__init__.py +0 -0
  127. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/conftest.py +0 -0
  128. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_binding.py +0 -0
  129. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_deferred.py +0 -0
  130. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_dispatch.py +0 -0
  131. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_event.py +0 -0
  132. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_function_listener.py +0 -0
  133. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_immediate.py +0 -0
  134. {python_neva-3.4.0 → python_neva-3.5.0}/tests/events/test_listen_on_parent_class.py +0 -0
  135. {python_neva-3.4.0 → python_neva-3.5.0}/tests/obs/__init__.py +0 -0
  136. {python_neva-3.4.0 → python_neva-3.5.0}/tests/obs/test_correlation.py +0 -0
  137. {python_neva-3.4.0 → python_neva-3.5.0}/tests/obs/test_profiler.py +0 -0
  138. {python_neva-3.4.0 → python_neva-3.5.0}/tests/security/__init__.py +0 -0
  139. {python_neva-3.4.0 → python_neva-3.5.0}/tests/security/test_encrypter.py +0 -0
  140. {python_neva-3.4.0 → python_neva-3.5.0}/tests/security/test_hash_manager.py +0 -0
  141. {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/__init__.py +0 -0
  142. {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_event_fake.py +0 -0
  143. {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_facade_restore.py +0 -0
  144. {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_fixtures.py +0 -0
  145. {python_neva-3.4.0 → python_neva-3.5.0}/tests/testing/test_refresh_database.py +0 -0
  146. {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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -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
- match hook:
39
- case contracts.AsyncBeforeDispatchHook():
40
- await hook(event)
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 EventDispatcher
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 dispatch(cls, event: contracts.Event) -> list[Result[None, str]]: ...
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."""
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-neva"
3
- version = "3.4.0"
3
+ version = "3.5.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
@@ -1733,7 +1733,7 @@ wheels = [
1733
1733
 
1734
1734
  [[package]]
1735
1735
  name = "python-neva"
1736
- version = "3.4.0"
1736
+ version = "3.5.0"
1737
1737
  source = { editable = "." }
1738
1738
  dependencies = [
1739
1739
  { name = "aiosqlite" },
@@ -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