python-neva 3.1.0__py3-none-any.whl → 3.2.0__py3-none-any.whl

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.
neva/arch/application.py CHANGED
@@ -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
 
neva/arch/markers.py ADDED
@@ -0,0 +1,4 @@
1
+ from dishka.entities.marker import BaseMarker, Has, Marker
2
+
3
+
4
+ __all__ = ["BaseMarker", "Has", "Marker"]
neva/arch/scopes.py ADDED
@@ -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.
neva/events/dispatcher.py CHANGED
@@ -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
 
neva/events/provider.py CHANGED
@@ -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
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 3.1.0
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
@@ -16,7 +16,7 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
16
16
  Requires-Dist: structlog>=25.5.0
17
17
  Requires-Dist: typer>=0.21.1
18
18
  Provides-Extra: fastapi
19
- Requires-Dist: neva-fastapi>=0.1.0; extra == 'fastapi'
19
+ 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'
@@ -1,9 +1,11 @@
1
1
  neva/arch/__init__.py,sha256=1ukTl5Ynz5PrGa5e-tFSMaNgVkfhQRrYQGPUqUeXMMg,429
2
- neva/arch/application.py,sha256=9qvg_MeSjhgQZnX6RSuWaJQfmC5GqEd2XtHR6FGGfpc,7142
2
+ neva/arch/application.py,sha256=DH1CXVEsiOCDQIigOXfXI2jYGVZr-TwMgilTQu6qPq0,7258
3
3
  neva/arch/config.py,sha256=R8_wMKTwX8vTFqfQhgglDQ9PfB6Yo6mx6OnKg8bnIWw,1042
4
4
  neva/arch/facade.py,sha256=1kYVhyYto8zSyOZV6sAYJ3xWzQDJRzoVV-e_EarTp0Q,7927
5
+ neva/arch/markers.py,sha256=STdhmW88qV8uogIa9kSD9gs-uMRaglZI8HOw3PrhX1Q,103
5
6
  neva/arch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- neva/arch/service_provider.py,sha256=iXOicvbkZ-c455NElEeWD9tzZyLVIXkje-C3PQWiE6k,3165
7
+ neva/arch/scopes.py,sha256=Gu0kJk39Yv8LCCzxjdu0lIE0FXtnsG36LGKDuNin2CE,87
8
+ neva/arch/service_provider.py,sha256=zEFwix2cL6l1Frv9w1OMtnL_zjkl_BVpGBhRaNK6bb0,5636
7
9
  neva/arch/integrations/__init__.py,sha256=cXuqC4JfZdGYLlh6POgYUVXUT2NQ84276eC-RYTyFxk,75
8
10
  neva/arch/integrations/faststream.py,sha256=AOwfpfdL3O5HLAx80SwUGQb_hKEeAkFOUyIXtjODaHM,1696
9
11
  neva/config/__init__.py,sha256=Ygfnh-MRRKYDdf54_LeMSHwmck5VpNHTdnnMtGEFLtU,121
@@ -19,11 +21,11 @@ neva/database/provider.py,sha256=l2be4Ri53RtQq1-qzohy3cqzJQpTaNzqXGVhXPKUCXc,167
19
21
  neva/database/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
22
  neva/database/transaction.py,sha256=_0Dsao_vhCJT5YRUmrHOLb0uxvfdE-5k3n0yMPLC1o8,5167
21
23
  neva/events/__init__.py,sha256=xwIAXNcOASpuCY4DJpshZc1gtxQHimzwy-s7c-4QuiQ,746
22
- neva/events/dispatcher.py,sha256=i7PYbesKnX6PkbRZomBfUOG2oNQ__eXPrKnglzwJFo0,2858
24
+ neva/events/dispatcher.py,sha256=mSjOvP1-new7z2QRBY3cbUzZ1Y_mlHSXMRt4NGhB8e0,3295
23
25
  neva/events/event.py,sha256=g3vAx-2qiR3QRM_or8G9mK5qx66A2LZiZ3n517R4lGQ,251
24
26
  neva/events/event_registry.py,sha256=jucYOUjCERz-ICdPiDrxYw_ySvmPJOcRzYKCgka29qY,1491
25
27
  neva/events/listener.py,sha256=-7RQOhVusKN8lopB3T2yfoQVlxX6GOSM5Pk1mPEMMro,1833
26
- neva/events/provider.py,sha256=L-yYJMM1KelWItUKCrESrmrF6On8SFg-OxpXF1--SVE,771
28
+ neva/events/provider.py,sha256=HE7k915m8lQiqBC6kaGtb3ZfoxrgsuXXZmxkaaSnlmM,857
27
29
  neva/events/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
30
  neva/obs/__init__.py,sha256=dVzgljk9Hvo44LI34RcwbDyT42_z4nSnFVmL4GLbKD0,331
29
31
  neva/obs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -83,6 +85,6 @@ neva/testing/fakes.py,sha256=5HuuwMeAKhFSZfn9T8raQIjdgHkjFeZsFXDud0T4v7Q,2674
83
85
  neva/testing/fixtures.py,sha256=oiPa5ntUkW-SbySDvwEHXtti8NqSwI-R0CT1_ezTx_Y,1445
84
86
  neva/testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
87
  neva/testing/test_case.py,sha256=zJBZJeKI-sv-EOaXNuSJ0V2Zp_6hdk64W4dOh0eP8RI,3290
86
- python_neva-3.1.0.dist-info/METADATA,sha256=d-vn_HcJkLsHsKTjd9t0h64E32BnM5IUCE7wCab6BEU,771
87
- python_neva-3.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
88
- python_neva-3.1.0.dist-info/RECORD,,
88
+ python_neva-3.2.0.dist-info/METADATA,sha256=2wZeNjt-ooHN3AIU1LE-K636R4shKM0-cIDXUGg1X2g,771
89
+ python_neva-3.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
90
+ python_neva-3.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any