django-queuebie 0.2.0__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.
Files changed (75) hide show
  1. django_queuebie-0.3.1/CHANGES.md +13 -0
  2. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/PKG-INFO +1 -1
  3. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/__init__.py +1 -1
  4. django_queuebie-0.3.1/queuebie/runner.py +63 -0
  5. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/handlers/commands/testapp.py +24 -6
  6. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/messages/commands/my_commands.py +10 -0
  7. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_registry.py +15 -1
  8. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_runner.py +11 -11
  9. django_queuebie-0.2.0/CHANGES.md +0 -7
  10. django_queuebie-0.2.0/queuebie/runner.py +0 -64
  11. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.ambient-package-update/metadata.py +0 -0
  12. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.ambient-package-update/templates/snippets/content.tpl +0 -0
  13. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.ambient-package-update/templates/snippets/tagline.tpl +0 -0
  14. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.editorconfig +0 -0
  15. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.github/workflows/ci.yml +0 -0
  16. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.gitignore +0 -0
  17. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.pre-commit-config.yaml +0 -0
  18. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/.readthedocs.yaml +0 -0
  19. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/LICENSE +0 -0
  20. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/LICENSE.md +0 -0
  21. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/MANIFEST.in +0 -0
  22. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/README.md +0 -0
  23. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/SECURITY.md +0 -0
  24. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/Makefile +0 -0
  25. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/conf.py +0 -0
  26. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/features/changelog.rst +0 -0
  27. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/features/getting-started.md +0 -0
  28. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/features/introduction.md +0 -0
  29. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/features/motivation.md +0 -0
  30. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/features/settings.md +0 -0
  31. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/features/setup.md +0 -0
  32. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/index.rst +0 -0
  33. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/docs/make.bat +0 -0
  34. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/manage.py +0 -0
  35. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/pyproject.toml +0 -0
  36. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/apps.py +0 -0
  37. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/database_blocker.py +0 -0
  38. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/exceptions.py +0 -0
  39. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/logger.py +0 -0
  40. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/management/__init__.py +0 -0
  41. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/management/commands/__init__.py +0 -0
  42. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/management/commands/clear_queuebie_registry.py +0 -0
  43. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/messages.py +0 -0
  44. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/registry.py +0 -0
  45. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/settings.py +0 -0
  46. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/queuebie/utils.py +0 -0
  47. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/scripts/unix/install_requirements.sh +0 -0
  48. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/scripts/unix/publish_to_pypi.sh +0 -0
  49. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/scripts/windows/install_requirements.ps1 +0 -0
  50. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/scripts/windows/publish_to_pypi.ps1 +0 -0
  51. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/settings.py +0 -0
  52. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/__init__.py +0 -0
  53. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/handlers/__init__.py +0 -0
  54. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/handlers/commands/__init__.py +0 -0
  55. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/handlers/events/__init__.py +0 -0
  56. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/handlers/events/testapp.py +0 -0
  57. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/messages/__init__.py +0 -0
  58. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/messages/commands/__init__.py +0 -0
  59. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/messages/commands/other_commands.py +0 -0
  60. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/messages/events/__init__.py +0 -0
  61. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/messages/events/my_events.py +0 -0
  62. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/testapp/urls.py +0 -0
  63. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/__init__.py +0 -0
  64. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/helpers/__init__.py +0 -0
  65. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/helpers/commands.py +0 -0
  66. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/helpers/events.py +0 -0
  67. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/management_commands/__init__.py +0 -0
  68. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/management_commands/test_clear_queuebie_registry.py +0 -0
  69. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_apps.py +0 -0
  70. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_database_blocker.py +0 -0
  71. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_exceptions.py +0 -0
  72. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_logger.py +0 -0
  73. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_messages.py +0 -0
  74. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_settings.py +0 -0
  75. {django_queuebie-0.2.0 → django_queuebie-0.3.1}/tests/test_utils.py +0 -0
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ * *0.3.1* (2025-03-19)
4
+ * Added a paranoid-ish test to check that the import logic isn't breaking any testing functionality
5
+
6
+ * *0.3.0* (2025-03-19)
7
+ * The whole queue iteration now is wrapped in a transaction atomic
8
+
9
+ * *0.2.0* (2025-03-17)
10
+ * Extend strict mode to prohibit event (!) handlers to talk to the database
11
+
12
+ * *0.1.0* (2025-01-29)
13
+ * Project init
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-queuebie
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Simple message queue for commands and events (CQRS)
5
5
  Author-email: Ambient Digital <hello@ambient.digital>
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  """Simple message queue for commands and events (CQRS)"""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.1"
4
4
 
