django-queuebie 0.1.0__tar.gz → 0.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 (78) hide show
  1. django_queuebie-0.2.0/.ambient-package-update/templates/snippets/content.tpl +53 -0
  2. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.gitignore +0 -2
  3. django_queuebie-0.2.0/CHANGES.md +7 -0
  4. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/PKG-INFO +47 -1
  5. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/README.md +46 -0
  6. django_queuebie-0.2.0/docs/features/motivation.md +30 -0
  7. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/features/settings.md +2 -2
  8. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/pyproject.toml +3 -6
  9. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/__init__.py +1 -1
  10. django_queuebie-0.2.0/queuebie/database_blocker.py +25 -0
  11. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/runner.py +10 -4
  12. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/handlers/commands/testapp.py +11 -2
  13. django_queuebie-0.2.0/testapp/handlers/events/testapp.py +20 -0
  14. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/messages/commands/my_commands.py +5 -0
  15. django_queuebie-0.2.0/testapp/messages/events/my_events.py +18 -0
  16. django_queuebie-0.2.0/tests/test_database_blocker.py +17 -0
  17. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_registry.py +9 -5
  18. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_runner.py +25 -3
  19. django_queuebie-0.1.0/.ambient-package-update/templates/snippets/content.tpl +0 -7
  20. django_queuebie-0.1.0/CHANGES.md +0 -4
  21. django_queuebie-0.1.0/docs/features/motivation.md +0 -14
  22. django_queuebie-0.1.0/testapp/handlers/events/testapp.py +0 -11
  23. django_queuebie-0.1.0/testapp/messages/events/my_events.py +0 -8
  24. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.ambient-package-update/metadata.py +0 -0
  25. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.ambient-package-update/templates/snippets/tagline.tpl +0 -0
  26. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.editorconfig +0 -0
  27. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.github/workflows/ci.yml +0 -0
  28. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.pre-commit-config.yaml +0 -0
  29. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/.readthedocs.yaml +0 -0
  30. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/LICENSE +0 -0
  31. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/LICENSE.md +0 -0
  32. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/MANIFEST.in +0 -0
  33. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/SECURITY.md +0 -0
  34. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/Makefile +0 -0
  35. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/conf.py +0 -0
  36. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/features/changelog.rst +0 -0
  37. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/features/getting-started.md +0 -0
  38. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/features/introduction.md +0 -0
  39. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/features/setup.md +0 -0
  40. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/index.rst +0 -0
  41. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/docs/make.bat +0 -0
  42. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/manage.py +0 -0
  43. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/apps.py +0 -0
  44. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/exceptions.py +0 -0
  45. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/logger.py +0 -0
  46. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/management/__init__.py +0 -0
  47. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/management/commands/__init__.py +0 -0
  48. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/management/commands/clear_queuebie_registry.py +0 -0
  49. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/messages.py +0 -0
  50. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/registry.py +0 -0
  51. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/settings.py +0 -0
  52. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/queuebie/utils.py +0 -0
  53. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/scripts/unix/install_requirements.sh +0 -0
  54. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/scripts/unix/publish_to_pypi.sh +0 -0
  55. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/scripts/windows/install_requirements.ps1 +0 -0
  56. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/scripts/windows/publish_to_pypi.ps1 +0 -0
  57. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/settings.py +0 -0
  58. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/__init__.py +0 -0
  59. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/handlers/__init__.py +0 -0
  60. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/handlers/commands/__init__.py +0 -0
  61. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/handlers/events/__init__.py +0 -0
  62. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/messages/__init__.py +0 -0
  63. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/messages/commands/__init__.py +0 -0
  64. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/messages/commands/other_commands.py +0 -0
  65. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/messages/events/__init__.py +0 -0
  66. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/testapp/urls.py +0 -0
  67. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/__init__.py +0 -0
  68. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/helpers/__init__.py +0 -0
  69. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/helpers/commands.py +0 -0
  70. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/helpers/events.py +0 -0
  71. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/management_commands/__init__.py +0 -0
  72. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/management_commands/test_clear_queuebie_registry.py +0 -0
  73. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_apps.py +0 -0
  74. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_exceptions.py +0 -0
  75. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_logger.py +0 -0
  76. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_messages.py +0 -0
  77. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_settings.py +0 -0
  78. {django_queuebie-0.1.0 → django_queuebie-0.2.0}/tests/test_utils.py +0 -0
