python-cq 0.2.2__tar.gz → 0.3.1__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,25 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.2.2
3
+ Version: 0.3.1
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: pydantic<3,>=2
25
22
  Requires-Dist: python-injection
26
- Project-URL: Repository, https://github.com/100nm/python-cq
27
23
  Description-Content-Type: text/markdown
28
24
 
29
25
  # python-cq
@@ -51,4 +47,3 @@ pip install python-cq
51
47
  * [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
52
48
  * [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
53
49
  * [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
54
-
@@ -12,9 +12,9 @@ from ._core.message import (
12
12
  QueryBus,
13
13
  command_handler,
14
14
  event_handler,
15
- find_command_bus,
16
- find_event_bus,
17
- find_query_bus,
15
+ get_command_bus,
16
+ get_event_bus,
17
+ get_query_bus,
18
18
  query_handler,
19
19
  )
20
20
  from ._core.middleware import Middleware, MiddlewareResult
@@ -35,8 +35,8 @@ __all__ = (
35
35
  "QueryBus",
36
36
  "command_handler",
37
37
  "event_handler",
38
- "find_command_bus",
39
- "find_event_bus",
40
- "find_query_bus",
38
+ "get_command_bus",
39
+ "get_event_bus",
40
+ "get_query_bus",
41
41
  "query_handler",
42
42
  )
@@ -1,18 +1,20 @@
1
1
  import asyncio
2
- from abc import abstractmethod
2
+ from abc import ABC, abstractmethod
3
3
  from collections import defaultdict
4
- from collections.abc import Callable
4
+ from collections.abc import Awaitable, Callable
5
5
  from dataclasses import dataclass, field
6
6
  from inspect import isclass
7
7
  from types import GenericAlias
8
- from typing import Protocol, Self, TypeAliasType, runtime_checkable
8
+ from typing import Any, Protocol, Self, TypeAliasType, runtime_checkable
9
9
 
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
+
17
+ type Listener[T] = Callable[[T], Awaitable[Any]]
16
18
 
17
19
  type BusType[I, O] = type[Bus[I, O]]
18
20
 
@@ -34,32 +36,56 @@ class Bus[I, O](Dispatcher[I, O], Protocol):
34
36
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
35
37
  raise NotImplementedError
36
38
 
39
+ @abstractmethod
40
+ def add_listeners(self, *listeners: Listener[I]) -> Self:
41
+ raise NotImplementedError
42
+
37
43
 
38
44
  @dataclass(eq=False, frozen=True, slots=True)
39
45
  class SubscriberDecorator[I, O]:
40
46
  bus_type: BusType[I, O] | TypeAliasType | GenericAlias
41
47
  injection_module: injection.Module = field(default_factory=injection.mod)
42
48
 
43
- def __call__(self, first_input_type: type[I], /, *input_types: type[I]): # type: ignore[no-untyped-def]
44
- 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]]:
45
51
  if not isclass(wrapped) or not issubclass(wrapped, Handler):
46
52
  raise TypeError(f"`{wrapped}` isn't a valid handler.")
47
53
 
48
- bus = self.__find_bus()
49
- factory = self.injection_module.make_injected_function(wrapped)
54
+ bus = self.injection_module.find_instance(self.bus_type)
55
+ lazy_instance = self.injection_module.aget_lazy_instance(
56
+ wrapped,
57
+ default=NotImplemented,
58
+ )
59
+
60
+ async def getter() -> Handler[[I], O]:
61
+ return await lazy_instance
50
62
 
51
63
  for input_type in (first_input_type, *input_types):
52
- bus.subscribe(input_type, factory)
64
+ bus.subscribe(input_type, getter)
53
65
 
54
- return wrapped
66
+ return self.injection_module.injectable(wrapped)
55
67
 
56
68
  return decorator
57
69
 
58
- def __find_bus(self) -> Bus[I, O]:
59
- return self.injection_module.find_instance(self.bus_type)
60
70
 
71
+ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
72
+ __slots__ = ("__listeners",)
73
+
74
+ __listeners: list[Listener[I]]
75
+
76
+ def __init__(self) -> None:
77
+ super().__init__()
78
+ self.__listeners = []
79
+
80
+ def add_listeners(self, *listeners: Listener[I]) -> Self:
81
+ self.__listeners.extend(listeners)
82
+ return self
61
83
 
62
- class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
84
+ async def _trigger_listeners(self, input_value: I, /) -> None:
85
+ await asyncio.gather(*(listener(input_value) for listener in self.__listeners))
86
+
87
+
88
+ class SimpleBus[I, O](BaseBus[I, O]):
63
89
  __slots__ = ("__handlers",)
64
90
 
65
91
  __handlers: dict[type[I], HandlerFactory[[I], O]]
@@ -69,6 +95,7 @@ class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
69
95
  self.__handlers = {}
70
96
 
71
97
  async def dispatch(self, input_value: I, /) -> O:
98
+ await self._trigger_listeners(input_value)
72
99
  input_type = type(input_value)
73
100
 
74
101
  try:
@@ -76,10 +103,8 @@ class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
76
103
  except KeyError:
77
104
  return NotImplemented
78
105
 
79
- return await self._invoke_with_middlewares(
80
- handler_factory().handle,
81
- input_value,
82
- )
106
+ handler = await handler_factory()
107
+ return await self._invoke_with_middlewares(handler.handle, input_value)
83
108
 
84
109
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
85
110
  if input_type in self.__handlers:
@@ -91,7 +116,7 @@ class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
91
116
  return self
92
117
 
93
118
 
94
- class TaskBus[I](BaseDispatcher[I, None], Bus[I, None]):
119
+ class TaskBus[I](BaseBus[I, None]):
95
120
  __slots__ = ("__handlers",)
96
121
 
97
122
  __handlers: dict[type[I], list[HandlerFactory[[I], None]]]
@@ -101,19 +126,20 @@ class TaskBus[I](BaseDispatcher[I, None], Bus[I, None]):
101
126
  self.__handlers = defaultdict(list)
102
127
 
103
128
  async def dispatch(self, input_value: I, /) -> None:
129
+ await self._trigger_listeners(input_value)
104
130
  handler_factories = self.__handlers.get(type(input_value))
105
131
 
106
132
  if not handler_factories:
107
133
  return
108
134
 
109
135
  await asyncio.gather(
110
- *(
136
+ *[
111
137
  self._invoke_with_middlewares(
112
- handler_factory().handle,
138
+ (await handler_factory()).handle,
113
139
  input_value,
114
140
  )
115
141
  for handler_factory in handler_factories
116
- )
142
+ ]
117
143
  )
118
144
 
119
145
  def subscribe(
@@ -39,13 +39,16 @@ injection.set_constant(TaskBus(), EventBus, alias=True)
39
39
  injection.set_constant(SimpleBus(), QueryBus, alias=True)
40
40
 
41
41
 
42
- def find_command_bus[T]() -> CommandBus[T]:
43
- return injection.find_instance(CommandBus)
42
+ @injection.inject
43
+ def get_command_bus[T](bus: CommandBus[T] = NotImplemented, /) -> CommandBus[T]:
44
+ return bus
44
45
 
45
46
 
46
- def find_event_bus() -> EventBus:
47
- return injection.find_instance(EventBus)
47
+ @injection.inject
48
+ def get_event_bus(bus: EventBus = NotImplemented, /) -> EventBus:
49
+ return bus
48
50
 
49
51
 
50
- def find_query_bus[T]() -> QueryBus[T]:
51
- return injection.find_instance(QueryBus)
52
+ @injection.inject
53
+ def get_query_bus[T](bus: QueryBus[T] = NotImplemented, /) -> QueryBus[T]:
54
+ return bus
@@ -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.2.2"
22
+ version = "0.3.1"
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,13 @@ 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
+ "pydantic (>=2, <3)",
44
+ "python-injection",
45
+ ]
36
46
 
37
- [tool.poetry.group.test.dependencies]
38
- pytest = "*"
39
- pytest-asyncio = "*"
40
- pytest-cov = "*"
47
+ [project.urls]
48
+ Repository = "https://github.com/100nm/python-cq"
41
49
 
42
50
  [tool.coverage.report]
43
51
  exclude_lines = [
@@ -46,6 +54,15 @@ exclude_lines = [
46
54
  "raise NotImplementedError",
47
55
  ]
48
56
 
57
+ [tool.hatch.build]
58
+ skip-excluded-dirs = true
59
+
60
+ [tool.hatch.build.targets.sdist]
61
+ include = ["cq"]
62
+
63
+ [tool.hatch.build.targets.wheel]
64
+ packages = ["cq"]
65
+
49
66
  [tool.mypy]
50
67
  check_untyped_defs = true
51
68
  disallow_any_generics = true
@@ -70,7 +87,6 @@ asyncio_mode = "auto"
70
87
  testpaths = "**/tests/"
71
88
 
72
89
  [tool.ruff]
73
- target-version = "py312"
74
90
  line-length = 88
75
91
  indent-width = 4
76
92
 
@@ -84,6 +100,6 @@ line-ending = "auto"
84
100
  extend-select = ["F", "I", "N"]
85
101
  fixable = ["ALL"]
86
102
 
87
- [build-system]
88
- requires = ["poetry-core"]
89
- build-backend = "poetry.core.masonry.api"
103
+ [tool.uv]
104
+ default-groups = ["dev", "test"]
105
+ package = true
File without changes
File without changes
File without changes
File without changes