5
5
  from queuebie.registry import MessageRegistry
6
6
 
@@ -0,0 +1,63 @@
1
+ import importlib
2
+ from contextlib import nullcontext
3
+
4
+ from django.db import transaction
5
+
6
+ from queuebie import message_registry
7
+ from queuebie.database_blocker import BlockDatabaseAccess
8
+ from queuebie.exceptions import InvalidMessageTypeError
9
+ from queuebie.logger import get_logger
10
+ from queuebie.messages import Command, Event, Message
11
+ from queuebie.settings import get_queuebie_strict_mode
12
+
13
+
14
+ def handle_message(messages: Message | list[Message]) -> None:
15
+ queue: list[Message] = messages if isinstance(messages, list) else [messages]
16
+
17
+ for message in queue:
18
+ if not isinstance(message, (Command, Event)):
19
+ raise InvalidMessageTypeError(class_name=message.__class__.__name__)
20
+
21
+ # Run auto-registry
22
+ message_registry.autodiscover()
23
+
24
+ with transaction.atomic():
25
+ while queue:
26
+ message = queue.pop(0)
27
+ if isinstance(message, Command):
28
+ handler_list = message_registry.command_dict.get(message.module_path(), [])
29
+ block_db_access = False
30
+ else:
31
+ handler_list = message_registry.event_dict.get(message.module_path(), [])
32
+ block_db_access = True if get_queuebie_strict_mode() else False
33
+
34
+ new_messages = _process_message(handler_list=handler_list, message=message, block_db_access=block_db_access)
35
+ queue.extend(new_messages)
36
+
37
+
38
+ def _process_message(*, handler_list: list, message: [Command, Event], block_db_access: bool) -> list[Message]:
39
+ """
40
+ Handler to process messages of type "Command"
41
+ """
42
+ logger = get_logger()
43
+ messages = []
44
+
45
+ for handler in handler_list:
46
+ try:
47
+ logger.debug(
48
+ f"Handling command '{message.module_path()}' ({message.uuid}) with handler '{handler['name']}'."
49
+ )
50
+ module = importlib.import_module(handler["module"])
51
+ handler_function = getattr(module, handler["name"])
52
+ with BlockDatabaseAccess() if block_db_access else nullcontext():
53
+ handler_messages = handler_function(context=message) or []
54
+ handler_messages = handler_messages if isinstance(handler_messages, list) else [handler_messages]
55
+ if len(handler_messages) > 0:
56
+ messages.extend(handler_messages)
57
+ uuid_list = [f"{m!s}" for m in handler_messages]
58
+ logger.debug(f"New messages: {uuid_list!s}")
59
+ except Exception as e:
60
+ logger.debug(f"Exception handling command {message.module_path()}: {e!s}")
61
+ raise e from e
62
+
63
+ return messages
@@ -5,7 +5,13 @@ from django.contrib.auth.models import User
5
5
  from queuebie import message_registry
6
6
  from queuebie.logger import get_logger
7
7
  from queuebie.messages import Event
8
- from testapp.messages.commands.my_commands import CriticalCommand, DoSomething, PersistSomething
8
+ from testapp.messages.commands.my_commands import (
9
+ CreateUser,
10
+ CriticalCommand,
11
+ DoSomething,
12
+ PersistSomething,
13
+ RaiseRuntimeError,
14
+ )
9
15
  from testapp.messages.events.my_events import SomethingHappened, SomethingHappenedThatWantsToBePersisted
10
16
 
11
17
 
@@ -17,7 +23,7 @@ def handle_my_command(*, context: DoSomething) -> list[Event] | Event:
17
23
 
18
24
 
19
25
  @message_registry.register_command(command=PersistSomething)