@@ -0,0 +1,53 @@
1
+ ## Features
2
+
3
+ * Split up your business logic in commands and events
4
+ * Commands are imperatives telling your system what to do, events reflect that something has happened
5
+ * Register light-weight functions via a decorator to listen to your commands and events
6
+ * Message handlers receive the context of the message (command or event), providing an explicit API
7
+ * No magic, no side effects since the queue works synchronously
8
+
9
+ ```python
10
+ import dataclasses
11
+
12
+ from queuebie.runner import handle_message
13
+ from queuebie.messages import Command, Event
14
+ from queuebie import message_registry
15
+
16
+
17
+ # Example command
18
+ @dataclasses.dataclass(kw_only=True)
19
+ class BuyProduct(Command):
20
+ product_id: int
21
+ customer_id: int
22
+ price: float
23
+ currency: str
24
+
25
+
26
+ # Example event
27
+ @dataclasses.dataclass(kw_only=True)
28
+ class ProductBought(Event):
29
+ product_id: int
30
+ customer_id: int
31
+
32
+
33
+ # Example handler
34
+ @message_registry.register_command(BuyProduct)
35
+ def handle_buy_product(context: BuyProduct) -> Event:
36
+ # Here lives your business logic
37
+
38
+ return ProductBought(
39
+ product_id=context.product_id,
40
+ customer_id=context.customer_id,
41
+ )
42
+
43
+
44
+ # Start queue and process messages
45
+ handle_message(
46
+ BuyProduct(
47
+ product_id=product.id,
48
+ customer_id=customer.id,
49
+ price=12.99,
50
+ currency="EUR",
51
+ )
52
+ )
53
+ ```
@@ -156,5 +156,3 @@ _build
156
156
 
157
157
  # Test databases
158
158
  *.sqlite
159
-
160
-
@@ -0,0 +1,7 @@
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-queuebie
3
- Version: 0.1.0
3
+ Version: 0.2.0
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
@@ -65,6 +65,52 @@ A simple message queue for commands and events (CQRS) for Django.
65
65
  * Message handlers receive the context of the message (command or event), providing an explicit API
66
66
  * No magic, no side effects since the queue works synchronously
67
67
 
68
+ ```python
69
+ import dataclasses
70
+
71
+ from queuebie.runner import handle_message
72
+ from queuebie.messages import Command, Event
73
+ from queuebie import message_registry
74
+
75
+
76
+ # Example command
77
+ @dataclasses.dataclass(kw_only=True)
78
+ class BuyProduct(Command):
79
+ product_id: int
80
+ customer_id: int
81
+ price: float
82
+ currency: str
83
+
84
+
85
+ # Example event
86
+ @dataclasses.dataclass(kw_only=True)
87
+ class ProductBought(Event):
88
+ product_id: int
89
+ customer_id: int
90
+
91
+
92
+ # Example handler
93
+ @message_registry.register_command(BuyProduct)
94
+ def handle_buy_product(context: BuyProduct) -> Event:
95
+ # Here lives your business logic
96
+
97
+ return ProductBought(
98
+ product_id=context.product_id,
99
+ customer_id=context.customer_id,
100
+ )
101
+
102
+
103
+ # Start queue and process messages
104
+ handle_message(
105
+ BuyProduct(
106
+ product_id=product.id,
107
+ customer_id=customer.id,
108
+ price=12.99,
109
+ currency="EUR",
110
+ )
111
+ )
112
+ ```
113
+
68
114
  ## Installation
69
115
 
70
116
  - Install the package via pip:
@@ -21,6 +21,52 @@ A simple message queue for commands and events (CQRS) for Django.
21
21
  * Message handlers receive the context of the message (command or event), providing an explicit API
