python-neva 3.1.1__tar.gz → 3.2.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 (136) hide show
  1. {python_neva-3.1.1 → python_neva-3.2.0}/CHANGELOG.md +32 -0
  2. {python_neva-3.1.1 → python_neva-3.2.0}/PKG-INFO +1 -1
  3. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/application.py +11 -6
  4. python_neva-3.2.0/neva/arch/markers.py +4 -0
  5. python_neva-3.2.0/neva/arch/scopes.py +5 -0
  6. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/service_provider.py +83 -2
  7. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/dispatcher.py +19 -5
  8. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/provider.py +2 -0
  9. {python_neva-3.1.1 → python_neva-3.2.0}/pyproject.toml +7 -15
  10. {python_neva-3.1.1 → python_neva-3.2.0}/scripts/retag-with-changelog.sh +1 -1
  11. python_neva-3.2.0/tests/arch/test_cache.py +52 -0
  12. python_neva-3.2.0/tests/arch/test_extends.py +98 -0
  13. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/conftest.py +2 -1
  14. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/test_dispatch.py +3 -2
  15. python_neva-3.1.1/.envrc +0 -2
  16. python_neva-3.1.1/uv.lock +0 -2440
  17. {python_neva-3.1.1 → python_neva-3.2.0}/.gitignore +0 -0
  18. {python_neva-3.1.1 → python_neva-3.2.0}/.pre-commit-config.yaml +0 -0
  19. {python_neva-3.1.1 → python_neva-3.2.0}/.python-version +0 -0
  20. {python_neva-3.1.1 → python_neva-3.2.0}/README.md +0 -0
  21. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/__init__.py +0 -0
  22. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/config.py +0 -0
  23. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/facade.py +0 -0
  24. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/integrations/__init__.py +0 -0
  25. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/integrations/faststream.py +0 -0
  26. {python_neva-3.1.1 → python_neva-3.2.0}/neva/arch/py.typed +0 -0
  27. {python_neva-3.1.1 → python_neva-3.2.0}/neva/config/__init__.py +0 -0
  28. {python_neva-3.1.1 → python_neva-3.2.0}/neva/config/base_providers.py +0 -0
  29. {python_neva-3.1.1 → python_neva-3.2.0}/neva/config/loader.py +0 -0
  30. {python_neva-3.1.1 → python_neva-3.2.0}/neva/config/py.typed +0 -0
  31. {python_neva-3.1.1 → python_neva-3.2.0}/neva/config/repository.py +0 -0
  32. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/__init__.py +0 -0
  33. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/config.py +0 -0
  34. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/connection.py +0 -0
  35. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/manager.py +0 -0
  36. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/provider.py +0 -0
  37. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/py.typed +0 -0
  38. {python_neva-3.1.1 → python_neva-3.2.0}/neva/database/transaction.py +0 -0
  39. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/__init__.py +0 -0
  40. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/event.py +0 -0
  41. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/event_registry.py +0 -0
  42. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/listener.py +0 -0
  43. {python_neva-3.1.1 → python_neva-3.2.0}/neva/events/py.typed +0 -0
  44. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/__init__.py +0 -0
  45. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/instrumentation/__init__.py +0 -0
  46. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  47. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/logging/__init__.py +0 -0
  48. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/logging/manager.py +0 -0
  49. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/logging/provider.py +0 -0
  50. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/middleware/__init__.py +0 -0
  51. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/middleware/correlation.py +0 -0
  52. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/middleware/profiler.py +0 -0
  53. {python_neva-3.1.1 → python_neva-3.2.0}/neva/obs/py.typed +0 -0
  54. {python_neva-3.1.1 → python_neva-3.2.0}/neva/polyfactory/__init__.py +0 -0
  55. {python_neva-3.1.1 → python_neva-3.2.0}/neva/polyfactory/factories.py +0 -0
  56. {python_neva-3.1.1 → python_neva-3.2.0}/neva/polyfactory/persistence.py +0 -0
  57. {python_neva-3.1.1 → python_neva-3.2.0}/neva/polyfactory/py.typed +0 -0
  58. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/__init__.py +0 -0
  59. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/encryption/__init__.py +0 -0
  60. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/encryption/encrypter.py +0 -0
  61. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/encryption/protocol.py +0 -0
  62. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/__init__.py +0 -0
  63. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/config.py +0 -0
  64. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/hash_manager.py +0 -0
  65. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/hashers/__init__.py +0 -0
  66. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/hashers/argon2.py +0 -0
  67. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  68. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/hashing/hashers/protocol.py +0 -0
  69. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/provider.py +0 -0
  70. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/py.typed +0 -0
  71. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/tokens/__init__.py +0 -0
  72. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/tokens/generate_token.py +0 -0
  73. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/tokens/hash_token.py +0 -0
  74. {python_neva-3.1.1 → python_neva-3.2.0}/neva/security/tokens/verify_token.py +0 -0
  75. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/__init__.py +0 -0
  76. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/accessors.py +0 -0
  77. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/__init__.py +0 -0
  78. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/app.py +0 -0
  79. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/app.pyi +0 -0
  80. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/config.py +0 -0
  81. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/config.pyi +0 -0
  82. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/crypt.py +0 -0
  83. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/crypt.pyi +0 -0
  84. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/db.py +0 -0
  85. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/db.pyi +0 -0
  86. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/event.py +0 -0
  87. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/event.pyi +0 -0
  88. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/hash.py +0 -0
  89. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/hash.pyi +0 -0
  90. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/log.py +0 -0
  91. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/facade/log.pyi +0 -0
  92. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/py.typed +0 -0
  93. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/results.py +0 -0
  94. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/strategy.py +0 -0
  95. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/strconv.py +0 -0
  96. {python_neva-3.1.1 → python_neva-3.2.0}/neva/support/time.py +0 -0
  97. {python_neva-3.1.1 → python_neva-3.2.0}/neva/testing/__init__.py +0 -0
  98. {python_neva-3.1.1 → python_neva-3.2.0}/neva/testing/fakes.py +0 -0
  99. {python_neva-3.1.1 → python_neva-3.2.0}/neva/testing/fixtures.py +0 -0
  100. {python_neva-3.1.1 → python_neva-3.2.0}/neva/testing/py.typed +0 -0
  101. {python_neva-3.1.1 → python_neva-3.2.0}/neva/testing/test_case.py +0 -0
  102. {python_neva-3.1.1 → python_neva-3.2.0}/ruff.toml +0 -0
  103. {python_neva-3.1.1 → python_neva-3.2.0}/tests/__init__.py +0 -0
  104. {python_neva-3.1.1 → python_neva-3.2.0}/tests/arch/__init__.py +0 -0
  105. {python_neva-3.1.1 → python_neva-3.2.0}/tests/arch/test_scope.py +0 -0
  106. {python_neva-3.1.1 → python_neva-3.2.0}/tests/config/__init__.py +0 -0
  107. {python_neva-3.1.1 → python_neva-3.2.0}/tests/config/test_loader.py +0 -0
  108. {python_neva-3.1.1 → python_neva-3.2.0}/tests/config/test_repository.py +0 -0
  109. {python_neva-3.1.1 → python_neva-3.2.0}/tests/conftest.py +0 -0
  110. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/__init__.py +0 -0
  111. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_connection_manager.py +0 -0
  112. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_database_manager.py +0 -0
  113. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_edge_cases.py +0 -0
  114. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_multi_connection.py +0 -0
  115. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  116. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_transaction.py +0 -0
  117. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_transaction_context.py +0 -0
  118. {python_neva-3.1.1 → python_neva-3.2.0}/tests/database/test_transaction_registry.py +0 -0
  119. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/__init__.py +0 -0
  120. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/test_deferred.py +0 -0
  121. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/test_event.py +0 -0
  122. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/test_event_registry.py +0 -0
  123. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/test_function_listener.py +0 -0
  124. {python_neva-3.1.1 → python_neva-3.2.0}/tests/events/test_immediate.py +0 -0
  125. {python_neva-3.1.1 → python_neva-3.2.0}/tests/obs/__init__.py +0 -0
  126. {python_neva-3.1.1 → python_neva-3.2.0}/tests/obs/test_correlation.py +0 -0
  127. {python_neva-3.1.1 → python_neva-3.2.0}/tests/obs/test_profiler.py +0 -0
  128. {python_neva-3.1.1 → python_neva-3.2.0}/tests/security/__init__.py +0 -0
  129. {python_neva-3.1.1 → python_neva-3.2.0}/tests/security/test_encrypter.py +0 -0
  130. {python_neva-3.1.1 → python_neva-3.2.0}/tests/security/test_hash_manager.py +0 -0
  131. {python_neva-3.1.1 → python_neva-3.2.0}/tests/testing/__init__.py +0 -0
  132. {python_neva-3.1.1 → python_neva-3.2.0}/tests/testing/test_event_fake.py +0 -0
  133. {python_neva-3.1.1 → python_neva-3.2.0}/tests/testing/test_facade_restore.py +0 -0
  134. {python_neva-3.1.1 → python_neva-3.2.0}/tests/testing/test_fixtures.py +0 -0
  135. {python_neva-3.1.1 → python_neva-3.2.0}/tests/testing/test_refresh_database.py +0 -0
  136. {python_neva-3.1.1 → python_neva-3.2.0}/tests/testing/test_test_case.py +0 -0