20
- def handle_something_that_needs_persistance(*, context: PersistSomething) -> Event:
26
+ def handle_something_that_needs_persistence(*, context: PersistSomething) -> Event:
21
27
  User.objects.create(username="testuser" + str(randint(1, 100)))
22
28
 
23
29
  return SomethingHappenedThatWantsToBePersisted(any_var=1)
@@ -29,9 +35,21 @@ def handle_critical_command(*, context: CriticalCommand) -> None:
29
35
  raise RuntimeError("Handler is broken.") # noqa: TRY003
30
36
 
31
37
 
32
- def create_user(*args, **kwargs):
33
- return User.objects.create_user(username="username")
38
+ @message_registry.register_command(command=CreateUser)
39
+ def create_user(context: CreateUser):
40
+ User.objects.create_user(username=context.username)
34
41
 
35
42
 
36
- def raise_exception(*args, **kwargs):
37
- raise RuntimeError("Something is broken.") # noqa: TRY003
43
+ @message_registry.register_command(command=RaiseRuntimeError)
44
+ def raise_exception(context: RaiseRuntimeError):
45
+ raise RuntimeError(context.error_msg)
46
+
47
+
48
+ class MyClass:
49
+ """
50
+ This is a test class since we observed the behaviour that in a similar setup, the "forced"
51
+ import was breaking mocking.
52
+ """
53
+
54
+ def process(self):
55
+ return 42
@@ -21,3 +21,13 @@ class SameNameCommand(Command):
21
21
  @dataclass(kw_only=True)
22
22
  class PersistSomething(Command):
23
23
  any_var: str
24
+
25
+
26
+ @dataclass(kw_only=True)
27
+ class CreateUser(Command):
28
+ username: str
29
+
30
+
31
+ @dataclass(kw_only=True)
32
+ class RaiseRuntimeError(Command):
33
+ error_msg: str
@@ -9,6 +9,7 @@ from django.test import override_settings
9
9
  from queuebie import MessageRegistry
10
10
  from queuebie.exceptions import RegisterOutOfScopeCommandError
11
11
  from queuebie.settings import get_queuebie_cache_key
12
+ from testapp.handlers.commands.testapp import MyClass
12
13
  from testapp.messages.commands.my_commands import CriticalCommand, DoSomething