22
22
  * No magic, no side effects since the queue works synchronously
23
23
 
24
+ ```python
25
+ import dataclasses
26
+
27
+ from queuebie.runner import handle_message
28
+ from queuebie.messages import Command, Event
29
+ from queuebie import message_registry
30
+
31
+
32
+ # Example command
33
+ @dataclasses.dataclass(kw_only=True)
34
+ class BuyProduct(Command):
35
+ product_id: int
36
+ customer_id: int
37
+ price: float
38
+ currency: str
39
+
40
+
41
+ # Example event
42
+ @dataclasses.dataclass(kw_only=True)
43
+ class ProductBought(Event):
44
+ product_id: int
45
+ customer_id: int
46
+
47
+
48
+ # Example handler
49
+ @message_registry.register_command(BuyProduct)
50
+ def handle_buy_product(context: BuyProduct) -> Event:
51
+ # Here lives your business logic
52
+
53
+ return ProductBought(
54
+ product_id=context.product_id,
55
+ customer_id=context.customer_id,
56
+ )
57
+
58
+
59
+ # Start queue and process messages
60
+ handle_message(
61
+ BuyProduct(
62
+ product_id=product.id,
63
+ customer_id=customer.id,
64
+ price=12.99,
65
+ currency="EUR",
66
+ )
67
+ )
68
+ ```
69
+
24
70
  ## Installation
25
71
 
26
72
  - Install the package via pip:
@@ -0,0 +1,30 @@
1
+ # Motivation
2
+
3
+ You might wonder what's the USP for this package. Messages (event) queues are usually async and use a broker, etc.
4
+
5
+ The main goal is to provide a way to separate your domains in Django and reduce coupling between domains or Django apps.
6
+
7
+ So why should you use "django-queuebie"?
8
+
9
+ * Thinking in commands and events splits up your business logic from one big chunk to manageable sizes
10
+ * Handlers are function-based with a defined input (the context), they are predictable and easy to test
11
+ * Decoupling different parts of your application by listening to events instead of imperative programming
12
+ * Avoid putting business logic in your view layer since it has to live inside a handler function
13
+ * Creates an explicit pattern for connecting services (places of different business logic) instead of chaining them
14
+ individually
15
+
16
+ # Why not Django signals?
17
+
18
+ The reader might think "Hey, sync queues in Django? I already have this!" — and that's true, somehow.
19
+
20
+ The key differences are mostly on a semantic level.
21
+
22
+ First, commands and events are a concept that doesn't exist for Django signals. They provide a proven way of separating
23
+ your business logic, and in addition, provide a basis for discussion with non-technical folks.
24
+
25
+ Furthermore, queuebie messages (commands and events) don't belong to a model. Models represent the way you store your
26
+ data. That's not necessarily the best way to structure your custom logic. So by using signals, you're strongly primed to
27
+ think in models, meaning database structure, and not in domain concerns.
28
+
29
+ Messages provide an explicit context via a dataclass which can be interpreted as an explicit internal API to communicate
30
+ between your domains. That's a huge plus since explicit is better than implicit.
@@ -33,8 +33,8 @@ Take care to use the same name in the logging configuration in your Django setti
33
33
 
34
34
  ## QUEUEBIE_STRICT_MODE
35
35
 
36
- Queuebie enforces by default that commands are not used outside its domain (aka Django app). If you want to skip that
37
- restriction for whatever reason, you can do so.
36
+ Queuebie enforces by default that commands are not used outside its domain (aka Django app) and event handlers don't
37
+ talk to the database. If you want to skip that restriction for whatever reason, you can do so.
38
38
 
