python-cq 0.3.0__tar.gz → 0.4.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.
@@ -0,0 +1,322 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm,dotenv,macos
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm,dotenv,macos
3
+
4
+ ### dotenv ###
5
+ .env
6
+
7
+ ### macOS ###
8
+ # General
9
+ .DS_Store
10
+ .AppleDouble
11
+ .LSOverride
12
+
13
+ # Icon must end with two \r
14
+ Icon
15
+
16
+
17
+ # Thumbnails
18
+ ._*
19
+
20
+ # Files that might appear in the root of a volume
21
+ .DocumentRevisions-V100
22
+ .fseventsd
23
+ .Spotlight-V100
24
+ .TemporaryItems
25
+ .Trashes
26
+ .VolumeIcon.icns
27
+ .com.apple.timemachine.donotpresent
28
+
29
+ # Directories potentially created on remote AFP share
30
+ .AppleDB
31
+ .AppleDesktop
32
+ Network Trash Folder
33
+ Temporary Items
34
+ .apdisk
35
+
36
+ ### macOS Patch ###
37
+ # iCloud generated files
38
+ *.icloud
39
+
40
+ ### PyCharm ###
41
+ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
42
+ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
43
+
44
+ # User-specific stuff
45
+ .idea/**/workspace.xml
46
+ .idea/**/tasks.xml
47
+ .idea/**/usage.statistics.xml
48
+ .idea/**/dictionaries
49
+ .idea/**/shelf
50
+
51
+ # AWS User-specific
52
+ .idea/**/aws.xml
53
+
54
+ # Generated files
55
+ .idea/**/contentModel.xml
56
+
57
+ # Sensitive or high-churn files
58
+ .idea/**/dataSources/
59
+ .idea/**/dataSources.ids
60
+ .idea/**/dataSources.local.xml
61
+ .idea/**/sqlDataSources.xml
62
+ .idea/**/dynamic.xml
63
+ .idea/**/uiDesigner.xml
64
+ .idea/**/dbnavigator.xml
65
+
66
+ # Gradle
67
+ .idea/**/gradle.xml
68
+ .idea/**/libraries
69
+
70
+ # Gradle and Maven with auto-import
71
+ # When using Gradle or Maven with auto-import, you should exclude module files,
72
+ # since they will be recreated, and may cause churn. Uncomment if using
73
+ # auto-import.
74
+ # .idea/artifacts
75
+ # .idea/compiler.xml
76
+ # .idea/jarRepositories.xml
77
+ # .idea/modules.xml
78
+ # .idea/*.iml
79
+ # .idea/modules
80
+ # *.iml
81
+ # *.ipr
82
+
83
+ # CMake
84
+ cmake-build-*/
85
+
86
+ # Mongo Explorer plugin
87
+ .idea/**/mongoSettings.xml
88
+
89
+ # File-based project format
90
+ *.iws
91
+
92
+ # IntelliJ
93
+ out/
94
+
95
+ # mpeltonen/sbt-idea plugin
96
+ .idea_modules/
97
+
98
+ # JIRA plugin
99
+ atlassian-ide-plugin.xml
100
+
101
+ # Cursive Clojure plugin
102
+ .idea/replstate.xml
103
+
104
+ # SonarLint plugin
105
+ .idea/sonarlint/
106
+
107
+ # Crashlytics plugin (for Android Studio and IntelliJ)
108
+ com_crashlytics_export_strings.xml
109
+ crashlytics.properties
110
+ crashlytics-build.properties
111
+ fabric.properties
112
+
113
+ # Editor-based Rest Client
114
+ .idea/httpRequests
115
+
116
+ # Android studio 3.1+ serialized cache file
117
+ .idea/caches/build_file_checksums.ser
118
+
119
+ ### PyCharm Patch ###
120
+ # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
121
+
122
+ # *.iml
123
+ # modules.xml
124
+ # .idea/misc.xml
125
+ # *.ipr
126
+
127
+ # Sonarlint plugin
128
+ # https://plugins.jetbrains.com/plugin/7973-sonarlint
129
+ .idea/**/sonarlint/
130
+
131
+ # SonarQube Plugin
132
+ # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
133
+ .idea/**/sonarIssues.xml
134
+
135
+ # Markdown Navigator plugin
136
+ # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
137
+ .idea/**/markdown-navigator.xml
138
+ .idea/**/markdown-navigator-enh.xml
139
+ .idea/**/markdown-navigator/
140
+
141
+ # Cache file creation bug
142
+ # See https://youtrack.jetbrains.com/issue/JBR-2257
143
+ .idea/$CACHE_FILE$
144
+
145
+ # CodeStream plugin
146
+ # https://plugins.jetbrains.com/plugin/12206-codestream
147
+ .idea/codestream.xml
148
+
149
+ # Azure Toolkit for IntelliJ plugin
150
+ # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
151
+ .idea/**/azureSettings.xml
152
+
153
+ ### Python ###
154
+ # Byte-compiled / optimized / DLL files
155
+ __pycache__/
156
+ *.py[cod]
157
+ *$py.class
158
+
159
+ # C extensions
160
+ *.so
161
+
162
+ # Distribution / packaging
163
+ .Python
164
+ build/
165
+ develop-eggs/
166
+ dist/
167
+ downloads/
168
+ eggs/
169
+ .eggs/
170
+ lib/
171
+ lib64/
172
+ parts/
173
+ sdist/
174
+ var/
175
+ wheels/
176
+ share/python-wheels/
177
+ *.egg-info/
178
+ .installed.cfg
179
+ *.egg
180
+ MANIFEST
181
+
182
+ # PyInstaller
183
+ # Usually these files are written by a python script from a template
184
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
185
+ *.manifest
186
+ *.spec
187
+
188
+ # Installer logs
189
+ pip-log.txt
190
+ pip-delete-this-directory.txt
191
+
192
+ # Unit test / coverage reports
193
+ htmlcov/
194
+ .tox/
195
+ .nox/
196
+ .coverage
197
+ .coverage.*
198
+ .cache
199
+ nosetests.xml
200
+ coverage.xml
201
+ *.cover
202
+ *.py,cover
203
+ .hypothesis/
204
+ .pytest_cache/
205
+ cover/
206
+
207
+ # Translations
208
+ *.mo
209
+ *.pot
210
+
211
+ # Django stuff:
212
+ *.log
213
+ local_settings.py
214
+ db.sqlite3
215
+ db.sqlite3-journal
216
+
217
+ # Flask stuff:
218
+ instance/
219
+ .webassets-cache
220
+
221
+ # Scrapy stuff:
222
+ .scrapy
223
+
224
+ # Sphinx documentation
225
+ docs/_build/
226
+
227
+ # PyBuilder
228
+ .pybuilder/
229
+ target/
230
+
231
+ # Jupyter Notebook
232
+ .ipynb_checkpoints
233
+
234
+ # IPython
235
+ profile_default/
236
+ ipython_config.py
237
+
238
+ # pyenv
239
+ # For a library or package, you might want to ignore these files since the code is
240
+ # intended to run in multiple environments; otherwise, check them in:
241
+ # .python-version
242
+
243
+ # pipenv
244
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
245
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
246
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
247
+ # install all needed dependencies.
248
+ #Pipfile.lock
249
+
250
+ # poetry
251
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
252
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
253
+ # commonly ignored for libraries.
254
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
255
+ #poetry.lock
256
+
257
+ # pdm
258
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
259
+ #pdm.lock
260
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
261
+ # in version control.
262
+ # https://pdm.fming.dev/#use-with-ide
263
+ .pdm.toml
264
+
265
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
266
+ __pypackages__/
267
+
268
+ # Celery stuff
269
+ celerybeat-schedule
270
+ celerybeat.pid
271
+
272
+ # SageMath parsed files
273
+ *.sage.py
274
+
275
+ # Environments
276
+ .venv
277
+ env/
278
+ venv/
279
+ ENV/
280
+ env.bak/
281
+ venv.bak/
282
+
283
+ # Spyder project settings
284
+ .spyderproject
285
+ .spyproject
286
+
287
+ # Rope project settings
288
+ .ropeproject
289
+
290
+ # mkdocs documentation
291
+ /site
292
+
293
+ # mypy
294
+ .mypy_cache/
295
+ .dmypy.json
296
+ dmypy.json
297
+
298
+ # Pyre type checker
299
+ .pyre/
300
+
301
+ # pytype static type analyzer
302
+ .pytype/
303
+
304
+ # Cython debug symbols
305
+ cython_debug/
306
+
307
+ # PyCharm
308
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
309
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
310
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
311
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
312
+ #.idea/
313
+
314
+ # End of https://www.toptal.com/developers/gitignore/api/python,pycharm,dotenv,macos
315
+
316
+ # Pyenv
317
+ .python-version
318
+
319
+ # Code editors
320
+ .fleet/
321
+ .idea/
322
+ .vscode/
@@ -1,29 +1,26 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Lightweight CQRS library.
5
- Home-page: https://github.com/100nm/python-cq
5
+ Project-URL: Repository, https://github.com/100nm/python-cq
6
+ Author: remimd
6
7
  License: MIT