@@ -46,3 +46,35 @@
46
46
 
47
47
  - improve auto-annotation of tags
48
48
  - Fix tagging script
49
+
50
+ ## python-neva-v3.2.0 (2026-06-04)
51
+
52
+ ### ✨ Features
53
+
54
+ - **core**: add optional activator on all binding functions
55
+ - **core**: remove scoping option from scoped, default to request scope
56
+ - **core**: introduce conditional activation
57
+ - **core**: add specifying interface in extends method
58
+ - **core**: add extends feature
59
+
60
+ ### build
61
+
62
+ - **core**: remove redundant .envrc files
63
+ - **core**: fixing version derivation
64
+
65
+ ### docs
66
+
67
+ - **core,-fastapi**: add contributing documentation
68
+
69
+ ### feat
70
+
71
+ - **core**: allow passing context data to scope method
72
+ - **core**: add some utility functions for binding
73
+ - **core**: add some utility functions for binding
74
+ - **core**: add the possibility to turn off dependency caching
75
+ - **core,-fastapi**: add .envrc file
76
+
77
+ ### refactor
78
+
79
+ - **core**: cleaning up type hints
80
+ - **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.2.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -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.
@@ -1,7 +1,5 @@
1
1
  """Base implementation of the event dispatcher."""
2
2
 