39
39
  ```python
40
40
  QUEUEBIE_STRICT_MODE = False
@@ -93,8 +93,8 @@ lint.select = [
93
93
  "INP" # Ban PEP-420 implicit namespace packages
94
94
  ]
95
95
  lint.ignore = [
96
- 'TD002', # Missing author in TODO
97
- 'TD003', # Missing issue link on the line following this TODO
96
+ "TD002", # Missing author in TODO
97
+ "TD003", # Missing issue link on the line following this TODO
98
98
  ]
99
99
 
100
100
  # Allow autofix for all enabled rules (when `--fix`) is provided.
@@ -179,26 +179,23 @@ line-ending = "auto"
179
179
  # Allow seemingly unused imports
180
180
  "F401",
181
181
  ]
182
-
183
182
  "**/tests/**/test_*.py" = [
184
183
  # Allow boolean positional params in tests (for assertIs())
185
184
  "FBT003",
186
185
  ]
187
-
188
186
  "scripts/*.py" = [
189
187
  # Checks for packages that are missing an __init__.py file
190
188
  "INP001",
191
189
  ]
192
-
193
190
  ".ambient-package-update/*.py" = [
194
191
  # Checks for packages that are missing an __init__.py file
195
192
  "INP001",
196
193
  ]
197
-
198
194
  "docs/*.py" = [
199
195
  # Checks for packages that are missing an __init__.py file
200
196
  "INP001",
201
197
  ]
198
+
202
199
  [tool.tox]
203
200
  legacy_tox_ini = """
204
201
  [testenv]
@@ -1,6 +1,6 @@
1
1
  """Simple message queue for commands and events (CQRS)"""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
4
4
 
5
5
  from queuebie.registry import MessageRegistry
6
6
 
@@ -0,0 +1,25 @@
1
+ from unittest.mock import patch
2
+
3
+ from django.db import connections
4
+
5
+
6
+ class DatabaseAccessDeniedError(RuntimeError):
7
+ def __init__(self):
8
+ super().__init__("Database access is disabled in this context.")
9
+
10
+
11
+ class BlockDatabaseAccess:
12
+ def __enter__(self):
13
+ # Patcht alle aktiven DB-Verbindungen, sodass jede Query fehlschlägt
14
+ self.patches = [
15
+ patch.object(connections[alias], "cursor", side_effect=self._raise_error) for alias in connections
16
+ ]
17
+ for p in self.patches:
18
+ p.start()
19
+
20
+ def __exit__(self, exc_type, exc_value, traceback):
21
+ for p in self.patches:
22
+ p.stop()
23
+
24
+ def _raise_error(self, *args, **kwargs):
25
+ raise DatabaseAccessDeniedError()
@@ -1,11 +1,14 @@
1
1
  import importlib
2
+ from contextlib import nullcontext
2
3
 
3
4
  from django.db import transaction
4
5
 
5
6
  from queuebie import message_registry
7
+ from queuebie.database_blocker import BlockDatabaseAccess
6
8
  from queuebie.exceptions import InvalidMessageTypeError
7
9
  from queuebie.logger import get_logger
8
10
  from queuebie.messages import Command, Event, Message
11
+ from queuebie.settings import get_queuebie_strict_mode
9
12
 
10
13
 
11
14
  def handle_message(messages: Message | list[Message]) -> None:
@@ -18,25 +21,27 @@ def handle_message(messages: Message | list[Message]) -> None:
18
21
  # Run auto-registry
19
22
  message_registry.autodiscover()
20
23
 
21
- handler_list = []
22
24
  while queue:
23
25
  message = queue.pop(0)
24
26
  if isinstance(message, Command):
25
27
  handler_list = message_registry.command_dict.get(message.module_path(), [])
28
+ block_db_access = False
26
29
  else:
27
30
  handler_list = message_registry.event_dict.get(message.module_path(), [])
31
+ block_db_access = True if get_queuebie_strict_mode() else False
28
32
 
29
- new_messages = _process_message(handler_list=handler_list, message=message)
33
+ new_messages = _process_message(handler_list=handler_list, message=message, block_db_access=block_db_access)
30
34
  queue.extend(new_messages)
31
35
 
32
36
 
33
- def _process_message(*, handler_list: list, message: [Command, Event]):
37
+ def _process_message(*, handler_list: list, message: [Command, Event], block_db_access: bool) -> list[Message]:
34
38
  """
35
39
  Handler to process messages of type "Command"
36
40
  """
37
41
  logger = get_logger()
38
42
  messages = []
39
43
 
44
+ # TODO: should the whole chain be atomic and not just the handler?
40
45
  with transaction.atomic():
41
46
  for handler in handler_list:
42
47
  try:
@@ -45,7 +50,8 @@ def _process_message(*, handler_list: list, message: [Command, Event]):
45
50
  )
46
51
  module = importlib.import_module(handler["module"])
47
52
  handler_function = getattr(module, handler["name"])
48
- handler_messages = handler_function(context=message) or []
53
+ with BlockDatabaseAccess() if block_db_access else nullcontext():
54
+ handler_messages = handler_function(context=message) or []
49
55
  handler_messages = handler_messages if isinstance(handler_messages, list) else [handler_messages]
50
56
  if len(handler_messages) > 0:
51
57
  messages.extend(handler_messages)
@@ -1,10 +1,12 @@
1
+ from random import randint
2
+
1
3
  from django.contrib.auth.models import User
2
4
 
3
5
  from queuebie import message_registry
4
6
  from queuebie.logger import get_logger
5
7
  from queuebie.messages import Event
6
- from testapp.messages.commands.my_commands import CriticalCommand, DoSomething
7
- from testapp.messages.events.my_events import SomethingHappened
8
+ from testapp.messages.commands.my_commands import CriticalCommand, DoSomething, PersistSomething
9
+ from testapp.messages.events.my_events import SomethingHappened, SomethingHappenedThatWantsToBePersisted
8
10
 
9
11
 
10
12
  @message_registry.register_command(command=DoSomething)
@@ -14,6 +16,13 @@ def handle_my_command(*, context: DoSomething) -> list[Event] | Event:
14
16
  return SomethingHappened(other_var=context.my_var + 1)
15
17
 
16
18
 
19
+ @message_registry.register_command(command=PersistSomething)
20
+ def handle_something_that_needs_persistance(*, context: PersistSomething) -> Event:
21
+ User.objects.create(username="testuser" + str(randint(1, 100)))
22
+
23
+ return SomethingHappenedThatWantsToBePersisted(any_var=1)
24
+
25
+
17
26
  @message_registry.register_command(command=CriticalCommand)
18
27
  def handle_critical_command(*, context: CriticalCommand) -> None:
19
28
  if context.my_var == 0:
@@ -0,0 +1,20 @@
1
+ import random
2
+
3
+ from django.contrib.auth.models import User
4
+
5
+ from queuebie import message_registry
6
+ from queuebie.logger import get_logger
7
+ from queuebie.messages import Command
8
+ from testapp.messages.events.my_events import SomethingHappened, SomethingHappenedThatWantsToBePersistedViaEvent
9
+
10
+
11
+ @message_registry.register_event(event=SomethingHappened)
12
+ def handle_my_event(*, context: SomethingHappened) -> list[Command] | Command:
13
+ logger = get_logger()
14
+ logger.info(f'Event "SomethingHappened" executed with other_var={context.other_var}.')
15
+ return []
16
+
17
+
18
+ @message_registry.register_event(event=SomethingHappenedThatWantsToBePersistedViaEvent)
19
+ def handle_event_and_try_to_persist_something(*, context: SomethingHappenedThatWantsToBePersistedViaEvent):
20
+ User.objects.create(username="testuser" + str(random.randint(1, 100)))
@@ -16,3 +16,8 @@ class CriticalCommand(Command):
16
16
  @dataclass(kw_only=True)
17
17
  class SameNameCommand(Command):
18
18
  name: str
19
+
20
+
21
+ @dataclass(kw_only=True)
22
+ class PersistSomething(Command):
23
+ any_var: str
@@ -0,0 +1,18 @@
1
+ import dataclasses
2
+
3
+ from queuebie.messages import Event
4
+
5
+
6
+ @dataclasses.dataclass(kw_only=True)
7
+ class SomethingHappened(Event):
8
+ other_var: int
9
+
10
+
11
+ @dataclasses.dataclass(kw_only=True)
12
+ class SomethingHappenedThatWantsToBePersisted(Event):
13
+ any_var: int
14
+
15
+
16
+ @dataclasses.dataclass(kw_only=True)
17
+ class SomethingHappenedThatWantsToBePersistedViaEvent(Event):
18
+ any_var: int
@@ -0,0 +1,17 @@
1
+ import pytest
2
+ from django.contrib.auth.models import User
3
+
4
+ from queuebie.database_blocker import BlockDatabaseAccess, DatabaseAccessDeniedError
5
+
6
+
7
+ def test_block_database_access_use_db():
8
+ with pytest.raises(DatabaseAccessDeniedError, match="Database access is disabled in this context."):
9
+ with BlockDatabaseAccess():
10
+ User.objects.get(pk=1)
11
+
12
+
13
+ def test_block_database_access_dont_use_db():
14
+ with BlockDatabaseAccess():
15
+ user = User(pk=1)
16
+
17
+ assert user.id == 1
@@ -10,7 +10,10 @@ from queuebie import MessageRegistry
10
10
  from queuebie.exceptions import RegisterOutOfScopeCommandError
11
11
  from queuebie.settings import get_queuebie_cache_key
12
12
  from testapp.messages.commands.my_commands import CriticalCommand, DoSomething
13
- from testapp.messages.events.my_events import SomethingHappened
13
+ from testapp.messages.events.my_events import (
14
+ SomethingHappened,
15
+ SomethingHappenedThatWantsToBePersistedViaEvent,
16
+ )
14
17
  from tests.helpers.commands import DoTestThings
15
18
 
16
19
 
@@ -121,7 +124,7 @@ def test_message_autodiscover_regular():
121
124
  message_registry.autodiscover()
122
125
 
123
126
  # Assert one command registered
124
- assert len(message_registry.command_dict) == 2 # noqa: PLR2004
127
+ assert len(message_registry.command_dict) == 3 # noqa: PLR2004
125
128
  assert DoSomething.module_path() in message_registry.command_dict.keys()
126
129
  assert CriticalCommand.module_path() in message_registry.command_dict.keys()
127
130
 
@@ -132,11 +135,12 @@ def test_message_autodiscover_regular():
132
135
  "name": "handle_my_command",
133
136
  } == message_registry.command_dict[DoSomething.module_path()][0]
134
137
 
135
- # Assert one event registered
136
- assert len(message_registry.event_dict) == 1
138
+ # Assert two events registered
139
+ assert len(message_registry.event_dict) == 2 # noqa: PLR2004
137
140
  assert SomethingHappened.module_path() in message_registry.event_dict.keys()
141
+ assert SomethingHappenedThatWantsToBePersistedViaEvent.module_path() in message_registry.event_dict.keys()
138
142
 
139
- # Assert one handler registered
143
+ # Assert two handlers registered
140
144
  assert len(message_registry.event_dict[SomethingHappened.module_path()]) == 1
141
145
  assert {"module": "testapp.handlers.events.testapp", "name": "handle_my_event"} == message_registry.event_dict[
142
146
  SomethingHappened.module_path()
@@ -3,12 +3,17 @@ from unittest import mock
3
3
 
4
4
  import pytest
5
5
  from django.contrib.auth.models import User
6
+ from django.test import override_settings
6
7
 
7
8
  from queuebie import MessageRegistry
9
+ from queuebie.database_blocker import DatabaseAccessDeniedError
8
10
  from queuebie.exceptions import InvalidMessageTypeError
9
11
  from queuebie.runner import _process_message, handle_message
10
- from testapp.messages.commands.my_commands import CriticalCommand, DoSomething, SameNameCommand
11
- from testapp.messages.events.my_events import SomethingHappened
12
+ from testapp.messages.commands.my_commands import CriticalCommand, DoSomething, PersistSomething, SameNameCommand
13
+ from testapp.messages.events.my_events import (
14
+ SomethingHappened,
15
+ SomethingHappenedThatWantsToBePersistedViaEvent,
16
+ )
12
17
 
13
18
 
14
19
  @pytest.mark.django_db
@@ -65,6 +70,23 @@ def test_handle_message_pass_invalid_type():
65
70
  handle_message(messages=MessageRegistry())
66
71
 
67
72
 
73
+ @pytest.mark.django_db
74
+ def test_handle_message_command_with_db_hit_is_ok():
75
+ handle_message(messages=[PersistSomething(any_var="noodle")])
76
+
77
+
78
+ @pytest.mark.django_db
79
+ def test_handle_message_event_with_db_hit_fails():
80
+ with pytest.raises(DatabaseAccessDeniedError, match="Database access is disabled in this context"):
81
+ handle_message(messages=[SomethingHappenedThatWantsToBePersistedViaEvent(any_var=1)])
82
+
83
+
84
+ @pytest.mark.django_db
85
+ @override_settings(QUEUEBIE_STRICT_MODE=False)
86
+ def test_handle_message_event_with_db_hit_ok_when_not_on_strict_mode(*args):
87
+ handle_message(messages=[SomethingHappenedThatWantsToBePersistedViaEvent(any_var=1)])
88
+
89
+
68
90
  @pytest.mark.django_db
69
91
  @mock.patch("queuebie.registry.get_queuebie_strict_mode", return_value=False)
70
92
  @mock.patch.object(MessageRegistry, "autodiscover")
@@ -112,6 +134,6 @@ def test_process_message_atomic_works(mocked_handle_command, *args):
112
134
  message = DoSomething(my_var=1)
113
135
 
114
136
  with pytest.raises(RuntimeError, match="Something is broken."):
115
- _process_message(handler_list=handler_list, message=message)
137
+ _process_message(handler_list=handler_list, message=message, block_db_access=False)
116
138
 
117
139
  assert User.objects.filter(username="username").exists() is False
@@ -1,7 +0,0 @@
1
- ## Features
2
-
3
- * Split up your business logic in commands and events
4
- * Commands are imperatives telling your system what to do, events reflect that something has happened
5
- * Register light-weight functions via a decorator to listen to your commands and events
6
- * Message handlers receive the context of the message (command or event), providing an explicit API
7
- * No magic, no side effects since the queue works synchronously
@@ -1,4 +0,0 @@
1
- # Changelog
2
-
3
- * *0.1.0* (2025-01-29)
4
- * Project init
@@ -1,14 +0,0 @@
1
- # Motivation
2
-
3
- You might wonder what's the USP for this package. Messages (event) queues are usually async and use a broker, etc.
4
-
5
- The main goal is to provide a way to separate your domains in Django and reduce coupling between domains or Django apps.
6
-
7
- So why should you use "django-queuebie"?
8
-
9
- * Thinking in commands and events splits up your business logic from one big chunk to manageable sizes
10
- * Handlers are function-based with a defined input (the context), they are predictable and easy to test
11
- * Decoupling different parts of your application by listening to events instead of imperative programming
12
- * Avoid putting business logic in your view layer since it has to live inside a handler function
13
- * Creates an explicit pattern for connecting services (places of different business logic) instead of chaining them
14
- individually
@@ -1,11 +0,0 @@
1
- from queuebie import message_registry
2
- from queuebie.logger import get_logger
3
- from queuebie.messages import Command
4
- from testapp.messages.events.my_events import SomethingHappened
5
-
6
-
7
- @message_registry.register_event(event=SomethingHappened)
8
- def handle_my_event(*, context: SomethingHappened) -> list[Command] | Command:
9
- logger = get_logger()
10
- logger.info(f'Event "SomethingHappened" executed with other_var={context.other_var}.')
11
- return []
@@ -1,8 +0,0 @@
1
- import dataclasses
2
-
3
- from queuebie.messages import Event
4
-
5
-
6
- @dataclasses.dataclass(kw_only=True)
7
- class SomethingHappened(Event):
8
- other_var: int
File without changes