7
8
  Keywords: cqrs
8
- Author: remimd
9
- Requires-Python: >=3.12,<4
10
9
  Classifier: Development Status :: 4 - Beta
11
10
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
11
  Classifier: Natural Language :: English
14
12
  Classifier: Operating System :: OS Independent
15
13
  Classifier: Programming Language :: Python
16
14
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
15
  Classifier: Programming Language :: Python :: 3 :: Only
20
16
  Classifier: Topic :: Software Development :: Libraries
21
17
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
19
  Classifier: Typing :: Typed
24
- Requires-Dist: pydantic (>=2,<3)
20
+ Requires-Python: <4,>=3.12
21
+ Requires-Dist: anyio
22
+ Requires-Dist: pydantic<3,>=2
25
23
  Requires-Dist: python-injection
26
- Project-URL: Repository, https://github.com/100nm/python-cq
27
24
  Description-Content-Type: text/markdown
28
25
 
29
26
  # python-cq
@@ -51,4 +48,3 @@ pip install python-cq
51
48
  * [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
52
49
  * [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
53
50
  * [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
54
-
@@ -18,10 +18,13 @@ from ._core.message import (
18
18
  query_handler,
19
19
  )
20
20
  from ._core.middleware import Middleware, MiddlewareResult
21
+ from ._core.related_events import RelatedEvents
22
+ from ._core.scope import CQScope
21
23
 
22
24
  __all__ = (
23
25
  "AnyCommandBus",
24
26
  "Bus",
27
+ "CQScope",
25
28
  "Command",
26
29
  "CommandBus",
27
30
  "DTO",
@@ -33,6 +36,7 @@ __all__ = (
33
36
  "Pipe",
34
37
  "Query",
35
38
  "QueryBus",
39
+ "RelatedEvents",
36
40
  "command_handler",
37
41
  "event_handler",
38
42
  "get_command_bus",
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  from abc import ABC, abstractmethod
3
2
  from collections.abc import Awaitable, Callable
4
3
  from typing import Protocol, Self, runtime_checkable
@@ -14,10 +13,6 @@ class Dispatcher[I, O](Protocol):
14
13
  async def dispatch(self, input_value: I, /) -> O:
15
14
  raise NotImplementedError
16
15
 
17
- @abstractmethod
18
- def dispatch_no_wait(self, *input_values: I) -> None:
19
- raise NotImplementedError
20
-
21
16
  @abstractmethod
22
17
  def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
23
18
  raise NotImplementedError
@@ -31,12 +26,6 @@ class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
31
26
  def __init__(self) -> None:
32
27
  self.__middleware_group = MiddlewareGroup()
33
28
 
34
- def dispatch_no_wait(self, *input_values: I) -> None:
35
- asyncio.gather(
36
- *(self.dispatch(input_value) for input_value in input_values),
37
- return_exceptions=True,
38
- )
39
-
40
29
  def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
41
30
  self.__middleware_group.add(*middlewares)
42
31
  return self
@@ -1,18 +1,18 @@
1
- import asyncio
2
1
  from abc import ABC, abstractmethod
3
2
  from collections import defaultdict
4
3
  from collections.abc import Awaitable, Callable
5
4
  from dataclasses import dataclass, field
6
- from inspect import isclass
5
+ from inspect import getmro, isclass
7
6
  from types import GenericAlias
8
7
  from typing import Any, Protocol, Self, TypeAliasType, runtime_checkable
9
8
 
9
+ import anyio
10
10
  import injection
11
11
 
12
12
  from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
13
13
 
14
14
  type HandlerType[**P, T] = type[Handler[P, T]]
15
- type HandlerFactory[**P, T] = Callable[..., Handler[P, T]]
15
+ type HandlerFactory[**P, T] = Callable[..., Awaitable[Handler[P, T]]]
16
16
 
17
17
  type Listener[T] = Callable[[T], Awaitable[Any]]
18
18
 
@@ -46,13 +46,13 @@ class SubscriberDecorator[I, O]:
46
46
  bus_type: BusType[I, O] | TypeAliasType | GenericAlias
47
47
  injection_module: injection.Module = field(default_factory=injection.mod)
48
48
 
49
- def __call__(self, first_input_type: type[I], /, *input_types: type[I]): # type: ignore[no-untyped-def]
50
- def decorator(wrapped): # type: ignore[no-untyped-def]
49
+ def __call__(self, first_input_type: type[I], /, *input_types: type[I]) -> Any:
50
+ def decorator(wrapped: type[Handler[[I], O]]) -> type[Handler[[I], O]]:
51
51
  if not isclass(wrapped) or not issubclass(wrapped, Handler):
52
52
  raise TypeError(f"`{wrapped}` isn't a valid handler.")
53
53
 
54
- bus = self.__find_bus()
55
- factory = self.injection_module.make_injected_function(wrapped)
54
+ bus = self.injection_module.find_instance(self.bus_type)
55
+ factory = self.injection_module.make_async_factory(wrapped)
56
56
 
57
57
  for input_type in (first_input_type, *input_types):
58
58
  bus.subscribe(input_type, factory)
@@ -61,9 +61,6 @@ class SubscriberDecorator[I, O]:
61
61
 
62
62
  return decorator
63
63
 
64
- def __find_bus(self) -> Bus[I, O]:
65
- return self.injection_module.find_instance(self.bus_type)
66
-
67
64
 
68
65
  class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
69
66
  __slots__ = ("__listeners",)
@@ -79,7 +76,24 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
79
76
  return self
80
77
 
81
78
  async def _trigger_listeners(self, input_value: I, /) -> None:
82
- await asyncio.gather(*(listener(input_value) for listener in self.__listeners))
79
+ listeners = self.__listeners
80
+
81
+ if not listeners:
82
+ return
83
+
84
+ async with anyio.create_task_group() as task_group:
85
+ for listener in listeners:
86
+ task_group.start_soon(listener, input_value)
87
+
88
+ @staticmethod
89
+ def _make_handle_function(
90
+ handler_factory: HandlerFactory[[I], O],
91
+ ) -> Callable[[I], Awaitable[O]]:
92
+ async def handle(input_value: I) -> O:
93
+ handler = await handler_factory()
94
+ return await handler.handle(input_value)
95
+
96
+ return handle
83
97
 
84
98
 
85
99
  class SimpleBus[I, O](BaseBus[I, O]):
@@ -93,17 +107,16 @@ class SimpleBus[I, O](BaseBus[I, O]):
93
107
 
94
108
  async def dispatch(self, input_value: I, /) -> O:
95
109
  await self._trigger_listeners(input_value)
96
- input_type = type(input_value)
97
110
 
98
- try:
99
- handler_factory = self.__handlers[input_type]
100
- except KeyError:
111
+ for input_type in getmro(type(input_value)):
112
+ if handler_factory := self.__handlers.get(input_type):
113
+ break
114
+
115
+ else:
101
116
  return NotImplemented
102
117
 
103
- return await self._invoke_with_middlewares(
104
- handler_factory().handle,
105
- input_value,
106
- )
118
+ handler = self._make_handle_function(handler_factory)
119
+ return await self._invoke_with_middlewares(handler, input_value)
107
120
 
108
121
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
109
122
  if input_type in self.__handlers:
@@ -126,20 +139,22 @@ class TaskBus[I](BaseBus[I, None]):
126
139
 
127
140
  async def dispatch(self, input_value: I, /) -> None:
128
141
  await self._trigger_listeners(input_value)
129
- handler_factories = self.__handlers.get(type(input_value))
130
142
 
131
- if not handler_factories:
143
+ for input_type in getmro(type(input_value)):
144
+ if handler_factories := self.__handlers.get(input_type):
145
+ break
146
+
147
+ else:
132
148
  return
133
149
 
134
- await asyncio.gather(
135
- *(
136
- self._invoke_with_middlewares(
137
- handler_factory().handle,
150
+ async with anyio.create_task_group() as task_group:
151
+ for handler_factory in handler_factories:
152
+ handler = self._make_handle_function(handler_factory)
153
+ task_group.start_soon(
154
+ self._invoke_with_middlewares,
155
+ handler,
138
156
  input_value,
139
157
  )
140
- for handler_factory in handler_factories
141
- )
142
- )
143
158
 
144
159
  def subscribe(
145
160
  self,
@@ -24,14 +24,14 @@ class Pipe[I, O](BaseDispatcher[I, O]):
24
24
  self.__dispatcher = dispatcher
25
25
  self.__steps = []
26
26
 
27
- def step[T]( # type: ignore[no-untyped-def]
27
+ def step[T](
28
28
  self,
29
29
  wrapped: PipeConverter[T, Any] | None = None,
30
30
  /,
31
31
  *,
32
32
  dispatcher: Dispatcher[T, Any] | None = None,
33
- ):
34
- def decorator(wp): # type: ignore[no-untyped-def]
33
+ ) -> Any:
34
+ def decorator(wp: PipeConverter[T, Any]) -> PipeConverter[T, Any]:
35
35
  step = PipeStep(wp, dispatcher)
36
36
  self.__steps.append(step)
37
37
  return wp
@@ -5,6 +5,8 @@ import injection
5
5
 
6
6
  from cq._core.dispatcher.bus import Bus, SimpleBus, SubscriberDecorator, TaskBus
7
7
  from cq._core.dto import DTO
8
+ from cq._core.scope import CQScope
9
+ from cq.middlewares.scope import InjectionScopeMiddleware
8
10
 
9
11
 
10
12
  class Message(DTO, ABC):
@@ -34,21 +36,38 @@ command_handler: SubscriberDecorator[Command, Any] = SubscriberDecorator(Command
34
36
  event_handler: SubscriberDecorator[Event, None] = SubscriberDecorator(EventBus)
35
37
  query_handler: SubscriberDecorator[Query, Any] = SubscriberDecorator(QueryBus)
36
38
 
37
- injection.set_constant(SimpleBus(), CommandBus, alias=True)
38
- injection.set_constant(TaskBus(), EventBus, alias=True)
39
- injection.set_constant(SimpleBus(), QueryBus, alias=True)
40
-
41
39
 
42
40
  @injection.inject
43
41
  def get_command_bus[T](bus: CommandBus[T] = NotImplemented, /) -> CommandBus[T]:
44
42
  return bus
45
43
 
46
44
 
45
+ def new_command_bus[T]() -> CommandBus[T]:
46
+ bus: CommandBus[T] = SimpleBus()
47
+ bus.add_middlewares(
48
+ InjectionScopeMiddleware(CQScope.ON_COMMAND),
49
+ )
50
+ return bus
51
+
52
+
47
53
  @injection.inject
48
54
  def get_event_bus(bus: EventBus = NotImplemented, /) -> EventBus:
49
55
  return bus
50
56
 
51
57
 
58
+ def new_event_bus() -> EventBus:
59
+ return TaskBus()
60
+
61
+
52
62
  @injection.inject
53
63
  def get_query_bus[T](bus: QueryBus[T] = NotImplemented, /) -> QueryBus[T]:
54
64
  return bus
65
+
66
+
67
+ def new_query_bus[T]() -> QueryBus[T]:
68
+ return SimpleBus()
69
+
70
+
71
+ injection.set_constant(new_command_bus(), CommandBus, alias=True)
72
+ injection.set_constant(new_event_bus(), EventBus, alias=True)
73
+ injection.set_constant(new_query_bus(), QueryBus, alias=True)
@@ -0,0 +1,43 @@
1
+ from abc import abstractmethod
2
+ from collections.abc import AsyncIterator
3
+ from dataclasses import dataclass, field
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ import anyio
7
+ import injection
8
+
9
+ from cq._core.message import Event, EventBus
10
+ from cq._core.scope import CQScope
11
+
12
+
13
+ @runtime_checkable
14
+ class RelatedEvents(Protocol):
15
+ __slots__ = ()
16
+
17
+ @abstractmethod
18
+ def add(self, *events: Event) -> None:
19
+ raise NotImplementedError
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class _RelatedEvents(RelatedEvents):
24
+ items: list[Event] = field(default_factory=list)
25
+
26
+ def add(self, *events: Event) -> None:
27
+ self.items.extend(events)
28
+
29
+
30
+ @injection.scoped(CQScope.ON_COMMAND)
31
+ async def _related_events_recipe(event_bus: EventBus) -> AsyncIterator[RelatedEvents]:
32
+ yield (instance := _RelatedEvents())
33
+ events = instance.items
34
+
35
+ if not events:
36
+ return
37
+
38
+ async with anyio.create_task_group() as task_group:
39
+ for event in events:
40
+ task_group.start_soon(event_bus.dispatch, event)
41
+
42
+
43
+ del _related_events_recipe
@@ -0,0 +1,5 @@
1
+ from enum import StrEnum, auto
2
+
3
+
4
+ class CQScope(StrEnum):
5
+ ON_COMMAND = auto()
@@ -1,6 +1,8 @@
1
- import asyncio
1
+ from collections.abc import Iterable
2
2
  from typing import Any
3
3
 
4
+ import anyio
5
+
4
6
  from cq import MiddlewareResult
5
7
 
6
8
  __all__ = ("RetryMiddleware",)
@@ -13,10 +15,10 @@ class RetryMiddleware:
13
15
  self,
14
16
  retry: int,
15
17
  delay: float = 0,
16
- exceptions: tuple[type[BaseException], ...] = (Exception,),
18
+ exceptions: Iterable[type[BaseException]] = (Exception,),
17
19
  ) -> None:
18
20
  self.__delay = delay
19
- self.__exceptions = exceptions
21
+ self.__exceptions = tuple(exceptions)
20
22
  self.__retry = retry
21
23
 
22
24
  async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
@@ -33,4 +35,4 @@ class RetryMiddleware:
33
35
  else:
34
36
  break
35
37
 
36
- await asyncio.sleep(self.__delay)
38
+ await anyio.sleep(self.__delay)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from injection import adefine_scope
6
+
7
+ if TYPE_CHECKING: # pragma: no cover
8
+ from cq import MiddlewareResult
9
+
10
+ __all__ = ("InjectionScopeMiddleware",)
11
+
12
+
13
+ class InjectionScopeMiddleware:
14
+ __slots__ = ("__scope_name",)
15
+
16
+ def __init__(self, scope_name: str) -> None:
17
+ self.__scope_name = scope_name
18
+
19
+ async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
20
+ async with adefine_scope(self.__scope_name):
21
+ yield
@@ -1,11 +1,30 @@
1
- [tool.poetry]
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [dependency-groups]
6
+ dev = [
7
+ "hatch",
8
+ "mypy",
9
+ "ruff",
10
+ ]
11
+ example = [
12
+ "fastapi",
13
+ ]
14
+ test = [
15
+ "pytest",
16
+ "pytest-asyncio",
17
+ "pytest-cov",
18
+ ]
19
+
20
+ [project]
2
21
  name = "python-cq"
3
- version = "0.3.0"
22
+ version = "0.4.0"
4
23
  description = "Lightweight CQRS library."
5
- license = "MIT"
6
- authors = ["remimd"]
24
+ license = { text = "MIT" }
7
25
  readme = "README.md"
8
- repository = "https://github.com/100nm/python-cq"
26
+ requires-python = ">=3.12, <4"
27
+ authors = [{ name = "remimd" }]
9
28
  keywords = ["cqrs"]
10
29
  classifiers = [
11
30
  "Development Status :: 4 - Beta",
@@ -20,24 +39,14 @@ classifiers = [
20
39
  "Natural Language :: English",
21
40
  "Typing :: Typed",
22
41
  ]
23
- packages = [{ include = "cq" }]
24
-
25
- [tool.poetry.dependencies]
26
- python = ">=3.12, <4"
27
- pydantic = ">=2, <3"
28
- python-injection = "*"
29
-
30
- [tool.poetry.group.dev.dependencies]
31
- mypy = "*"
32
- ruff = "*"
33
-
34
- [tool.poetry.group.example.dependencies]
35
- fastapi = "*"
42
+ dependencies = [
43
+ "anyio",
44
+ "pydantic (>=2, <3)",
45
+ "python-injection",
46
+ ]
36
47
 
37
- [tool.poetry.group.test.dependencies]
38
- pytest = "*"
39
- pytest-asyncio = "*"
40
- pytest-cov = "*"
48
+ [project.urls]
49
+ Repository = "https://github.com/100nm/python-cq"
41
50
 
42
51
  [tool.coverage.report]
43
52
  exclude_lines = [
@@ -46,6 +55,15 @@ exclude_lines = [
46
55
  "raise NotImplementedError",
47
56
  ]
48
57
 
58
+ [tool.hatch.build]
59
+ skip-excluded-dirs = true
60
+
61
+ [tool.hatch.build.targets.sdist]
62
+ include = ["cq"]
63
+
64
+ [tool.hatch.build.targets.wheel]
65
+ packages = ["cq"]
66
+
49
67
  [tool.mypy]
50
68
  check_untyped_defs = true
51
69
  disallow_any_generics = true
@@ -70,7 +88,6 @@ asyncio_mode = "auto"
70
88
  testpaths = "**/tests/"
71
89
 
72
90
  [tool.ruff]
73
- target-version = "py312"
74
91
  line-length = 88
75
92
  indent-width = 4
76
93
 
@@ -84,6 +101,6 @@ line-ending = "auto"
84
101
  extend-select = ["F", "I", "N"]
85
102
  fixable = ["ALL"]
86
103
 
87
- [build-system]
88
- requires = ["poetry-core"]
89
- build-backend = "poetry.core.masonry.api"
104
+ [tool.uv]
105
+ default-groups = ["dev", "test"]
106
+ package = true
File without changes
File without changes
File without changes
File without changes