13
14
  from testapp.messages.events.my_events import (
14
15
  SomethingHappened,
@@ -124,7 +125,7 @@ def test_message_autodiscover_regular():
124
125
  message_registry.autodiscover()
125
126
 
126
127
  # Assert one command registered
127
- assert len(message_registry.command_dict) == 3 # noqa: PLR2004
128
+ assert len(message_registry.command_dict) == 5 # noqa: PLR2004
128
129
  assert DoSomething.module_path() in message_registry.command_dict.keys()
129
130
  assert CriticalCommand.module_path() in message_registry.command_dict.keys()
130
131
 
@@ -191,3 +192,16 @@ def test_message_autodiscover_load_handlers_from_cache_dummy_cache(*args):
191
192
 
192
193
  assert len(commands) == 0
193
194
  assert len(events) == 0
195
+
196
+
197
+ @mock.patch.object(MyClass, "process")
198
+ def test_registry_forced_import_doesnt_break_mocking(mocked_process):
199
+ """
200
+ This is a test for ensuring that the "forced" import isn't breaking mocking features.
201
+ """
202
+
203
+ def my_func():
204
+ return MyClass().process()
205
+
206
+ my_func()
207
+ mocked_process.assert_called_once()
@@ -8,8 +8,15 @@ from django.test import override_settings
8
8
  from queuebie import MessageRegistry
9
9
  from queuebie.database_blocker import DatabaseAccessDeniedError
10
10
  from queuebie.exceptions import InvalidMessageTypeError
11
- from queuebie.runner import _process_message, handle_message
12
- from testapp.messages.commands.my_commands import CriticalCommand, DoSomething, PersistSomething, SameNameCommand
11
+ from queuebie.runner import handle_message
12
+ from testapp.messages.commands.my_commands import (
13
+ CreateUser,
14
+ CriticalCommand,
15
+ DoSomething,
16
+ PersistSomething,
17
+ RaiseRuntimeError,
18
+ SameNameCommand,
19
+ )
13
20
  from testapp.messages.events.my_events import (
14
21
  SomethingHappened,
15
22
  SomethingHappenedThatWantsToBePersistedViaEvent,
@@ -125,15 +132,8 @@ def test_handle_message_other_command_with_same_name(mocked_handle_command, *arg
125
132
 
126
133
  @pytest.mark.django_db
127
134
  @mock.patch("queuebie.registry.get_queuebie_strict_mode", return_value=False)
128
- def test_process_message_atomic_works(mocked_handle_command, *args):
129
- handler_list = [
130
- {"module": "testapp.handlers.commands.testapp", "name": "create_user"},
131
- {"module": "testapp.handlers.commands.testapp", "name": "raise_exception"},
132
- ]
133
-
134
- message = DoSomething(my_var=1)
135
-
135
+ def test_handle_message_atomic_works(*args):
136
136
  with pytest.raises(RuntimeError, match="Something is broken."):
137
- _process_message(handler_list=handler_list, message=message, block_db_access=False)
137
+ handle_message([CreateUser(username="username"), RaiseRuntimeError(error_msg="Something is broken.")])
138
138
 
139
139
  assert User.objects.filter(username="username").exists() is False
@@ -1,7 +0,0 @@
1
- # Changelog
2
-
3
- * *0.2.0* (2025-03-17)
4
- * Extend strict mode to prohibit event (!) handlers to talk to the database
5
-
6
- * *0.1.0* (2025-01-29)
7
- * Project init
@@ -1,64 +0,0 @@
1
- import importlib
2
- from contextlib import nullcontext
3
-
4
- from django.db import transaction
5
-
6
- from queuebie import message_registry
7
- from queuebie.database_blocker import BlockDatabaseAccess
8
- from queuebie.exceptions import InvalidMessageTypeError
9
- from queuebie.logger import get_logger
10
- from queuebie.messages import Command, Event, Message
11
- from queuebie.settings import get_queuebie_strict_mode
12
-
13
-
14
- def handle_message(messages: Message | list[Message]) -> None:
15
- queue: list[Message] = messages if isinstance(messages, list) else [messages]
16
-
17
- for message in queue:
18
- if not isinstance(message, (Command, Event)):
19
- raise InvalidMessageTypeError(class_name=message.__class__.__name__)
20
-
21
- # Run auto-registry
22
- message_registry.autodiscover()
23
-
24
- while queue:
25
- message = queue.pop(0)
26
- if isinstance(message, Command):
27
- handler_list = message_registry.command_dict.get(message.module_path(), [])
28
- block_db_access = False
29
- else:
30
- handler_list = message_registry.event_dict.get(message.module_path(), [])
31
- block_db_access = True if get_queuebie_strict_mode() else False
32
-
33
- new_messages = _process_message(handler_list=handler_list, message=message, block_db_access=block_db_access)
34
- queue.extend(new_messages)
35
-
36
-
37
- def _process_message(*, handler_list: list, message: [Command, Event], block_db_access: bool) -> list[Message]:
38
- """
39
- Handler to process messages of type "Command"
40
- """
41
- logger = get_logger()
42
- messages = []
43
-
44
- # TODO: should the whole chain be atomic and not just the handler?
45
- with transaction.atomic():
46
- for handler in handler_list:
47
- try:
48
- logger.debug(
49
- f"Handling command '{message.module_path()}' ({message.uuid}) with handler '{handler['name']}'."
50
- )
51
- module = importlib.import_module(handler["module"])
52
- handler_function = getattr(module, handler["name"])
53
- with BlockDatabaseAccess() if block_db_access else nullcontext():
54
- handler_messages = handler_function(context=message) or []
55
- handler_messages = handler_messages if isinstance(handler_messages, list) else [handler_messages]
56
- if len(handler_messages) > 0:
57
- messages.extend(handler_messages)
58
- uuid_list = [f"{m!s}" for m in handler_messages]
59
- logger.debug(f"New messages: {uuid_list!s}")
60
- except Exception as e:
61
- logger.debug(f"Exception handling command {message.module_path()}: {e!s}")
62
- raise e from e
63
-
64
- return messages
File without changes