django-queuebie 0.4.1__tar.gz → 0.6.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.
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/workflows/quality-gate.yml +3 -7
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.pre-commit-config.yaml +5 -5
- django_queuebie-0.6.0/CHANGES.md +86 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/PKG-INFO +4 -4
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/features/getting-started.md +9 -3
- django_queuebie-0.6.0/docs/features/settings.md +106 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/pyproject.toml +10 -8
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/__init__.py +1 -1
- django_queuebie-0.6.0/queuebie/exceptions.py +49 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/registry.py +48 -20
- django_queuebie-0.6.0/queuebie/settings.py +72 -0
- django_queuebie-0.6.0/queuebie/utils.py +38 -0
- django_queuebie-0.6.0/testapp/handlers/commands/messages.py +9 -0
- django_queuebie-0.6.0/testapp/messages/commands/messages.py +13 -0
- django_queuebie-0.6.0/testapp/nested_domain/handlers/commands/nested_domain.py +13 -0
- django_queuebie-0.6.0/testapp/nested_domain/handlers/events/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/nested_domain/handlers/events/nested_domain.py +9 -0
- django_queuebie-0.6.0/testapp/nested_domain/messages/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/nested_domain/messages/commands/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/nested_domain/messages/commands/nested_commands.py +8 -0
- django_queuebie-0.6.0/testapp/nested_domain/messages/events/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/nested_domain/messages/events/nested_events.py +8 -0
- django_queuebie-0.6.0/testapp/tests/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/tests/handlers/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/tests/handlers/commands/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/tests/handlers/commands/decoy.py +12 -0
- django_queuebie-0.6.0/testapp/tests/messages/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/tests/messages/commands/__init__.py +0 -0
- django_queuebie-0.6.0/testapp/tests/messages/commands/decoy_commands.py +8 -0
- django_queuebie-0.6.0/tests/__init__.py +0 -0
- django_queuebie-0.6.0/tests/helpers/__init__.py +0 -0
- django_queuebie-0.6.0/tests/management_commands/__init__.py +0 -0
- django_queuebie-0.6.0/tests/test_exceptions.py +67 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/test_registry.py +141 -11
- django_queuebie-0.6.0/tests/test_settings.py +91 -0
- django_queuebie-0.6.0/tests/test_utils.py +81 -0
- django_queuebie-0.6.0/uv.lock +1360 -0
- django_queuebie-0.4.1/CHANGES.md +0 -56
- django_queuebie-0.4.1/docs/features/settings.md +0 -41
- django_queuebie-0.4.1/queuebie/exceptions.py +0 -17
- django_queuebie-0.4.1/queuebie/settings.py +0 -32
- django_queuebie-0.4.1/queuebie/utils.py +0 -30
- django_queuebie-0.4.1/tests/test_exceptions.py +0 -23
- django_queuebie-0.4.1/tests/test_settings.py +0 -45
- django_queuebie-0.4.1/tests/test_utils.py +0 -38
- django_queuebie-0.4.1/uv.lock +0 -1255
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.ambient-package-update/metadata.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.ambient-package-update/templates/snippets/content.tpl +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.ambient-package-update/templates/snippets/tagline.tpl +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.editorconfig +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/workflows/ci.yml +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.github/workflows/release.yml +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.gitignore +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/.readthedocs.yaml +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/CODE_OF_CONDUCT.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/CONTRIBUTING.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/LICENSE.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/MANIFEST.in +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/README.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/SECURITY.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/Makefile +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/conf.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/features/changelog.rst +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/features/introduction.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/features/motivation.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/features/setup.md +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/index.rst +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/docs/make.bat +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/manage.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/apps.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/database_blocker.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/logger.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/management/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/management/commands/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/management/commands/clear_queuebie_registry.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/messages.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/queuebie/runner.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/scripts/unix/install_requirements.sh +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/scripts/unix/publish_to_pypi.sh +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/scripts/windows/install_requirements.ps1 +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/scripts/windows/publish_to_pypi.ps1 +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/settings.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/handlers/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/handlers/commands/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/handlers/commands/testapp.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/handlers/events/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/handlers/events/testapp.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/messages/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/messages/commands/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/messages/commands/my_commands.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/messages/commands/other_commands.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/messages/events/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/messages/events/my_events.py +0 -0
- {django_queuebie-0.4.1/tests → django_queuebie-0.6.0/testapp/nested_domain}/__init__.py +0 -0
- {django_queuebie-0.4.1/tests/helpers → django_queuebie-0.6.0/testapp/nested_domain/handlers}/__init__.py +0 -0
- {django_queuebie-0.4.1/tests/management_commands → django_queuebie-0.6.0/testapp/nested_domain/handlers/commands}/__init__.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/testapp/urls.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/helpers/commands.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/helpers/events.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/management_commands/test_clear_queuebie_registry.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/test_apps.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/test_database_blocker.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/test_logger.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/test_messages.py +0 -0
- {django_queuebie-0.4.1 → django_queuebie-0.6.0}/tests/test_runner.py +0 -0
|
@@ -26,18 +26,14 @@ jobs:
|
|
|
26
26
|
strategy:
|
|
27
27
|
matrix:
|
|
28
28
|
python-version: ['3.11', '3.12', '3.13', '3.14', ]
|
|
29
|
-
django-version: ['
|
|
29
|
+
django-version: ['52', '60', '61', ]
|
|
30
30
|
|
|
31
31
|
# Exclude Python/Django combinations that are not supported upstream.
|
|
32
32
|
exclude:
|
|
33
33
|
- python-version: '3.11'
|
|
34
34
|
django-version: 60
|
|
35
|
-
- python-version: '3.
|
|
36
|
-
django-version:
|
|
37
|
-
- python-version: '3.14'
|
|
38
|
-
django-version: 42
|
|
39
|
-
- python-version: '3.14'
|
|
40
|
-
django-version: 52
|
|
35
|
+
- python-version: '3.11'
|
|
36
|
+
django-version: 61
|
|
41
37
|
|
|
42
38
|
steps:
|
|
43
39
|
- uses: actions/checkout@v6
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
repos:
|
|
5
5
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
6
|
-
rev: v0.
|
|
6
|
+
rev: v0.16.4
|
|
7
7
|
hooks:
|
|
8
8
|
# Run the Ruff formatter.
|
|
9
9
|
- id: ruff-format
|
|
@@ -16,7 +16,7 @@ repos:
|
|
|
16
16
|
hooks:
|
|
17
17
|
- id: blacken-docs
|
|
18
18
|
additional_dependencies:
|
|
19
|
-
- black==
|
|
19
|
+
- black==26.5.1
|
|
20
20
|
files: '(?:README\.md|\.ambient-package-update\/templates\/snippets\/.*\.tpl|docs\/.*\.(?:md|rst))'
|
|
21
21
|
|
|
22
22
|
- repo: https://github.com/asottile/pyupgrade
|
|
@@ -26,16 +26,16 @@ repos:
|
|
|
26
26
|
args: [ --py311-plus ]
|
|
27
27
|
|
|
28
28
|
- repo: https://github.com/adamchainz/django-upgrade
|
|
29
|
-
rev: 1.
|
|
29
|
+
rev: 1.32.0
|
|
30
30
|
hooks:
|
|
31
31
|
- id: django-upgrade
|
|
32
|
-
args: [--target-version, "
|
|
32
|
+
args: [--target-version, "5.2"]
|
|
33
33
|
|
|
34
34
|
- repo: https://github.com/adamchainz/djade-pre-commit
|
|
35
35
|
rev: 1.9.0
|
|
36
36
|
hooks:
|
|
37
37
|
- id: djade
|
|
38
|
-
args: [--target-version, "
|
|
38
|
+
args: [--target-version, "5.2"]
|
|
39
39
|
exclude: |
|
|
40
40
|
(?x)^(
|
|
41
41
|
charts/.*
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
**0.6.0** (2026-09-09)
|
|
4
|
+
* Auto-discovery now walks the whole subtree of every local Django app, so `handlers/commands` and `handlers/events`
|
|
5
|
+
directories may live in sub-packages instead of only at the app root
|
|
6
|
+
* Auto-discovery skips the directory names which occur inside Django apps but never hold handlers -
|
|
7
|
+
`__pycache__`, `fixtures`, `locale`, `media`, `migrations`, `node_modules`, `static`, `templates` and `tests`.
|
|
8
|
+
`QUEUEBIE_DEFAULT_EXCLUDED_DIRECTORIES` holds that list and can be overwritten
|
|
9
|
+
* Added the setting `QUEUEBIE_EXCLUDED_DIRECTORIES` for project-specific directory names. It is added to the list
|
|
10
|
+
above instead of replacing it
|
|
11
|
+
* **Breaking change:** Strict mode compares the package owning the `handlers/` or `messages/` directory instead of
|
|
12
|
+
the Django app. Nested layouts get the boundary enforced they always described, and two cases which used to pass
|
|
13
|
+
the check unconditionally are now validated:
|
|
14
|
+
* handlers and commands living outside any installed Django app
|
|
15
|
+
* commands living inside an app but outside a `messages/` directory - a command in, say,
|
|
16
|
+
`my_app/domain/orders/commands.py` has no owning `messages/` directory, so its scope is its full module path,
|
|
17
|
+
which only a handler in that same module can share. Registering a handler from anywhere else raises
|
|
18
|
+
`RegisterOutOfScopeCommandError` at import time. Move such commands into a `messages/` directory or turn
|
|
19
|
+
strict mode off
|
|
20
|
+
* **Breaking change:** Replaced `queuebie.utils.is_part_of_app()` with `queuebie.utils.is_same_scope()` and
|
|
21
|
+
`queuebie.utils.message_scope()`
|
|
22
|
+
* **Breaking change:** `RegisterOutOfScopeCommandError` now names both scopes instead of only the command and the
|
|
23
|
+
handler, so a mismatch between two sub-packages of the same app can be read off the message
|
|
24
|
+
* Handlers are now registered in a deterministic order - the handler package first, then its modules sorted by
|
|
25
|
+
name. Where a message has more than one handler, and since the bus is synchronous, that order is observable and
|
|
26
|
+
may differ from the one the file system happened to yield before
|
|
27
|
+
|
|
28
|
+
**0.5.0** (2026-08-27)
|
|
29
|
+
* Added support for Django 6.1
|
|
30
|
+
* **Breaking change:** Dropped support for Django 4.2, whose extended support ended in April 2026
|
|
31
|
+
* Updated the linting and CI setup to the current ambient-package-update template
|
|
32
|
+
|
|
33
|
+
**0.4.1** (2026-07-03)
|
|
34
|
+
* Updated company and maintainer information to "Beyonder Deutschland"
|
|
35
|
+
|
|
36
|
+
**0.4.0** (2026-07-03)
|
|
37
|
+
* **Breaking change:** Dropped support for Python 3.10 (nearing end-of-life in October 2026)
|
|
38
|
+
* Added support for Python 3.14
|
|
39
|
+
* Added native uv support to the rendered Read the Docs configuration
|
|
40
|
+
* Replaced the unmaintained "m2r2" documentation dependency with "sphinx-mdinclude"
|
|
41
|
+
* Added a Code of Conduct, issue templates and a pull request template to rendered packages
|
|
42
|
+
* Made the single-version CI and Read the Docs jobs track the newest supported Python version
|
|
43
|
+
* Bumped rendered single-version jobs to Python 3.14
|
|
44
|
+
* Added a cache suffix to the uv setup step to avoid CI cache namespace conflicts
|
|
45
|
+
* Excluded unsupported Python/Django combinations (Python 3.14 with Django 4.2 and 5.2) from the rendered CI matrix
|
|
46
|
+
* Fixed the rendered ruff target-version to track the minimum supported Python (matching requires-python) instead of the newest
|
|
47
|
+
* Removed the stale .md source suffix from the rendered Sphinx config, since sphinx-mdinclude provides only the mdinclude directive (not a Markdown source parser)
|
|
48
|
+
|
|
49
|
+
**0.3.10** (2026-03-30)
|
|
50
|
+
* Maintenance updates via ambient-package-update
|
|
51
|
+
|
|
52
|
+
**0.3.9** (2026-03-30)
|
|
53
|
+
* Maintenance updates via ambient-package-update
|
|
54
|
+
|
|
55
|
+
**0.3.8** (2025-12-11)
|
|
56
|
+
* Maintenance updates via ambient-package-update
|
|
57
|
+
|
|
58
|
+
**0.3.7** (2025-10-15)
|
|
59
|
+
* Maintenance updates via ambient-package-update
|
|
60
|
+
|
|
61
|
+
**0.3.6** (2025-10-10)
|
|
62
|
+
* Maintenance updates via ambient-package-update
|
|
63
|
+
|
|
64
|
+
**0.3.5** (2025-10-09)
|
|
65
|
+
* Maintenance updates via ambient-package-update
|
|
66
|
+
|
|
67
|
+
**0.3.4** (2025-05-29)
|
|
68
|
+
* Maintenance updates via ambient-package-update
|
|
69
|
+
|
|
70
|
+
**0.3.3** (2025-04-03)
|
|
71
|
+
* Clarified package tagline
|
|
72
|
+
|
|
73
|
+
**0.3.2** (2025-04-03)
|
|
74
|
+
* Maintenance updates via ambient-package-update
|
|
75
|
+
|
|
76
|
+
* *0.3.1* (2025-03-19)
|
|
77
|
+
* Added a paranoid-ish test to check that the import logic isn't breaking any testing functionality
|
|
78
|
+
|
|
79
|
+
* *0.3.0* (2025-03-19)
|
|
80
|
+
* The whole queue iteration now is wrapped in a transaction atomic
|
|
81
|
+
|
|
82
|
+
* *0.2.0* (2025-03-17)
|
|
83
|
+
* Extend strict mode to prohibit event (!) handlers to talk to the database
|
|
84
|
+
|
|
85
|
+
* *0.1.0* (2025-01-29)
|
|
86
|
+
* Project init
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: django-queuebie
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A simple and synchronous message queue for commands and events for Django
|
|
5
5
|
Project-URL: Homepage, https://github.com/ambient-innovation/django-queuebie/
|
|
6
6
|
Project-URL: Documentation, https://django-queuebie.readthedocs.io/en/latest/index.html
|
|
@@ -33,9 +33,9 @@ License-File: LICENSE.md
|
|
|
33
33
|
Classifier: Development Status :: 4 - Beta
|
|
34
34
|
Classifier: Environment :: Web Environment
|
|
35
35
|
Classifier: Framework :: Django
|
|
36
|
-
Classifier: Framework :: Django :: 4.2
|
|
37
36
|
Classifier: Framework :: Django :: 5.2
|
|
38
37
|
Classifier: Framework :: Django :: 6.0
|
|
38
|
+
Classifier: Framework :: Django :: 6.1
|
|
39
39
|
Classifier: Intended Audience :: Developers
|
|
40
40
|
Classifier: License :: OSI Approved :: MIT License
|
|
41
41
|
Classifier: Natural Language :: English
|
|
@@ -48,7 +48,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
48
48
|
Classifier: Programming Language :: Python :: 3.14
|
|
49
49
|
Classifier: Topic :: Utilities
|
|
50
50
|
Requires-Python: >=3.11
|
|
51
|
-
Requires-Dist: django>=
|
|
51
|
+
Requires-Dist: django>=5.2
|
|
52
52
|
Provides-Extra: dev
|
|
53
53
|
Requires-Dist: ambient-package-update; extra == 'dev'
|
|
54
54
|
Requires-Dist: keyring~=25.7; extra == 'dev'
|
|
@@ -52,15 +52,21 @@ class ProductBought(Event):
|
|
|
52
52
|
Here's an example of a simple handler function. By design, these are functions and not objects to keep it as simple,
|
|
53
53
|
understandable and testable as possible.
|
|
54
54
|
|
|
55
|
-
They have to live
|
|
56
|
-
|
|
55
|
+
They have to live in a `handlers/commands/[your_filename].py` or respectively `handlers/events/[your_filename].py`
|
|
56
|
+
directory for the auto-discovery to find them. Such a directory may sit at any depth below the root of a Django app,
|
|
57
|
+
so `my_app/handlers/commands/product.py` and `my_app/shipping/handlers/commands/product.py` both work. Directories
|
|
58
|
+
listed in `QUEUEBIE_EXCLUDED_DIRECTORIES` are skipped.
|
|
57
59
|
|
|
58
60
|
A handler becomes a handler when three conditions are met:
|
|
59
61
|
|
|
60
62
|
* The function is registered as such via one of the two decorators `register_command` and `register_event`
|
|
61
|
-
* The function lives within `
|
|
63
|
+
* The function lives within a `handlers/commands/` or `handlers/events/` directory inside one of your Django apps
|
|
62
64
|
* The function takes a message (command or event) and returns optionally the other type of message (event or command).
|
|
63
65
|
|
|
66
|
+
The package owning that `handlers/` directory is the handler's *scope*. In strict mode, a command handler may only
|
|
67
|
+
handle commands whose `messages/` directory has the same owner. See
|
|
68
|
+
[QUEUEBIE_STRICT_MODE](settings.md#queuebie_strict_mode) for the details.
|
|
69
|
+
|
|
64
70
|
```python
|
|
65
71
|
# my_app/handlers/commands/product.py
|
|
66
72
|
from queuebie import message_registry
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Settings
|
|
2
|
+
|
|
3
|
+
## QUEUEBIE_APP_BASE_PATH
|
|
4
|
+
|
|
5
|
+
Queuebie needs to know where your project lives to detect local Django apps. It defaults to `settings.BASE_PATH`
|
|
6
|
+
but you can overwrite it with a string or a `Pathlib` object.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
QUEUEBIE_APP_BASE_PATH = Path(__file__).resolve(strict=True).parent
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## QUEUEBIE_CACHE_KEY
|
|
15
|
+
|
|
16
|
+
Queuebie will cache all detected message handlers in Django's default cache. The default cache key is "queuebie".
|
|
17
|
+
You can overwrite it with this variable.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
QUEUEBIE_CACHE_KEY = "my_very_special_cache_key"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## QUEUEBIE_LOGGER_NAME
|
|
24
|
+
|
|
25
|
+
Queuebie defines a Django logger with the default name "queuebie". If you want to rename that logger, you can set this
|
|
26
|
+
variable.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
QUEUEBIE_LOGGER_NAME = "my_very_special_logger"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Take care to use the same name in the logging configuration in your Django settings.
|
|
33
|
+
|
|
34
|
+
## QUEUEBIE_EXCLUDED_DIRECTORIES
|
|
35
|
+
|
|
36
|
+
Queuebie searches the whole subtree of every local Django app for `handlers/commands` and `handlers/events`
|
|
37
|
+
directories. Directory names listed here are skipped, which keeps handler-shaped trees that are not meant to be
|
|
38
|
+
registered - most notably a test suite mirroring your handler layout - out of the auto-discovery.
|
|
39
|
+
|
|
40
|
+
Whatever you put here is **added to** the built-in list below, so you only name what is special about your project.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
QUEUEBIE_EXCLUDED_DIRECTORIES = {"vendor"}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A directory is skipped when any part of its path below the app root matches one of these names.
|
|
47
|
+
|
|
48
|
+
## QUEUEBIE_DEFAULT_EXCLUDED_DIRECTORIES
|
|
49
|
+
|
|
50
|
+
The built-in list of directory names which occur inside Django apps but never hold message handlers:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
QUEUEBIE_DEFAULT_EXCLUDED_DIRECTORIES = {
|
|
54
|
+
"__pycache__",
|
|
55
|
+
"fixtures",
|
|
56
|
+
"locale",
|
|
57
|
+
"media",
|
|
58
|
+
"migrations",
|
|
59
|
+
"node_modules",
|
|
60
|
+
"static",
|
|
61
|
+
"templates",
|
|
62
|
+
"tests",
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You can overwrite it, but you probably don't want to - use `QUEUEBIE_EXCLUDED_DIRECTORIES` to add names and leave
|
|
67
|
+
this one alone. Overwriting is the way to get one of these directories searched after all, for instance if your
|
|
68
|
+
handlers really do live under `tests/`. Note that an overwritten list is frozen: names a later queuebie release adds
|
|
69
|
+
to the built-in list won't reach your project.
|
|
70
|
+
|
|
71
|
+
## QUEUEBIE_STRICT_MODE
|
|
72
|
+
|
|
73
|
+
Queuebie enforces by default that commands are not used outside their scope and event handlers don't talk to the
|
|
74
|
+
database. If you want to skip that restriction for whatever reason, you can do so.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
QUEUEBIE_STRICT_MODE = False
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### What a scope is
|
|
81
|
+
|
|
82
|
+
The scope of a message or a handler is the package which owns the `handlers/` or `messages/` directory the module
|
|
83
|
+
lives in - the last such directory in the module path, so a package legitimately called `messages` further up doesn't
|
|
84
|
+
truncate the scope.
|
|
85
|
+
|
|
86
|
+
| Module | Scope |
|
|
87
|
+
|----------------------------------------------------|--------------------------|
|
|
88
|
+
| `apps.shipping.messages.commands.shipment` | `apps.shipping` |
|
|
89
|
+
| `apps.shipping.handlers.commands.shipment` | `apps.shipping` |
|
|
90
|
+
| `apps.logistics.billing.handlers.commands.invoice` | `apps.logistics.billing` |
|
|
91
|
+
|
|
92
|
+
A module which lives in neither directory has no owning package, so its full module path becomes its scope. Only a
|
|
93
|
+
handler in that very module shares it; registering one from anywhere else raises `RegisterOutOfScopeCommandError` at
|
|
94
|
+
import time. Keep your commands in a `messages/` directory.
|
|
95
|
+
|
|
96
|
+
Put them in a module *inside* that directory - `messages/commands/orders.py` - and not in `messages/__init__.py`
|
|
97
|
+
itself. A class defined there reports `apps.shipping.messages` as its module, which is indistinguishable from a
|
|
98
|
+
module called `messages.py`, so the last path segment is never treated as the marker and the scope ends up as
|
|
99
|
+
`apps.shipping.messages` rather than `apps.shipping`. Every handler in `apps/shipping/handlers/commands/` then fails
|
|
100
|
+
the strict-mode check.
|
|
101
|
+
|
|
102
|
+
A command handler may only handle commands of its own scope. For the common layout - one `handlers/` directory at the
|
|
103
|
+
root of a Django app - the scope is that app, so nothing changes. If you organise a Django app into sub-packages, the
|
|
104
|
+
boundary follows those sub-packages instead.
|
|
105
|
+
|
|
106
|
+
Event handlers are deliberately not scope-checked: crossing scopes is what events are for.
|
|
@@ -12,9 +12,9 @@ classifiers = [
|
|
|
12
12
|
"Development Status :: 4 - Beta",
|
|
13
13
|
"Environment :: Web Environment",
|
|
14
14
|
"Framework :: Django",
|
|
15
|
-
"Framework :: Django :: 4.2",
|
|
16
15
|
"Framework :: Django :: 5.2",
|
|
17
16
|
"Framework :: Django :: 6.0",
|
|
17
|
+
"Framework :: Django :: 6.1",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Natural Language :: English",
|
|
@@ -32,7 +32,7 @@ description = "A simple and synchronous message queue for commands and events fo
|
|
|
32
32
|
license = {"file" = "LICENSE.md"}
|
|
33
33
|
requires-python = ">=3.11"
|
|
34
34
|
dependencies = [
|
|
35
|
-
'Django>=
|
|
35
|
+
'Django>=5.2',
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
@@ -196,26 +196,28 @@ line-ending = "auto"
|
|
|
196
196
|
|
|
197
197
|
[tool.tox]
|
|
198
198
|
requires = ["tox>=4", "tox-uv>=1.0.0"]
|
|
199
|
-
env_list = ["
|
|
199
|
+
env_list = ["django52", "django60", "django61", ]
|
|
200
200
|
|
|
201
201
|
[tool.tox.env_run_base]
|
|
202
202
|
# Django deprecation overview: https://www.djangoproject.com/download/
|
|
203
203
|
package = "wheel"
|
|
204
204
|
wheel_build_env = ".pkg"
|
|
205
|
+
# This runner syncs from the lockfile and ignores "deps", so every environment below
|
|
206
|
+
# installs its matrix Django version on top of the synced environment.
|
|
205
207
|
runner = "uv-venv-lock-runner"
|
|
206
208
|
extras = ["dev", ]
|
|
207
209
|
commands = [
|
|
208
210
|
["pytest", "--cov=queuebie", "--cov-report=term", "--cov-report=xml", "--ds", "settings", "tests"]
|
|
209
211
|
]
|
|
210
212
|
|
|
211
|
-
[tool.tox.env.django42]
|
|
212
|
-
deps = ["Django==4.2.*"]
|
|
213
|
-
|
|
214
213
|
[tool.tox.env.django52]
|
|
215
|
-
|
|
214
|
+
commands_pre = [["uv", "pip", "install", "Django==5.2.*"]]
|
|
216
215
|
|
|
217
216
|
[tool.tox.env.django60]
|
|
218
|
-
|
|
217
|
+
commands_pre = [["uv", "pip", "install", "Django==6.0.*"]]
|
|
218
|
+
|
|
219
|
+
[tool.tox.env.django61]
|
|
220
|
+
commands_pre = [["uv", "pip", "install", "Django==6.1.*"]]
|
|
219
221
|
|
|
220
222
|
[tool.tox.gh_actions.python]
|
|
221
223
|
"3.11" = "py311"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
2
|
+
|
|
3
|
+
# Unpickling replays an exception's rendered message through "args", so every exception below accepts it
|
|
4
|
+
# positionally and renders a new one only when constructed without. Frameworks which ship exceptions
|
|
5
|
+
# between processes - Celery, a multiprocessing test runner - rely on that.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RegisterWrongMessageTypeError(TypeError):
|
|
9
|
+
def __init__(self, *args, message_name: str = "", decoratee_name: str = ""):
|
|
10
|
+
if not args:
|
|
11
|
+
args = (
|
|
12
|
+
f'Trying to register message function of wrong type: "{message_name}" on handler "{decoratee_name}".',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
super().__init__(*args)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RegisterOutOfScopeCommandError(TypeError):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*args,
|
|
22
|
+
message_name: str = "",
|
|
23
|
+
message_scope: str = "",
|
|
24
|
+
decoratee_name: str = "",
|
|
25
|
+
decoratee_scope: str = "",
|
|
26
|
+
):
|
|
27
|
+
if not args:
|
|
28
|
+
args = (
|
|
29
|
+
f'Command "{message_name}" (scope "{message_scope}") cannot be handled by '
|
|
30
|
+
f'"{decoratee_name}" (scope "{decoratee_scope}").',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
super().__init__(*args)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InvalidMessageTypeError(TypeError):
|
|
37
|
+
def __init__(self, *args, class_name: str = ""):
|
|
38
|
+
if not args:
|
|
39
|
+
args = (f'"{class_name}" is not an Event or Command',)
|
|
40
|
+
|
|
41
|
+
super().__init__(*args)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InvalidExcludedDirectoriesError(ImproperlyConfigured):
|
|
45
|
+
def __init__(self, *args, setting_name: str = "QUEUEBIE_EXCLUDED_DIRECTORIES"):
|
|
46
|
+
if not args:
|
|
47
|
+
args = (f"{setting_name} has to be a collection of directory names, not a string.",)
|
|
48
|
+
|
|
49
|
+
super().__init__(*args)
|
|
@@ -11,8 +11,15 @@ from django.core.cache import cache
|
|
|
11
11
|
from queuebie.exceptions import RegisterOutOfScopeCommandError, RegisterWrongMessageTypeError
|
|
12
12
|
from queuebie.logger import get_logger
|
|
13
13
|
from queuebie.messages import Command, Event
|
|
14
|
-
from queuebie.settings import
|
|
15
|
-
|
|
14
|
+
from queuebie.settings import (
|
|
15
|
+
get_queuebie_app_base_path,
|
|
16
|
+
get_queuebie_cache_key,
|
|
17
|
+
get_queuebie_excluded_directories,
|
|
18
|
+
get_queuebie_strict_mode,
|
|
19
|
+
)
|
|
20
|
+
from queuebie.utils import HANDLERS_DIRECTORY_NAME, is_same_scope, message_scope, unique_append_to_inner_list
|
|
21
|
+
|
|
22
|
+
MESSAGE_TYPE_DIRECTORY_NAMES = ("commands", "events")
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
@dataclasses.dataclass(kw_only=True)
|
|
@@ -44,8 +51,13 @@ class MessageRegistry:
|
|
|
44
51
|
if not (issubclass(command, Command)):
|
|
45
52
|
raise RegisterWrongMessageTypeError(message_name=command.__name__, decoratee_name=decoratee.__name__)
|
|
46
53
|
|
|
47
|
-
if get_queuebie_strict_mode() and not
|
|
48
|
-
raise RegisterOutOfScopeCommandError(
|
|
54
|
+
if get_queuebie_strict_mode() and not is_same_scope(function=decoratee, class_type=command):
|
|
55
|
+
raise RegisterOutOfScopeCommandError(
|
|
56
|
+
message_name=command.__name__,
|
|
57
|
+
message_scope=message_scope(module_path=command.__module__),
|
|
58
|
+
decoratee_name=decoratee.__name__,
|
|
59
|
+
decoratee_scope=message_scope(module_path=decoratee.__module__),
|
|
60
|
+
)
|
|
49
61
|
|
|
50
62
|
# Add decoratee to dependency list
|
|
51
63
|
function_definition = dataclasses.asdict(
|
|
@@ -86,7 +98,7 @@ class MessageRegistry:
|
|
|
86
98
|
|
|
87
99
|
return decorator
|
|
88
100
|
|
|
89
|
-
def autodiscover(self) -> None:
|
|
101
|
+
def autodiscover(self) -> None:
|
|
90
102
|
"""
|
|
91
103
|
Detects message registries which have been registered via the "register_*" decorator.
|
|
92
104
|
"""
|
|
@@ -101,6 +113,8 @@ class MessageRegistry:
|
|
|
101
113
|
project_path = get_queuebie_app_base_path()
|
|
102
114
|
logger = get_logger()
|
|
103
115
|
|
|
116
|
+
excluded_directories = get_queuebie_excluded_directories()
|
|
117
|
+
|
|
104
118
|
for app_config in apps.get_app_configs():
|
|
105
119
|
app_path = Path(app_config.path).resolve()
|
|
106
120
|
|
|
@@ -108,21 +122,23 @@ class MessageRegistry:
|
|
|
108
122
|
if project_path not in app_path.parents:
|
|
109
123
|
continue
|
|
110
124
|
|
|
111
|
-
for
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
for directory, directory_names, file_names in os.walk(app_path):
|
|
126
|
+
# Excluded directories are pruned from the walk so their subtrees are never visited
|
|
127
|
+
directory_names[:] = sorted(name for name in directory_names if name not in excluded_directories)
|
|
128
|
+
|
|
129
|
+
current_path = Path(directory)
|
|
130
|
+
if (
|
|
131
|
+
current_path.name not in MESSAGE_TYPE_DIRECTORY_NAMES
|
|
132
|
+
or current_path.parent.name != HANDLERS_DIRECTORY_NAME
|
|
133
|
+
):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
package_path = f"{app_config.name}.{'.'.join(current_path.relative_to(app_path).parts)}"
|
|
137
|
+
|
|
138
|
+
# Importing the package covers handlers registered in its "__init__.py"
|
|
139
|
+
self._import_handler_module(module_path=package_path)
|
|
140
|
+
for file_name in sorted(name for name in file_names if name.endswith(".py") and name != "__init__.py"):
|
|
141
|
+
self._import_handler_module(module_path=f"{package_path}.{Path(file_name).stem}")
|
|
126
142
|
|
|
127
143
|
# Log to shell which functions have been detected
|
|
128
144
|
logger.debug("Message autodiscovery running for commands...")
|
|
@@ -139,6 +155,18 @@ class MessageRegistry:
|
|
|
139
155
|
# Update cache
|
|
140
156
|
cache.set(get_queuebie_cache_key(), json.dumps({"commands": self.command_dict, "events": self.event_dict}))
|
|
141
157
|
|
|
158
|
+
def _import_handler_module(self, *, module_path: str) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Imports a module containing message handlers, reloading it if it was imported before.
|
|
161
|
+
"""
|
|
162
|
+
sys_module = sys.modules.get(module_path)
|
|
163
|
+
if sys_module:
|
|
164
|
+
importlib.reload(sys_module)
|
|
165
|
+
else:
|
|
166
|
+
importlib.import_module(module_path)
|
|
167
|
+
|
|
168
|
+
get_logger().debug('"%s" imported.', module_path)
|
|
169
|
+
|
|
142
170
|
def _load_handlers_from_cache(self) -> tuple[dict, dict]:
|
|
143
171
|
"""
|
|
144
172
|
Get registered handler definitions from Django cache
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
|
|
5
|
+
from queuebie.exceptions import InvalidExcludedDirectoriesError
|
|
6
|
+
|
|
7
|
+
DEFAULT_EXCLUDED_DIRECTORIES = {
|
|
8
|
+
"__pycache__",
|
|
9
|
+
"fixtures",
|
|
10
|
+
"locale",
|
|
11
|
+
"media",
|
|
12
|
+
"migrations",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"static",
|
|
15
|
+
"templates",
|
|
16
|
+
"tests",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_queuebie_app_base_path() -> Path | str:
|
|
21
|
+
"""
|
|
22
|
+
Base path of the application queuebie should look for registered handlers.
|
|
23
|
+
:return:
|
|
24
|
+
"""
|
|
25
|
+
return getattr(settings, "QUEUEBIE_APP_BASE_PATH", getattr(settings, "BASE_PATH", None))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_queuebie_cache_key() -> str:
|
|
29
|
+
"""
|
|
30
|
+
Cache key to store registered handlers in.
|
|
31
|
+
"""
|
|
32
|
+
return getattr(settings, "QUEUEBIE_CACHE_KEY", "queuebie")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_queuebie_logger_name() -> str:
|
|
36
|
+
"""
|
|
37
|
+
Django logger name
|
|
38
|
+
"""
|
|
39
|
+
return getattr(settings, "QUEUEBIE_LOGGER_NAME", "queuebie")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_queuebie_strict_mode() -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Determines if commands are allowed to be handled outside the scope they are defined in.
|
|
45
|
+
"""
|
|
46
|
+
return getattr(settings, "QUEUEBIE_STRICT_MODE", True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_queuebie_default_excluded_directories() -> set[str]:
|
|
50
|
+
"""
|
|
51
|
+
Directory names which occur inside Django apps but never hold message handlers.
|
|
52
|
+
"""
|
|
53
|
+
return _directory_names(setting_name="QUEUEBIE_DEFAULT_EXCLUDED_DIRECTORIES", default=DEFAULT_EXCLUDED_DIRECTORIES)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_queuebie_excluded_directories() -> set[str]:
|
|
57
|
+
"""
|
|
58
|
+
Directory names which are skipped when searching for handler modules.
|
|
59
|
+
"""
|
|
60
|
+
return get_queuebie_default_excluded_directories() | _directory_names(
|
|
61
|
+
setting_name="QUEUEBIE_EXCLUDED_DIRECTORIES", default=set()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _directory_names(*, setting_name: str, default: set[str]) -> set[str]:
|
|
66
|
+
directory_names = getattr(settings, setting_name, default)
|
|
67
|
+
|
|
68
|
+
# A string would decay into a set of single characters, silently excluding the wrong directories
|
|
69
|
+
if isinstance(directory_names, str):
|
|
70
|
+
raise InvalidExcludedDirectoriesError(setting_name=setting_name)
|
|
71
|
+
|
|
72
|
+
return set(directory_names)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
HANDLERS_DIRECTORY_NAME = "handlers"
|
|
4
|
+
MESSAGES_DIRECTORY_NAME = "messages"
|
|
5
|
+
SCOPE_MARKERS = (HANDLERS_DIRECTORY_NAME, MESSAGES_DIRECTORY_NAME)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def message_scope(*, module_path: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Determines the package owning the "handlers" or "messages" directory the given module lives in.
|
|
11
|
+
Falls back to the full module path for modules outside such a directory.
|
|
12
|
+
"""
|
|
13
|
+
parts = module_path.split(".")
|
|
14
|
+
# The last segment is the module itself, so a module called "messages.py" is not a marker
|
|
15
|
+
marker_indices = [index for index, part in enumerate(parts[:-1]) if part in SCOPE_MARKERS]
|
|
16
|
+
|
|
17
|
+
return ".".join(parts[: marker_indices[-1]]) if marker_indices else module_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_same_scope(*, function: Callable, class_type: type) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Checks if a class and the given function belong to the same scope.
|
|
23
|
+
"""
|
|
24
|
+
return message_scope(module_path=class_type.__module__) == message_scope(module_path=function.__module__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def unique_append_to_inner_list(*, data: dict, key: str | int, value) -> dict:
|
|
28
|
+
"""
|
|
29
|
+
Inserts "value" in the dictionary "data" on "key".
|
|
30
|
+
If "key" doesn't exist yet, it will create a new list containing "value".
|
|
31
|
+
If "value" at "key" already exists, it won't be appended.
|
|
32
|
+
"""
|
|
33
|
+
if key not in data:
|
|
34
|
+
data[key] = [value]
|
|
35
|
+
elif value not in data[key]:
|
|
36
|
+
data[key].append(value)
|
|
37
|
+
|
|
38
|
+
return data
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from queuebie import message_registry
|
|
2
|
+
from queuebie.logger import get_logger
|
|
3
|
+
from testapp.messages.commands.messages import SendMessage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@message_registry.register_command(command=SendMessage)
|
|
7
|
+
def handle_send_message(*, context: SendMessage) -> None:
|
|
8
|
+
logger = get_logger()
|
|
9
|
+
logger.info(f'Command "SendMessage" executed with text={context.text}.')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
from queuebie.messages import Command
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclasses.dataclass(kw_only=True)
|
|
7
|
+
class SendMessage(Command):
|
|
8
|
+
"""
|
|
9
|
+
Lives in a module called "messages" on purpose: the module name must not be mistaken for the
|
|
10
|
+
directory determining the scope.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
text: str
|