python-neva 3.1.1__tar.gz → 3.3.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 (149) hide show
  1. python_neva-3.3.0/CHANGELOG.md +115 -0
  2. {python_neva-3.1.1 → python_neva-3.3.0}/PKG-INFO +6 -1
  3. python_neva-3.3.0/README.md +3 -0
  4. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/application.py +11 -6
  5. python_neva-3.3.0/neva/arch/markers.py +4 -0
  6. python_neva-3.3.0/neva/arch/scopes.py +5 -0
  7. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/service_provider.py +83 -2
  8. {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/__init__.py +2 -1
  9. python_neva-3.3.0/neva/events/contracts/__init__.py +7 -0
  10. python_neva-3.3.0/neva/events/contracts/dispatcher.py +24 -0
  11. python_neva-3.3.0/neva/events/contracts/event.py +7 -0
  12. python_neva-3.3.0/neva/events/contracts/handler.py +14 -0
  13. python_neva-3.3.0/neva/events/contracts/listener.py +19 -0
  14. python_neva-3.3.0/neva/events/dispatcher.py +135 -0
  15. {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/event.py +2 -1
  16. python_neva-3.3.0/neva/events/event_registry.py +59 -0
  17. {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/listener.py +10 -33
  18. python_neva-3.3.0/neva/events/policy.py +8 -0
  19. {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/provider.py +5 -2
  20. {python_neva-3.1.1 → python_neva-3.3.0}/pyproject.toml +3 -15
  21. python_neva-3.3.0/tests/arch/test_cache.py +52 -0
  22. python_neva-3.3.0/tests/arch/test_extends.py +98 -0
  23. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/conftest.py +2 -1
  24. python_neva-3.3.0/tests/events/test_binding.py +11 -0
  25. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_dispatch.py +3 -2
  26. python_neva-3.3.0/tests/events/test_listen_on_parent_class.py +77 -0
  27. {python_neva-3.1.1 → python_neva-3.3.0}/tests/obs/test_correlation.py +4 -0
  28. {python_neva-3.1.1 → python_neva-3.3.0}/tests/obs/test_profiler.py +4 -0
  29. python_neva-3.3.0/uv.lock +2436 -0
  30. python_neva-3.1.1/CHANGELOG.md +0 -48
  31. python_neva-3.1.1/README.md +0 -0
  32. python_neva-3.1.1/neva/events/dispatcher.py +0 -89
  33. python_neva-3.1.1/neva/events/event_registry.py +0 -44
  34. python_neva-3.1.1/tests/events/test_event_registry.py +0 -101
  35. python_neva-3.1.1/uv.lock +0 -2440
  36. {python_neva-3.1.1 → python_neva-3.3.0}/.envrc +0 -0
  37. {python_neva-3.1.1 → python_neva-3.3.0}/.gitignore +0 -0
  38. {python_neva-3.1.1 → python_neva-3.3.0}/.pre-commit-config.yaml +0 -0
  39. {python_neva-3.1.1 → python_neva-3.3.0}/.python-version +0 -0
  40. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/__init__.py +0 -0
  41. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/config.py +0 -0
  42. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/facade.py +0 -0
  43. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/integrations/__init__.py +0 -0
  44. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/integrations/faststream.py +0 -0
  45. {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/py.typed +0 -0
  46. {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/__init__.py +0 -0
  47. {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/base_providers.py +0 -0
  48. {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/loader.py +0 -0
  49. {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/py.typed +0 -0
  50. {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/repository.py +0 -0
  51. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/__init__.py +0 -0
  52. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/config.py +0 -0
  53. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/connection.py +0 -0
  54. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/manager.py +0 -0
  55. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/provider.py +0 -0
  56. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/py.typed +0 -0
  57. {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/transaction.py +0 -0
  58. {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/py.typed +0 -0
  59. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/__init__.py +0 -0
  60. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/instrumentation/__init__.py +0 -0
  61. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  62. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/logging/__init__.py +0 -0
  63. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/logging/manager.py +0 -0
  64. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/logging/provider.py +0 -0
  65. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/middleware/__init__.py +0 -0
  66. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/middleware/correlation.py +0 -0
  67. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/middleware/profiler.py +0 -0
  68. {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/py.typed +0 -0
  69. {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/__init__.py +0 -0
  70. {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/factories.py +0 -0
  71. {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/persistence.py +0 -0
  72. {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/py.typed +0 -0
  73. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/__init__.py +0 -0
  74. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/encryption/__init__.py +0 -0
  75. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/encryption/encrypter.py +0 -0
  76. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/encryption/protocol.py +0 -0
  77. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/__init__.py +0 -0
  78. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/config.py +0 -0
  79. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hash_manager.py +0 -0
  80. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/__init__.py +0 -0
  81. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/argon2.py +0 -0
  82. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  83. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/protocol.py +0 -0
  84. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/provider.py +0 -0
  85. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/py.typed +0 -0
  86. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/__init__.py +0 -0
  87. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/generate_token.py +0 -0
  88. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/hash_token.py +0 -0
  89. {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/verify_token.py +0 -0
  90. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/__init__.py +0 -0
  91. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/accessors.py +0 -0
  92. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/__init__.py +0 -0
  93. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/app.py +0 -0
  94. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/app.pyi +0 -0
  95. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/config.py +0 -0
  96. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/config.pyi +0 -0
  97. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/crypt.py +0 -0
  98. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/crypt.pyi +0 -0
  99. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/db.py +0 -0
  100. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/db.pyi +0 -0
  101. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/event.py +0 -0
  102. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/event.pyi +0 -0
  103. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/hash.py +0 -0
  104. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/hash.pyi +0 -0
  105. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/log.py +0 -0
  106. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/log.pyi +0 -0
  107. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/py.typed +0 -0
  108. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/results.py +0 -0
  109. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/strategy.py +0 -0
  110. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/strconv.py +0 -0
  111. {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/time.py +0 -0
  112. {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/__init__.py +0 -0
  113. {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/fakes.py +0 -0
  114. {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/fixtures.py +0 -0
  115. {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/py.typed +0 -0
  116. {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/test_case.py +0 -0
  117. {python_neva-3.1.1 → python_neva-3.3.0}/ruff.toml +0 -0
  118. {python_neva-3.1.1 → python_neva-3.3.0}/scripts/retag-with-changelog.sh +0 -0
  119. {python_neva-3.1.1 → python_neva-3.3.0}/tests/__init__.py +0 -0
  120. {python_neva-3.1.1 → python_neva-3.3.0}/tests/arch/__init__.py +0 -0
  121. {python_neva-3.1.1 → python_neva-3.3.0}/tests/arch/test_scope.py +0 -0
  122. {python_neva-3.1.1 → python_neva-3.3.0}/tests/config/__init__.py +0 -0
  123. {python_neva-3.1.1 → python_neva-3.3.0}/tests/config/test_loader.py +0 -0
  124. {python_neva-3.1.1 → python_neva-3.3.0}/tests/config/test_repository.py +0 -0
  125. {python_neva-3.1.1 → python_neva-3.3.0}/tests/conftest.py +0 -0
  126. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/__init__.py +0 -0
  127. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_connection_manager.py +0 -0
  128. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_database_manager.py +0 -0
  129. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_edge_cases.py +0 -0
  130. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_multi_connection.py +0 -0
  131. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  132. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_transaction.py +0 -0
  133. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_transaction_context.py +0 -0
  134. {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_transaction_registry.py +0 -0
  135. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/__init__.py +0 -0
  136. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_deferred.py +0 -0
  137. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_event.py +0 -0
  138. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_function_listener.py +0 -0
  139. {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_immediate.py +0 -0
  140. {python_neva-3.1.1 → python_neva-3.3.0}/tests/obs/__init__.py +0 -0
  141. {python_neva-3.1.1 → python_neva-3.3.0}/tests/security/__init__.py +0 -0
  142. {python_neva-3.1.1 → python_neva-3.3.0}/tests/security/test_encrypter.py +0 -0
  143. {python_neva-3.1.1 → python_neva-3.3.0}/tests/security/test_hash_manager.py +0 -0
  144. {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/__init__.py +0 -0
  145. {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_event_fake.py +0 -0
  146. {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_facade_restore.py +0 -0
  147. {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_fixtures.py +0 -0
  148. {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_refresh_database.py +0 -0
  149. {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_test_case.py +0 -0
@@ -0,0 +1,115 @@
1
+ ## 3.3.0 (2026-06-05)
2
+
3
+ ### ✨ Features
4
+
5
+ - **tooling**: placeholder commit
6
+
7
+ ### 💚👷 CI & Build
8
+
9
+ - **tooling**: default to base tag version for commitizen
10
+
11
+ ### 📝💡 Documentation
12
+
13
+ - **project**: update doc
14
+ - **core**: placeholde readme
15
+
16
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
17
+
18
+ - **tooling**: update changelog script
19
+ - **project**: update versioning config
20
+
21
+ ## 3.2.0 (2026-06-05)
22
+
23
+ ### ✨ Features
24
+
25
+ - **tooling**: placeholder commit
26
+
27
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
28
+
29
+ - **tooling**: update changelog script
30
+ - **project**: update versioning config
31
+
32
+ ### 📝💡 Documentation
33
+
34
+ - **core**: placeholde readme
35
+
36
+ ## 3.1.1 (2026-05-11)
37
+
38
+ ### 📌➕⬇️➖⬆️ Dependencies
39
+
40
+ - bump opt dep on neva-fastapi
41
+
42
+ ## 3.1.0 (2026-05-11)
43
+
44
+ ### ✨ Features
45
+
46
+ - fixing db facade
47
+ - Add eq method to Some / Ok
48
+ - Deprecate register_engine, add register_connection
49
+ - RefreshDatabase now properly use the testcase inner app
50
+ - Add neva-fastapi as optional dependency
51
+ - renaming of make / make_async for retrocompatibility
52
+
53
+ ### 🐛🚑️ Fixes
54
+
55
+ - Fix provider registration ordering
56
+
57
+ ### ♻️ Refactorings
58
+
59
+ - refactor of results / db toolkit
60
+
61
+ ## 3.1.0a1 (2026-05-05)
62
+
63
+ ### build
64
+
65
+ - add commitizen + versioningit
66
+
67
+ ### 💚👷 CI & Build
68
+
69
+ - update perms on tag script
70
+ - tweaking cz tags
71
+ - remove auto-annotated tags
72
+ - configure versioningit + cz
73
+ - Fix cz config >>> ⏰ 1
74
+ - add and configure cz_gitmoji >>> ⏰ 5m
75
+
76
+ ### 📝💡 Documentation
77
+
78
+ - clear changelog
79
+
80
+ ### 🔧🔨📦️ Configuration, Scripts, Packages
81
+
82
+ - improve auto-annotation of tags
83
+ - Fix tagging script
84
+
85
+ ## python-neva-v3.2.0 (2026-06-04)
86
+
87
+ ### ✨ Features
88
+
89
+ - **core**: add optional activator on all binding functions
90
+ - **core**: remove scoping option from scoped, default to request scope
91
+ - **core**: introduce conditional activation
92
+ - **core**: add specifying interface in extends method
93
+ - **core**: add extends feature
94
+
95
+ ### build
96
+
97
+ - **core**: remove redundant .envrc files
98
+ - **core**: fixing version derivation
99
+
100
+ ### docs
101
+
102
+ - **core,-fastapi**: add contributing documentation
103
+
104
+ ### feat
105
+
106
+ - **core**: allow passing context data to scope method
107
+ - **core**: add some utility functions for binding
108
+ - **core**: add some utility functions for binding
109
+ - **core**: add the possibility to turn off dependency caching
110
+ - **core,-fastapi**: add .envrc file
111
+
112
+ ### refactor
113
+
114
+ - **core**: cleaning up type hints
115
+ - **core**: update type hints on main application
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 3.1.1
3
+ Version: 3.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -20,3 +20,8 @@ Requires-Dist: neva-fastapi>=1.0.0; extra == 'fastapi'
20
20
  Provides-Extra: testing
21
21
  Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
22
22
  Requires-Dist: pytest>=9.0.2; extra == 'testing'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # The Neva framework
26
+
27
+ PLACEHOLDER
@@ -0,0 +1,3 @@
1
+ # The Neva framework
2
+
3
+ PLACEHOLDER
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from collections import OrderedDict
5
- from collections.abc import AsyncIterator, Sequence
5
+ from collections.abc import AsyncGenerator, Sequence
6
6
  from contextlib import AsyncExitStack, asynccontextmanager
7
7
  from contextvars import ContextVar
8
8
  from pathlib import Path
@@ -13,6 +13,7 @@ from dishka.provider import BaseProvider
13
13
  from typing_extensions import deprecated
14
14
 
15
15
  from neva.arch.facade import Facade
16
+ from neva.arch.scopes import BaseScope, Scope
16
17
  from neva.arch.service_provider import Bootable, ServiceProvider
17
18
  from neva.config.loader import ConfigLoader
18
19
  from neva.support import Err, Ok, Result
@@ -41,7 +42,7 @@ class Application:
41
42
 
42
43
  self.config: ConfigRepository = ConfigRepository()
43
44
  self.providers: OrderedDict[type, ServiceProvider] = OrderedDict()
44
- self.root_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
45
+ self.root_provider: dishka.Provider = dishka.Provider(scope=Scope.APP)
45
46
 
46
47
  configuration_path = (
47
48
  config_path
@@ -112,7 +113,7 @@ class Application:
112
113
  source: type | Callable[..., Any],
113
114
  *,
114
115
  interface: type | None = None,
115
- scope: dishka.BaseScope | None = None,
116
+ scope: BaseScope | None = None,
116
117
  ) -> None:
117
118
  """Binds a source to the container."""
118
119
  _ = self.root_provider.provide(
@@ -146,14 +147,18 @@ class Application:
146
147
  return Err(f"Failed to resolve service '{interface.__name__}': {e}")
147
148
 
148
149
  @asynccontextmanager
149
- async def scope(self, scope: dishka.BaseScope | None = None) -> AsyncIterator[Self]:
150
+ async def scope(
151
+ self,
152
+ scope: BaseScope | None = None,
153
+ context: dict[type, Any] | None = None,
154
+ ) -> AsyncGenerator[Self]:
150
155
  """Enter a new scope.
151
156
 
152
157
  Yields:
153
158
  The application instance with the new scope.
154
159
  """
155
160
  parent = _current_container.get(self.container)
156
- async with parent(scope=scope) as container:
161
+ async with parent(scope=scope, context=context) as container:
157
162
  token = _current_container.set(container)
158
163
  try:
159
164
  yield self
@@ -161,7 +166,7 @@ class Application:
161
166
  _current_container.reset(token)
162
167
 
163
168
  @asynccontextmanager
164
- async def lifespan(self) -> AsyncIterator[None]:
169
+ async def lifespan(self) -> AsyncGenerator[None]:
165
170
  """Wire the facades and providers."""
166
171
  Facade.set_facade_application(self)
167
172
 
@@ -0,0 +1,4 @@
1
+ from dishka.entities.marker import BaseMarker, Has, Marker
2
+
3
+
4
+ __all__ = ["BaseMarker", "Has", "Marker"]
@@ -0,0 +1,5 @@
1
+ from dishka.entities.scope import BaseScope, Scope
2
+
3
+
4
+ __all__ = ["BaseScope", "Scope"]
5
+
@@ -21,6 +21,8 @@ from typing import (
21
21
 
22
22
  import dishka
23
23
 
24
+ from neva.arch.markers import BaseMarker, Marker
25
+ from neva.arch.scopes import BaseScope, Scope
24
26
  from neva.support import Result
25
27
 
26
28
 
@@ -69,6 +71,7 @@ class ServiceProvider(abc.ABC):
69
71
 
70
72
  app: "Application"
71
73
  listen: ClassVar[dict[type[Event], list[type[EventListener[Any]]]]] = {}
74
+ when: ClassVar[Marker | None] = None
72
75
 
73
76
  def __init__(self, app: "Application") -> None:
74
77
  """Initialize the service provider.
@@ -77,23 +80,101 @@ class ServiceProvider(abc.ABC):
77
80
  app: The application instance.
78
81
 
79
82
  """
80
- self.provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
83
+ self.provider: dishka.Provider = dishka.Provider(
84
+ scope=dishka.Scope.APP,
85
+ when=self.when,
86
+ )
81
87
  self.app = app
82
88
 
89
+ def activator(
90
+ self,
91
+ activation_fn: Callable[..., bool],
92
+ *markers: Marker | type[Marker],
93
+ ) -> None:
94
+ """Registers an activator function for the given markers.
95
+
96
+ Args:
97
+ activation_fn: The activator function.
98
+ markers: Markers triggered by this function.
99
+ """
100
+ _ = self.provider.activate(activation_fn, *markers)
101
+
83
102
  def bind(
84
103
  self,
85
104
  source: type | Callable[..., Any],
86
105
  *,
87
106
  interface: type | None = None,
88
- scope: dishka.BaseScope | None = None,
107
+ scope: BaseScope | None = None,
108
+ when: BaseMarker | None = None,
109
+ cache: bool = True,
89
110
  ) -> None:
90
111
  """Binds a source to the container."""
91
112
  _ = self.provider.provide(
92
113
  source=source,
93
114
  scope=scope,
94
115
  provides=interface,
116
+ cache=cache,
117
+ when=when,
95
118
  )
96
119
 
120
+ def scoped(
121
+ self,
122
+ source: type | Callable[..., Any],
123
+ *,
124
+ interface: type | None = None,
125
+ when: BaseMarker | None = None,
126
+ ) -> Self:
127
+ """Binds the source to the container.
128
+
129
+ Scope is REQUEST by default but a custom scope be provided.
130
+ Dependency declared with this are cached no matter what.
131
+
132
+ Returns:
133
+ the provider itself for chaining purposes
134
+ """
135
+ self.bind(source, interface=interface, scope=Scope.REQUEST, when=when)
136
+ return self
137
+
138
+ def transient(
139
+ self,
140
+ source: type | Callable[..., Any],
141
+ *,
142
+ interface: type | None = None,
143
+ when: BaseMarker | None = None,
144
+ ) -> Self:
145
+ """Binds the source to the container as a transient dependency.
146
+
147
+ Dependencies declared this way have global scope and are never
148
+ cached.
149
+
150
+ Returns:
151
+ the provider itself for chaining purposes
152
+ """
153
+ self.bind(source, interface=interface, when=when, cache=False)
154
+ return self
155
+
156
+ def extend(
157
+ self,
158
+ source: Callable[..., Any],
159
+ *,
160
+ interface: type | None = None,
161
+ scope: dishka.BaseScope | None = None,
162
+ when: BaseMarker | None = None,
163
+ ) -> Self:
164
+ """Extends a dependency declared by another provider.
165
+
166
+ This allows to 'extend' a dependency, maybe even override it entirely.
167
+ Particularly useful if you want to run some code on a dependency declared
168
+ by another provider / package.
169
+
170
+ You may also provide a scope for this.
171
+
172
+ Returns:
173
+ the provider itself for chaining purposes
174
+ """
175
+ _ = self.provider.decorate(source, provides=interface, scope=scope, when=when)
176
+ return self
177
+
97
178
  @abc.abstractmethod
98
179
  def register(self) -> Result[Self, str]:
99
180
  """Register services into the application container.
@@ -9,7 +9,8 @@ Three components are of particular interest:
9
9
 
10
10
  from neva.events.dispatcher import EventDispatcher
11
11
  from neva.events.event import Event
12
- from neva.events.listener import EventListener, HandlingPolicy, listener
12
+ from neva.events.listener import EventListener, listener
13
+ from neva.events.policy import HandlingPolicy
13
14
  from neva.events.provider import EventServiceProvider
14
15
 
15
16
 
@@ -0,0 +1,7 @@
1
+ from neva.events.contracts.dispatcher import EventDispatcher
2
+ from neva.events.contracts.event import Event
3
+ from neva.events.contracts.handler import EventHandler
4
+ from neva.events.contracts.listener import EventListener
5
+
6
+
7
+ __all__ = ["Event", "EventDispatcher", "EventHandler", "EventListener"]
@@ -0,0 +1,24 @@
1
+ from typing import Protocol
2
+
3
+ from neva.events.contracts.event import Event
4
+ from neva.events.contracts.listener import EventListener
5
+ from neva.support import Result
6
+
7
+
8
+ class EventDispatcher(Protocol):
9
+ """Event dispatcher protocol."""
10
+
11
+ async def dispatch(
12
+ self,
13
+ event: Event,
14
+ ) -> list[Result[None, str]]:
15
+ """Dispatch an event to all registered listeners."""
16
+ ...
17
+
18
+ def listen[T: Event](
19
+ self,
20
+ event_cls: type[T],
21
+ listener_cls: type[EventListener[T]],
22
+ ) -> None:
23
+ """Register a listener for an event."""
24
+ ...
@@ -0,0 +1,7 @@
1
+ import pydantic
2
+
3
+
4
+ class Event(pydantic.BaseModel):
5
+ """Base Event class."""
6
+
7
+ pass
@@ -0,0 +1,14 @@
1
+ from typing import Protocol
2
+
3
+ from neva.events.contracts.event import Event
4
+ from neva.support import Result
5
+
6
+
7
+ class EventHandler[T: Event](Protocol):
8
+ """Define a valid function for event handling."""
9
+
10
+ __name__: str
11
+
12
+ async def __call__(self, event: T) -> Result[None, str]:
13
+ """Handle an event."""
14
+ ...
@@ -0,0 +1,19 @@
1
+ from typing import Protocol
2
+
3
+ from neva.events.contracts.event import Event
4
+ from neva.events.policy import HandlingPolicy
5
+ from neva.support import Result
6
+
7
+
8
+ class EventListener[T: Event](Protocol):
9
+ """Event listener protocol."""
10
+
11
+ policy: HandlingPolicy
12
+
13
+ async def handle(self, event: T) -> Result[None, str]:
14
+ """Handle an event.
15
+
16
+ Returns:
17
+ A result indicating whether the event was handled successfully.
18
+ """
19
+ ...
@@ -0,0 +1,135 @@
1
+ """Base implementation of the event dispatcher."""
2
+
3
+ from typing import Callable, Protocol, override, runtime_checkable
4
+
5
+ from neva.arch.application import Application
6
+ from neva.database.manager import DatabaseManager
7
+ from neva.database.transaction import TransactionCallback
8
+ from neva.events import contracts
9
+ from neva.events.event_registry import EventRegistry
10
+ from neva.support import Err, Nothing, Result, Some
11
+
12
+
13
+ @runtime_checkable
14
+ class AsyncBeforeDispatchHook(Protocol):
15
+ async def __call__(self, context: contracts.Event) -> None: ...
16
+
17
+
18
+ @runtime_checkable
19
+ class SyncBeforeDispatchHook(Protocol):
20
+ def __call__(self, context: contracts.Event) -> None: ...
21
+
22
+
23
+ BeforeDispatchHook = AsyncBeforeDispatchHook | SyncBeforeDispatchHook
24
+
25
+
26
+ class EventDispatcher(contracts.EventDispatcher):
27
+ """Event dispatcher implementation."""
28
+
29
+ def __init__(
30
+ self,
31
+ app: Application,
32
+ db: DatabaseManager,
33
+ registry: EventRegistry,
34
+ ) -> None:
35
+ self._registry: EventRegistry = registry
36
+ self._app: Application = app
37
+ self._db: DatabaseManager = db
38
+ self._before_dispatch_hooks: list[BeforeDispatchHook] = []
39
+
40
+ async def _apply_before_dispatch(self, event: contracts.Event) -> None:
41
+ """Extension hook called before listeners are invoked.
42
+
43
+ Override in a subclass to add cross-cutting behaviour such as
44
+ persisting the event to an event store. The default implementation
45
+ is a no-op.
46
+
47
+ Args:
48
+ event: The event about to be dispatched.
49
+ """
50
+ for hook in self._before_dispatch_hooks:
51
+ match hook:
52
+ case AsyncBeforeDispatchHook():
53
+ await hook(event)
54
+ case SyncBeforeDispatchHook():
55
+ hook(event)
56
+
57
+ async def before_dispatch(self, hook: BeforeDispatchHook) -> None:
58
+ """Register a hook to be called before listeners are invoked."""
59
+ self._before_dispatch_hooks.append(hook)
60
+
61
+ @override
62
+ async def dispatch(self, event: contracts.Event) -> list[Result[None, str]]:
63
+ """Dispatch an event to all registered listeners.
64
+
65
+ Listeners are resolved from the DI container when available,
66
+ falling back to direct instantiation otherwise.
67
+
68
+ Args:
69
+ event: The event to dispatch.
70
+
71
+ Returns:
72
+ A list of results, one per listener invocation.
73
+ """
74
+ await self._apply_before_dispatch(event)
75
+ results: list[Result[None, str]] = []
76
+ listeners = self._registry.resolve_listeners(event)
77
+
78
+ immediate: list[type[contracts.EventListener[contracts.Event]]] = []
79
+ deferred: list[type[contracts.EventListener[contracts.Event]]] = []
80
+ [immediate.extend(listener.immediate.copy()) for listener in listeners]
81
+ [deferred.extend(listener.deferred.copy()) for listener in listeners]
82
+
83
+ match self._db.current():
84
+ case Some(tx):
85
+ for listener_cls in deferred:
86
+ _ = tx.on_commit(self._build_callback(event, listener_cls))
87
+ case Nothing():
88
+ immediate.extend(deferred)
89
+
90
+ for listener_cls in immediate:
91
+ listener = self._resolve_listener(listener_cls)
92
+ try:
93
+ results.append(await listener.handle(event))
94
+ except Exception as e:
95
+ results.append(Err(f"Listener raised: {e}"))
96
+
97
+ return results
98
+
99
+ @override
100
+ def listen[T: contracts.Event](
101
+ self,
102
+ event_cls: type[T] | list[type[T]],
103
+ listener_cls: type[contracts.EventListener[T]],
104
+ ) -> None:
105
+ """Register a listener for an event."""
106
+ if not isinstance(event_cls, list):
107
+ event_cls = [event_cls]
108
+ self._registry.register(event_cls, listener_cls)
109
+
110
+ def _resolve_listener[T: contracts.Event](
111
+ self, listener_cls: type[contracts.EventListener[T]]
112
+ ) -> contracts.EventListener[T]:
113
+ """Resolve a listener from the container.
114
+
115
+ Listeners are resolved from the DI container when available,
116
+ falling back to direct instantiation otherwise.
117
+
118
+ Returns:
119
+ EventListener[Any]: The resolved listener.
120
+ """
121
+ result = self._app.make(listener_cls)
122
+ if result.is_ok:
123
+ return result.unwrap()
124
+ return listener_cls()
125
+
126
+ def _build_callback[T: contracts.Event](
127
+ self,
128
+ event: T,
129
+ listener_cls: type[contracts.EventListener[T]],
130
+ ) -> TransactionCallback:
131
+ async def callback() -> Result[None, str]:
132
+ listener = self._resolve_listener(listener_cls)
133
+ return await listener.handle(event)
134
+
135
+ return callback
@@ -4,10 +4,11 @@ import datetime as dt
4
4
 
5
5
  import pydantic
6
6
 
7
+ from neva.events import contracts
7
8
  from neva.support import time
8
9
 
9
10
 
10
- class Event[T](pydantic.BaseModel):
11
+ class Event[T](contracts.Event):
11
12
  """Base event class."""
12
13
 
13
14
  event_id: T
@@ -0,0 +1,59 @@
1
+ """Event/listener registry."""
2
+
3
+ import inspect
4
+ from typing import Any, TypeVar
5
+
6
+ from neva.events import contracts, policy
7
+
8
+
9
+ class EventListenerRegistry[T: contracts.Event]:
10
+ """Event listener registry for a given event."""
11
+
12
+ def __init__(self) -> None:
13
+ self.immediate: list[type[contracts.EventListener[T]]] = []
14
+ self.deferred: list[type[contracts.EventListener[T]]] = []
15
+
16
+ def add_listener(self, listener_cls: type[contracts.EventListener[T]]) -> None:
17
+ """Add a listener to the correct category in the registry."""
18
+ match listener_cls.policy:
19
+ case policy.HandlingPolicy.IMMEDIATE:
20
+ self.immediate.append(listener_cls)
21
+ case policy.HandlingPolicy.DEFERRED:
22
+ self.deferred.append(listener_cls)
23
+
24
+
25
+ T = TypeVar("T", bound=contracts.Event, covariant=True)
26
+
27
+
28
+ class EventRegistry:
29
+ """Event/listener registry."""
30
+
31
+ __listeners: dict[type[Any], EventListenerRegistry[Any]]
32
+
33
+ def __init__(self) -> None:
34
+ """Initialize the registry."""
35
+ self.__listeners = {}
36
+
37
+ def register(
38
+ self,
39
+ event_cls: list[type[T]],
40
+ listener_cls: type[contracts.EventListener[T]],
41
+ ) -> None:
42
+ """Register a listener for an event."""
43
+ for cls in event_cls:
44
+ self.__listeners.setdefault(cls, EventListenerRegistry()).add_listener(
45
+ listener_cls
46
+ )
47
+
48
+ def resolve_listeners(self, event: T) -> list[EventListenerRegistry[T]]:
49
+ """Return all listeners registries for an event type."""
50
+ registries = [
51
+ self.__listeners[cls]
52
+ for cls in inspect.getmro(type(event))
53
+ if cls in self.__listeners
54
+ ]
55
+ return registries
56
+
57
+ def get_listeners(self, event: T) -> EventListenerRegistry[T]:
58
+ """Return all listeners registered for an event type."""
59
+ return self.__listeners.get(type(event), EventListenerRegistry[T]())