3
- from typing import final
4
-
5
3
  from neva.arch.application import Application
6
4
  from neva.database.manager import DatabaseManager
7
5
  from neva.database.transaction import TransactionCallback
@@ -11,15 +9,30 @@ from neva.events.listener import EventListener
11
9
  from neva.support import Err, Nothing, Result, Some
12
10
 
13
11
 
14
- @final
15
12
  class EventDispatcher:
16
13
  """Event dispatcher implementation."""
17
14
 
18
- def __init__(self, app: Application, db: DatabaseManager) -> None:
19
- self._registry = EventRegistry()
15
+ def __init__(
16
+ self,
17
+ app: Application,
18
+ db: DatabaseManager,
19
+ registry: EventRegistry,
20
+ ) -> None:
21
+ self._registry = registry
20
22
  self._app = app
21
23
  self._db = db
22
24
 
25
+ async def _before_dispatch(self, event: Event) -> None:
26
+ """Extension hook called before listeners are invoked.
27
+
28
+ Override in a subclass to add cross-cutting behaviour such as
29
+ persisting the event to an event store. The default implementation
30
+ is a no-op.
31
+
32
+ Args:
33
+ event: The event about to be dispatched.
34
+ """
35
+
23
36
  async def dispatch(self, event: Event) -> list[Result[None, str]]:
24
37
  """Dispatch an event to all registered listeners.
25
38
 
@@ -32,6 +45,7 @@ class EventDispatcher:
32
45
  Returns:
33
46
  A list of results, one per listener invocation.
34
47
  """
48
+ await self._before_dispatch(event)
35
49
  results: list[Result[None, str]] = []
36
50
  listeners = self._registry.get_listeners(event)
37
51
 
@@ -3,6 +3,7 @@
3
3
  from typing import Self, override
4
4
 
5
5
  from neva.arch.service_provider import ServiceProvider
6
+ from neva.events.event_registry import EventRegistry
6
7
  from neva.support import Ok, Result
7
8
 
8
9
 
@@ -23,5 +24,6 @@ class EventServiceProvider(ServiceProvider):
23
24
  """
24
25
  from neva.events.dispatcher import EventDispatcher
25
26
 
27
+ self.bind(EventRegistry)
26
28
  self.bind(EventDispatcher)
27
29
  return Ok(self)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-neva"
3
- dynamic = ["version"]
3
+ version = "3.2.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -39,32 +39,24 @@ dev = [
39
39
  testing = ["pytest>=9.0.2", "pytest-asyncio>=0.25.3"]
40
40
  fastapi = ["neva-fastapi>=1.0.0"]
41
41
 
42
- [tool.uv.sources]
43
- neva-fastapi = { path = "../neva-fastapi", editable = true }
44
-
45
42
  [build-system]
46
- requires = ["hatchling", "versioningit"]
43
+ requires = ["hatchling"]
47
44
  build-backend = "hatchling.build"
48
45
 
49
46
  [tool.hatch.build.targets.wheel]
50
47
  packages = ["neva"]
51
48
 
52
- [tool.hatch.version]
53
- source = "versioningit"
54
-
55
- [tool.hatch.version.format]
56
- distance = "{base_version}.dev{distance}+{vcs}{rev}"
57
- dirty = "{version}+dirty"
58
- distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.dirty"
59
-
60
49
  [tool.commitizen]
61
50
  name = "cz_gitmoji"
62
- version_provider = "scm"
63
- tag_format = "$version"
51
+ version_provider = "uv"
52
+ tag_format = "python-neva-v$version"
64
53
  version_scheme = "pep440"
65
54
  update_changelog_on_bump = true
66
55
  annotated_tag = true
67
56
  post_bump_hooks = ["scripts/retag-with-changelog.sh"]
57
+ # Only commits scoped to `core` (alone or in a comma-separated scope list) feed
58
+ # this package's changelog. See CONTRIBUTING.md.
59
+ changelog_pattern = '''^(?::[\w_]+:\s+)?\w+\([^)]*\bcore\b[^)]*\)!?:\s'''
68
60
 
69
61
  [tool.basedpyright]
70
62
  enableExperimentalFeatures = true
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env sh
2
2
  set -e
3
3
 
4
- TAG="${CZ_POST_CURRENT_VERSION}"
4
+ TAG="python-neva-v${CZ_POST_CURRENT_VERSION}"
5
5
 
6
6
  # Generate the changelog section for this version only
7
7
  CHANGELOG=$(cz changelog "${CZ_POST_CURRENT_VERSION}" --dry-run |
@@ -0,0 +1,52 @@
1
+ import asyncio
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Self, override
5
+
6
+ from neva.arch import ServiceProvider
7
+ from neva.support import Ok, Result
8
+ from neva.support.time import utcnow
9
+ from neva.testing import TestCase
10
+
11
+
12
+ def _make_config_dir(tmp_path: Path) -> Path:
13
+ config_dir = tmp_path / "config"
14
+ config_dir.mkdir()
15
+
16
+ _ = (config_dir / "app.py").write_text(
17
+ 'config = {"name": "TestApp", "debug": True}'
18
+ )
19
+ _ = (config_dir / "providers.py").write_text(
20
+ """
21
+ from tests.arch.test_cache import CacheServiceProvider
22
+
23
+ config = {"providers": [CacheServiceProvider]}
24
+ """
25
+ )
26
+
27
+ return config_dir
28
+
29
+
30
+ class CacheProvider:
31
+ def __init__(self) -> None:
32
+ self.ts: datetime = utcnow()
33
+
34
+
35
+ class CacheServiceProvider(ServiceProvider):
36
+ @override
37
+ def register(self) -> Result[Self, str]:
38
+ self.bind(CacheProvider, cache=False)
39
+ return Ok(self)
40
+
41
+
42
+ class TestCache(TestCase):
43
+ @override
44
+ def create_config(self, tmp_path: Path) -> Path:
45
+ return _make_config_dir(tmp_path)
46
+
47
+ async def test_transient_dependency(self) -> None:
48
+ provider1 = self.app.make(CacheProvider).unwrap()
49
+ await asyncio.sleep(0.1)
50
+ provider2 = self.app.make(CacheProvider).unwrap()
51
+ assert provider1 is not provider2
52
+ assert provider1.ts != provider2.ts
@@ -0,0 +1,98 @@
1
+ from dataclasses import dataclass
2
+ import uuid
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Protocol, Self, override
6
+
7
+ from neva.arch import ServiceProvider
8
+ from neva.support import Ok, Result
9
+ from neva.support.time import utcnow
10
+ from neva.testing import TestCase
11
+
12
+
13
+ def _base_config_dir(tmp_path: Path) -> Path:
14
+ config_dir = tmp_path / "config"
15
+ config_dir.mkdir()
16
+
17
+ _ = (config_dir / "app.py").write_text(
18
+ 'config = {"name": "TestApp", "debug": True}'
19
+ )
20
+ return config_dir
21
+
22
+
23
+ def _make_config_dir(tmp_path: Path) -> Path:
24
+ config_dir = _base_config_dir(tmp_path)
25
+
26
+ _ = (config_dir / "providers.py").write_text(
27
+ """
28
+ from tests.arch.test_extends import CacheServiceProvider, ExtendsServiceProvider
29
+
30
+ config = {"providers": [CacheServiceProvider, ExtendsServiceProvider]}
31
+ """
32
+ )
33
+
34
+ return config_dir
35
+
36
+
37
+ class CacheProtocol(Protocol):
38
+ def add(self, key: str, value: str) -> Self: ...
39
+ def get(self, key: str) -> str: ...
40
+
41
+
42
+ class Cache:
43
+ def __init__(self) -> None:
44
+ self.ts: datetime = utcnow()
45
+ self._inner: dict[str, str] = {}
46
+
47
+ def add(self, key: str, value: str) -> Self:
48
+ self._inner[key] = value
49
+ return self
50
+
51
+ def get(self, key: str) -> str:
52
+ return self._inner[key]
53
+
54
+
55
+ class TransientCache(Cache):
56
+ pass
57
+
58
+
59
+ class CacheServiceProvider(ServiceProvider):
60
+ @override
61
+ def register(self) -> Result[Self, str]:
62
+ self.bind(Cache)
63
+ self.bind(TransientCache, cache=False)
64
+ return Ok(self)
65
+
66
+
67
+ class ExtendsServiceProvider(ServiceProvider):
68
+ def override_cache(self, cache: Cache) -> Cache:
69
+ return cache.add("override", uuid.uuid4().hex)
70
+
71
+ def override_transient_cache(self, cache: TransientCache) -> TransientCache:
72
+ return cache.add("override", uuid.uuid4().hex)
73
+
74
+ @override
75
+ def register(self) -> Result[Self, str]:
76
+ _ = self.extend(self.override_cache)
77
+ _ = self.extend(self.override_transient_cache)
78
+ return Ok(self)
79
+
80
+
81
+ class TestExtends(TestCase):
82
+ @override
83
+ def create_config(self, tmp_path: Path) -> Path:
84
+ return _make_config_dir(tmp_path)
85
+
86
+ async def test_extends_works(self) -> None:
87
+ cache = self.app.make(Cache).unwrap()
88
+ assert cache.get("override") is not None
89
+
90
+ async def test_extends_respects_cache(self) -> None:
91
+ cache = self.app.make(Cache).unwrap()
92
+ assert cache.get("override") is not None
93
+ cache2 = self.app.make(Cache).unwrap()
94
+ assert cache.get("override") == cache2.get("override")
95
+
96
+ cache3 = self.app.make(TransientCache).unwrap()
97
+ cache4 = self.app.make(TransientCache).unwrap()
98
+ assert cache3.get("override") != cache4.get("override")
@@ -8,6 +8,7 @@ import pytest
8
8
  from neva.arch import Application
9
9
  from neva.database.manager import DatabaseManager
10
10
  from neva.events import Event, EventDispatcher
11
+ from neva.events.event_registry import EventRegistry
11
12
 
12
13
 
13
14
  class OrderPlaced(Event[int]):
@@ -25,4 +26,4 @@ class UserCreated(Event[int]):
25
26
 
26
27
  @pytest.fixture
27
28
  def dispatcher(application: Application, db: DatabaseManager) -> EventDispatcher:
28
- return EventDispatcher(app=application, db=db)
29
+ return EventDispatcher(app=application, db=db, registry=EventRegistry())
@@ -7,6 +7,7 @@ import dishka
7
7
  from neva.arch import Application
8
8
  from neva.database.manager import DatabaseManager
9
9
  from neva.events import EventDispatcher, EventListener
10
+ from neva.events.event_registry import EventRegistry
10
11
  from neva.support import Err, Ok, Result
11
12
  from tests.events.conftest import OrderPlaced
12
13
 
@@ -134,7 +135,7 @@ class TestDIResolution:
134
135
 
135
136
  di_instance_id = id((await application.make_async(TrackedListener)).unwrap())
136
137
 
137
- dispatcher = EventDispatcher(app=application, db=db)
138
+ dispatcher = EventDispatcher(app=application, db=db, registry=EventRegistry())
138
139
  dispatcher.listen(OrderPlaced, TrackedListener)
139
140
  _ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
140
141
 
@@ -153,7 +154,7 @@ class TestDIResolution:
153
154
  async def handle(self, event: OrderPlaced) -> Result[None, str]:
154
155
  return Ok(None)
155
156
 
156
- dispatcher = EventDispatcher(app=application, db=db)
157
+ dispatcher = EventDispatcher(app=application, db=db, registry=EventRegistry())
157
158
  dispatcher.listen(OrderPlaced, UnboundListener)
158
159
  _ = await dispatcher.dispatch(OrderPlaced(event_id=1, order_id=1))
159
160
 
python_neva-3.1.1/.envrc DELETED
@@ -1,2 +0,0 @@
1
- export VIRTUAL_ENV="$PWD/.venv"
2
- layout python