deegzlibs-command-bus 1.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.
- deegzlibs_command_bus-1.2.0/PKG-INFO +94 -0
- deegzlibs_command_bus-1.2.0/README.md +54 -0
- deegzlibs_command_bus-1.2.0/pyproject.toml +84 -0
- deegzlibs_command_bus-1.2.0/setup.cfg +4 -0
- deegzlibs_command_bus-1.2.0/setup.py +43 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/__init__.py +45 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/__init__.py +30 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/__init__.py +21 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/file.py +193 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/in_memory.py +59 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/rabbitmq.py +119 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/redis.py +67 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/sqs.py +45 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/__init__.py +13 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/file.py +129 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/in_memory.py +42 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/redis.py +56 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/bus.py +159 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/cli.py +345 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/command_bus_group.py +105 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/interfaces.py +111 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/parsers/__init__.py +23 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/parsers/base.py +17 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/parsers/base64_parser.py +50 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/parsers/json_parser.py +58 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/parsers/repr_parser.py +95 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/registry.py +163 -0
- deegzlibs_command_bus-1.2.0/src/command_bus/utils.py +45 -0
- deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/PKG-INFO +94 -0
- deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/SOURCES.txt +48 -0
- deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/dependency_links.txt +1 -0
- deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/entry_points.txt +2 -0
- deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/requires.txt +15 -0
- deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/top_level.txt +1 -0
- deegzlibs_command_bus-1.2.0/tests/test_base64_parser.py +82 -0
- deegzlibs_command_bus-1.2.0/tests/test_bus_event_decorator.py +85 -0
- deegzlibs_command_bus-1.2.0/tests/test_cli.py +109 -0
- deegzlibs_command_bus-1.2.0/tests/test_command_bus_group.py +90 -0
- deegzlibs_command_bus-1.2.0/tests/test_file_queue_adapter.py +398 -0
- deegzlibs_command_bus-1.2.0/tests/test_file_response_store.py +176 -0
- deegzlibs_command_bus-1.2.0/tests/test_handler_async.py +26 -0
- deegzlibs_command_bus-1.2.0/tests/test_in_memory_adapter.py +62 -0
- deegzlibs_command_bus-1.2.0/tests/test_interfaces.py +33 -0
- deegzlibs_command_bus-1.2.0/tests/test_json_parser.py +83 -0
- deegzlibs_command_bus-1.2.0/tests/test_parser.py +75 -0
- deegzlibs_command_bus-1.2.0/tests/test_rabbitmq_adapter.py +71 -0
- deegzlibs_command_bus-1.2.0/tests/test_redis_queue_adapter.py +65 -0
- deegzlibs_command_bus-1.2.0/tests/test_registry.py +150 -0
- deegzlibs_command_bus-1.2.0/tests/test_response_store.py +122 -0
- deegzlibs_command_bus-1.2.0/tests/test_sqs_adapter.py +92 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deegzlibs-command-bus
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: A small command bus with pluggable queue adapters and response stores (e.g. SQS, RabbitMQ, Redis, etc.)
|
|
5
|
+
Home-page: https://github.com/deegzlibs/deegzlibs
|
|
6
|
+
Author: Diego Alejos
|
|
7
|
+
Author-email: Diego Alejos <lego.admin@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/deegzlibs/deegzlibs-command-bus
|
|
10
|
+
Project-URL: Repository, https://github.com/deegzlibs/deegzlibs-command-bus
|
|
11
|
+
Project-URL: Issues, https://github.com/deegzlibs/deegzlibs-command-bus/issues
|
|
12
|
+
Keywords: command-bus,command-pattern,queue,sqs,rabbitmq,redis,async,pydantic
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: pydantic>=2.0
|
|
27
|
+
Provides-Extra: sqs
|
|
28
|
+
Requires-Dist: boto3; extra == "sqs"
|
|
29
|
+
Provides-Extra: rabbitmq
|
|
30
|
+
Requires-Dist: pika>=1.0; extra == "rabbitmq"
|
|
31
|
+
Provides-Extra: redis
|
|
32
|
+
Requires-Dist: redis>=4.0; extra == "redis"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
37
|
+
Dynamic: author
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
|
|
41
|
+
# DeegzLibs CommandBus (Python)
|
|
42
|
+
|
|
43
|
+
A small command bus with pluggable queue adapters. Define command messages as Pydantic models, register handlers, and execute commands in-process or via a queue (e.g. AWS SQS, RabbitMQ, Redis).
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install deegzlibs-command-bus
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Optional extras: **`[sqs]`**, **`[redis]`**, **`[rabbitmq]`**. See [Installation](docs/installation.md).
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from command_bus import CommandBus, CommandBusRouter, CommandMessage, CommandHandler
|
|
57
|
+
from command_bus.adapters import InMemoryCommandBusAdapter
|
|
58
|
+
|
|
59
|
+
router = CommandBusRouter()
|
|
60
|
+
adapter = InMemoryCommandBusAdapter(queue_name="commands")
|
|
61
|
+
bus = CommandBus(queue_adapter=adapter, command_router=router)
|
|
62
|
+
|
|
63
|
+
@router.command()
|
|
64
|
+
def on_order_created(order_id: str, amount_cents: int):
|
|
65
|
+
print(f"Order {order_id}: {amount_cents} cents")
|
|
66
|
+
|
|
67
|
+
# Fire-and-forget
|
|
68
|
+
await bus.execute(on_order_created(order_id="ord-1", amount_cents=1999), wait=False)
|
|
69
|
+
|
|
70
|
+
# Worker: poll and dispatch
|
|
71
|
+
await bus.work()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To run workers as **separate OS processes** (useful when handlers do a lot of CPU work), use the built-in CLI, e.g. **`command-bus-worker myapp.worker:bus --workers 4`** (omit **`:bus`** if your bus lives on attribute **`bus`**). For several queues in one supervised tree, point at a **`CommandBusGroup`**, e.g. **`myapp.worker:command_bus_group`** (see [Worker CLI](docs/cli.md)).
|
|
75
|
+
|
|
76
|
+
For full examples (messages, handlers, SQS/Redis, execute-and-wait), see the [documentation](docs/index.md).
|
|
77
|
+
|
|
78
|
+
## Documentation
|
|
79
|
+
|
|
80
|
+
| Topic | Description |
|
|
81
|
+
|-------|-------------|
|
|
82
|
+
| [Installation](docs/installation.md) | Package and extras. |
|
|
83
|
+
| [Quick start](docs/quickstart.md) | Messages, handlers, register, execute. |
|
|
84
|
+
| [Handler decorator](docs/handler-decorator.md) | `@router.command()` and message factory. |
|
|
85
|
+
| [Message formats and parsers](docs/message-formats-and-parsers.md) | Repr, JSON, Base64, custom parser. |
|
|
86
|
+
| [Client and worker](docs/client-and-worker.md) | Shared module, producer, consumer. |
|
|
87
|
+
| [Worker CLI](docs/cli.md) | `command-bus-worker module[:attr]` — `CommandBus` or `CommandBusGroup`, fork/spawn. |
|
|
88
|
+
| [Queue adapters](docs/queue-adapters.md) | In-memory, SQS, RabbitMQ, Redis. |
|
|
89
|
+
| [Execute and wait](docs/execute-and-wait.md) | Response store, request/response. |
|
|
90
|
+
| [API reference](docs/api-reference.md) | Types and methods overview. |
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# DeegzLibs CommandBus (Python)
|
|
2
|
+
|
|
3
|
+
A small command bus with pluggable queue adapters. Define command messages as Pydantic models, register handlers, and execute commands in-process or via a queue (e.g. AWS SQS, RabbitMQ, Redis).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install deegzlibs-command-bus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional extras: **`[sqs]`**, **`[redis]`**, **`[rabbitmq]`**. See [Installation](docs/installation.md).
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from command_bus import CommandBus, CommandBusRouter, CommandMessage, CommandHandler
|
|
17
|
+
from command_bus.adapters import InMemoryCommandBusAdapter
|
|
18
|
+
|
|
19
|
+
router = CommandBusRouter()
|
|
20
|
+
adapter = InMemoryCommandBusAdapter(queue_name="commands")
|
|
21
|
+
bus = CommandBus(queue_adapter=adapter, command_router=router)
|
|
22
|
+
|
|
23
|
+
@router.command()
|
|
24
|
+
def on_order_created(order_id: str, amount_cents: int):
|
|
25
|
+
print(f"Order {order_id}: {amount_cents} cents")
|
|
26
|
+
|
|
27
|
+
# Fire-and-forget
|
|
28
|
+
await bus.execute(on_order_created(order_id="ord-1", amount_cents=1999), wait=False)
|
|
29
|
+
|
|
30
|
+
# Worker: poll and dispatch
|
|
31
|
+
await bus.work()
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To run workers as **separate OS processes** (useful when handlers do a lot of CPU work), use the built-in CLI, e.g. **`command-bus-worker myapp.worker:bus --workers 4`** (omit **`:bus`** if your bus lives on attribute **`bus`**). For several queues in one supervised tree, point at a **`CommandBusGroup`**, e.g. **`myapp.worker:command_bus_group`** (see [Worker CLI](docs/cli.md)).
|
|
35
|
+
|
|
36
|
+
For full examples (messages, handlers, SQS/Redis, execute-and-wait), see the [documentation](docs/index.md).
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
| Topic | Description |
|
|
41
|
+
|-------|-------------|
|
|
42
|
+
| [Installation](docs/installation.md) | Package and extras. |
|
|
43
|
+
| [Quick start](docs/quickstart.md) | Messages, handlers, register, execute. |
|
|
44
|
+
| [Handler decorator](docs/handler-decorator.md) | `@router.command()` and message factory. |
|
|
45
|
+
| [Message formats and parsers](docs/message-formats-and-parsers.md) | Repr, JSON, Base64, custom parser. |
|
|
46
|
+
| [Client and worker](docs/client-and-worker.md) | Shared module, producer, consumer. |
|
|
47
|
+
| [Worker CLI](docs/cli.md) | `command-bus-worker module[:attr]` — `CommandBus` or `CommandBusGroup`, fork/spawn. |
|
|
48
|
+
| [Queue adapters](docs/queue-adapters.md) | In-memory, SQS, RabbitMQ, Redis. |
|
|
49
|
+
| [Execute and wait](docs/execute-and-wait.md) | Response store, request/response. |
|
|
50
|
+
| [API reference](docs/api-reference.md) | Types and methods overview. |
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "deegzlibs-command-bus"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "A small command bus with pluggable queue adapters (e.g. AWS SQS)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = ["Diego Alejos <lego.admin@gmail.com>"]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
packages = [{include = "command_bus", from = "src"}]
|
|
13
|
+
|
|
14
|
+
[tool.poetry.dependencies]
|
|
15
|
+
python = "^3.8"
|
|
16
|
+
pydantic = ">=2.0"
|
|
17
|
+
boto3 = {version = "*", optional = true}
|
|
18
|
+
pika = {version = ">=1.0", optional = true}
|
|
19
|
+
redis = {version = ">=4.0", optional = true}
|
|
20
|
+
|
|
21
|
+
[tool.poetry.group.dev.dependencies]
|
|
22
|
+
pytest = "^7.0.0"
|
|
23
|
+
pytest-cov = "^4.0.0"
|
|
24
|
+
pytest-asyncio = "^0.21.0"
|
|
25
|
+
commitizen = "^3.0.0"
|
|
26
|
+
|
|
27
|
+
[tool.poetry.extras]
|
|
28
|
+
sqs = ["boto3"]
|
|
29
|
+
rabbitmq = ["pika"]
|
|
30
|
+
redis = ["redis"]
|
|
31
|
+
|
|
32
|
+
[project]
|
|
33
|
+
name = "deegzlibs-command-bus"
|
|
34
|
+
version = "1.2.0"
|
|
35
|
+
description = "A small command bus with pluggable queue adapters and response stores (e.g. SQS, RabbitMQ, Redis, etc.)"
|
|
36
|
+
readme = "README.md"
|
|
37
|
+
requires-python = ">=3.8"
|
|
38
|
+
license = "MIT"
|
|
39
|
+
authors = [
|
|
40
|
+
{name = "Diego Alejos", email = "lego.admin@gmail.com"}
|
|
41
|
+
]
|
|
42
|
+
keywords = ["command-bus", "command-pattern", "queue", "sqs", "rabbitmq", "redis", "async", "pydantic"]
|
|
43
|
+
dependencies = [
|
|
44
|
+
"pydantic>=2.0",
|
|
45
|
+
]
|
|
46
|
+
classifiers = [
|
|
47
|
+
"Development Status :: 4 - Beta",
|
|
48
|
+
"Intended Audience :: Developers",
|
|
49
|
+
"Operating System :: OS Independent",
|
|
50
|
+
"Programming Language :: Python :: 3",
|
|
51
|
+
"Programming Language :: Python :: 3.8",
|
|
52
|
+
"Programming Language :: Python :: 3.9",
|
|
53
|
+
"Programming Language :: Python :: 3.10",
|
|
54
|
+
"Programming Language :: Python :: 3.11",
|
|
55
|
+
"Programming Language :: Python :: 3.12",
|
|
56
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
57
|
+
"Topic :: System :: Distributed Computing",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[project.scripts]
|
|
61
|
+
command-bus-worker = "command_bus.cli:main"
|
|
62
|
+
|
|
63
|
+
[project.urls]
|
|
64
|
+
Homepage = "https://github.com/deegzlibs/deegzlibs-command-bus"
|
|
65
|
+
Repository = "https://github.com/deegzlibs/deegzlibs-command-bus"
|
|
66
|
+
Issues = "https://github.com/deegzlibs/deegzlibs-command-bus/issues"
|
|
67
|
+
|
|
68
|
+
[project.optional-dependencies]
|
|
69
|
+
sqs = ["boto3"]
|
|
70
|
+
rabbitmq = ["pika>=1.0"]
|
|
71
|
+
redis = ["redis>=4.0"]
|
|
72
|
+
dev = ["pytest>=7.0.0", "pytest-cov>=4.0.0", "pytest-asyncio>=0.21.0"]
|
|
73
|
+
|
|
74
|
+
[tool.setuptools.packages.find]
|
|
75
|
+
where = ["src"]
|
|
76
|
+
|
|
77
|
+
[tool.commitizen]
|
|
78
|
+
name = "cz_conventional_commits"
|
|
79
|
+
version = "1.2.0"
|
|
80
|
+
tag_format = "v$version"
|
|
81
|
+
version_files = [
|
|
82
|
+
"pyproject.toml:version",
|
|
83
|
+
"setup.py:version",
|
|
84
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="deegzlibs-command-bus",
|
|
8
|
+
version="1.2.0",
|
|
9
|
+
author="Diego Alejos",
|
|
10
|
+
author_email="lego.admin@gmail.com",
|
|
11
|
+
description="A small command bus with pluggable queue adapters (e.g. AWS SQS)",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/deegzlibs/deegzlibs",
|
|
15
|
+
packages=find_packages(where="src"),
|
|
16
|
+
package_dir={"": "src"},
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
],
|
|
29
|
+
python_requires=">=3.8",
|
|
30
|
+
install_requires=[
|
|
31
|
+
"pydantic>=2.0",
|
|
32
|
+
],
|
|
33
|
+
extras_require={
|
|
34
|
+
"sqs": ["boto3"],
|
|
35
|
+
"rabbitmq": ["pika>=1.0"],
|
|
36
|
+
"redis": ["redis>=4.0"],
|
|
37
|
+
"dev": [
|
|
38
|
+
"pytest>=7.0.0",
|
|
39
|
+
"pytest-cov>=4.0.0",
|
|
40
|
+
"pytest-asyncio>=0.21.0",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeegzLibs CommandBus: a small command bus with pluggable queue adapters (e.g. SQS).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .bus import CommandBus
|
|
6
|
+
from .command_bus_group import CommandBusGroup, WorkerConfig, resolve_bus_attr_on_module
|
|
7
|
+
from .interfaces import (
|
|
8
|
+
CommandBusAdapter,
|
|
9
|
+
CommandBusInterface,
|
|
10
|
+
CommandBusRouterInterface,
|
|
11
|
+
CommandMessage,
|
|
12
|
+
CommandHandler,
|
|
13
|
+
ResponseStore,
|
|
14
|
+
TransmissibleBaseModel,
|
|
15
|
+
)
|
|
16
|
+
from .parsers import (
|
|
17
|
+
Base64MessageParser,
|
|
18
|
+
JsonMessageParser,
|
|
19
|
+
MessageParser,
|
|
20
|
+
MessageParserBase,
|
|
21
|
+
ReprMessageParser,
|
|
22
|
+
)
|
|
23
|
+
from .registry import CommandBusRouter, CommandBusRouterEntry, get_qual_name
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Base64MessageParser",
|
|
27
|
+
"CommandBus",
|
|
28
|
+
"CommandBusGroup",
|
|
29
|
+
"CommandBusAdapter",
|
|
30
|
+
"ResponseStore",
|
|
31
|
+
"CommandBusInterface",
|
|
32
|
+
"CommandBusRouter",
|
|
33
|
+
"CommandBusRouterEntry",
|
|
34
|
+
"CommandBusRouterInterface",
|
|
35
|
+
"CommandMessage",
|
|
36
|
+
"CommandHandler",
|
|
37
|
+
"JsonMessageParser",
|
|
38
|
+
"MessageParser",
|
|
39
|
+
"MessageParserBase",
|
|
40
|
+
"ReprMessageParser",
|
|
41
|
+
"TransmissibleBaseModel",
|
|
42
|
+
"WorkerConfig",
|
|
43
|
+
"resolve_bus_attr_on_module",
|
|
44
|
+
"get_qual_name",
|
|
45
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Queue and response adapters for the command bus."""
|
|
2
|
+
|
|
3
|
+
from .queue import FileQueueAdapter, InMemoryCommandBusAdapter, SqsCommandBusAdapter
|
|
4
|
+
|
|
5
|
+
__all__ = ["InMemoryCommandBusAdapter", "SqsCommandBusAdapter", "FileQueueAdapter"]
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from .queue import RabbitMqCommandBusAdapter
|
|
9
|
+
|
|
10
|
+
__all__ += ["RabbitMqCommandBusAdapter"]
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from .queue import RedisCommandBusAdapter
|
|
16
|
+
|
|
17
|
+
__all__ += ["RedisCommandBusAdapter"]
|
|
18
|
+
except ImportError:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
from .response import FileResponseStore, InMemoryResponseStore
|
|
22
|
+
|
|
23
|
+
__all__ += ["InMemoryResponseStore", "FileResponseStore"]
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from .response import RedisResponseStore
|
|
27
|
+
|
|
28
|
+
__all__ += ["RedisResponseStore"]
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Queue adapters for the command bus (SQS, RabbitMQ, Redis, in-memory, file, etc.)."""
|
|
2
|
+
|
|
3
|
+
from .file import FileQueueAdapter
|
|
4
|
+
from .in_memory import InMemoryCommandBusAdapter
|
|
5
|
+
from .sqs import SqsCommandBusAdapter
|
|
6
|
+
|
|
7
|
+
__all__ = ["InMemoryCommandBusAdapter", "SqsCommandBusAdapter", "FileQueueAdapter"]
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from .rabbitmq import RabbitMqCommandBusAdapter
|
|
11
|
+
|
|
12
|
+
__all__ += ["RabbitMqCommandBusAdapter"]
|
|
13
|
+
except ImportError:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from .redis import RedisCommandBusAdapter
|
|
18
|
+
|
|
19
|
+
__all__ += ["RedisCommandBusAdapter"]
|
|
20
|
+
except ImportError:
|
|
21
|
+
pass
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""File-based queue adapter for persistent cross-process command queuing."""
|
|
2
|
+
|
|
3
|
+
import fcntl
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from ...interfaces import CommandBusAdapter, CommandMessage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileQueueAdapter(CommandBusAdapter):
|
|
14
|
+
"""File-based queue adapter that persists to disk for cross-process access."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
queue_name: str = "events",
|
|
19
|
+
storage_file: Path = None,
|
|
20
|
+
default_visibility_timeout: int = 60
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Initialize file-based queue adapter.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
queue_name: Name of the queue
|
|
27
|
+
storage_file: Path to JSON file for storage. Defaults to ~/.qubot/queue.json
|
|
28
|
+
default_visibility_timeout: Default visibility timeout in seconds (how long a message
|
|
29
|
+
is hidden after being retrieved). Defaults to 60 seconds.
|
|
30
|
+
"""
|
|
31
|
+
self.queue_name = queue_name
|
|
32
|
+
self.default_visibility_timeout = default_visibility_timeout
|
|
33
|
+
|
|
34
|
+
if storage_file is None:
|
|
35
|
+
storage_dir = Path.home() / ".qubot"
|
|
36
|
+
storage_dir.mkdir(exist_ok=True)
|
|
37
|
+
storage_file = storage_dir / "queue.json"
|
|
38
|
+
|
|
39
|
+
self.storage_file = Path(storage_file)
|
|
40
|
+
self._lock_file = self.storage_file.with_suffix('.lock')
|
|
41
|
+
self._lock = None
|
|
42
|
+
|
|
43
|
+
def _acquire_lock(self):
|
|
44
|
+
"""Simple file-based lock."""
|
|
45
|
+
self._lock = open(self._lock_file, 'w', encoding='utf-8')
|
|
46
|
+
fcntl.flock(self._lock.fileno(), fcntl.LOCK_EX)
|
|
47
|
+
|
|
48
|
+
def _release_lock(self):
|
|
49
|
+
"""Release file-based lock."""
|
|
50
|
+
if self._lock is not None:
|
|
51
|
+
try:
|
|
52
|
+
fcntl.flock(self._lock.fileno(), fcntl.LOCK_UN)
|
|
53
|
+
except (OSError, ValueError):
|
|
54
|
+
pass
|
|
55
|
+
finally:
|
|
56
|
+
self._lock.close()
|
|
57
|
+
self._lock = None
|
|
58
|
+
|
|
59
|
+
def _load_queue(self) -> List[Dict[str, Any]]:
|
|
60
|
+
"""Load queue from disk."""
|
|
61
|
+
if not self.storage_file.exists():
|
|
62
|
+
return []
|
|
63
|
+
try:
|
|
64
|
+
with open(self.storage_file, 'r', encoding='utf-8') as f:
|
|
65
|
+
return json.load(f)
|
|
66
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
def _save_queue(self, queue: List[Dict[str, Any]]) -> None:
|
|
70
|
+
"""Save queue to disk."""
|
|
71
|
+
try:
|
|
72
|
+
# Create parent directory if it doesn't exist
|
|
73
|
+
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
with open(self.storage_file, 'w', encoding='utf-8') as f:
|
|
75
|
+
json.dump(queue, f, indent=2)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise RuntimeError(f"Failed to save queue: {e}") from e
|
|
78
|
+
|
|
79
|
+
def enqueue(self, message_instance: CommandMessage, delay_seconds: int = 0) -> None:
|
|
80
|
+
"""Add a message to the queue."""
|
|
81
|
+
try:
|
|
82
|
+
self._acquire_lock()
|
|
83
|
+
queue = self._load_queue()
|
|
84
|
+
|
|
85
|
+
# Serialize the message with full module path
|
|
86
|
+
# Format: module.ClassName(arg1=val1, arg2=val2, ...)
|
|
87
|
+
class_name = message_instance.__class__.__name__
|
|
88
|
+
module_name = message_instance.__class__.__module__
|
|
89
|
+
full_class_path = f"{module_name}.{class_name}"
|
|
90
|
+
|
|
91
|
+
# Get the arguments from the message instance
|
|
92
|
+
if hasattr(message_instance, 'model_dump'):
|
|
93
|
+
args_dict = message_instance.model_dump()
|
|
94
|
+
else:
|
|
95
|
+
args_dict = message_instance.__dict__
|
|
96
|
+
|
|
97
|
+
args_str = ", ".join(f"{k}={repr(v)}" for k, v in args_dict.items())
|
|
98
|
+
message_str = f"{full_class_path}({args_str})"
|
|
99
|
+
|
|
100
|
+
queue.append({
|
|
101
|
+
"id": str(uuid.uuid4()),
|
|
102
|
+
"message": message_str,
|
|
103
|
+
"delay_until": time.time() + delay_seconds,
|
|
104
|
+
"queue_name": self.queue_name,
|
|
105
|
+
"hidden_until": 0 # Initially visible
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
self._save_queue(queue)
|
|
109
|
+
finally:
|
|
110
|
+
self._release_lock()
|
|
111
|
+
|
|
112
|
+
def dequeue(self, message_instance: Any) -> None:
|
|
113
|
+
"""Remove a message from the queue."""
|
|
114
|
+
try:
|
|
115
|
+
self._acquire_lock()
|
|
116
|
+
queue = self._load_queue()
|
|
117
|
+
|
|
118
|
+
# Find and remove the message by ID (more reliable than message string)
|
|
119
|
+
msg_id = None
|
|
120
|
+
if hasattr(message_instance, '_data') and isinstance(message_instance._data, dict):
|
|
121
|
+
msg_id = message_instance._data.get("id") # noqa: SLF001
|
|
122
|
+
|
|
123
|
+
if msg_id:
|
|
124
|
+
queue[:] = [msg for msg in queue if msg.get("id") != msg_id]
|
|
125
|
+
else:
|
|
126
|
+
# Fallback to message string matching
|
|
127
|
+
message_str = message_instance.body if hasattr(message_instance, 'body') else str(message_instance)
|
|
128
|
+
queue[:] = [msg for msg in queue if msg.get("message") != message_str]
|
|
129
|
+
|
|
130
|
+
self._save_queue(queue)
|
|
131
|
+
finally:
|
|
132
|
+
self._release_lock()
|
|
133
|
+
|
|
134
|
+
def get_messages(
|
|
135
|
+
self,
|
|
136
|
+
max_messages: int = 1,
|
|
137
|
+
wait_seconds: int = 0, # noqa: ARG002
|
|
138
|
+
visibility_timeout: int = None,
|
|
139
|
+
**kwargs: Any
|
|
140
|
+
) -> List[Any]:
|
|
141
|
+
"""
|
|
142
|
+
Get messages from the queue.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
max_messages: Maximum number of messages to retrieve
|
|
146
|
+
wait_seconds: Not used (file-based queue doesn't support blocking)
|
|
147
|
+
visibility_timeout: How long (in seconds) to hide the message after retrieval.
|
|
148
|
+
If None, uses the adapter's default_visibility_timeout.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
self._acquire_lock()
|
|
152
|
+
queue = self._load_queue()
|
|
153
|
+
current_time = time.time()
|
|
154
|
+
|
|
155
|
+
# Use provided visibility timeout or default
|
|
156
|
+
vis_timeout = visibility_timeout if visibility_timeout is not None else self.default_visibility_timeout
|
|
157
|
+
|
|
158
|
+
# Filter messages for this queue that are:
|
|
159
|
+
# 1. For this queue name
|
|
160
|
+
# 2. Delay has passed (delay_until <= current_time)
|
|
161
|
+
# 3. Not currently hidden (hidden_until <= current_time)
|
|
162
|
+
visible_messages = [
|
|
163
|
+
msg for msg in queue
|
|
164
|
+
if msg.get("queue_name") == self.queue_name
|
|
165
|
+
and msg.get("delay_until", 0) <= current_time
|
|
166
|
+
and msg.get("hidden_until", 0) <= current_time
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# Limit to max_messages
|
|
170
|
+
messages_to_return = visible_messages[:max_messages]
|
|
171
|
+
|
|
172
|
+
# Update hidden_until for returned messages (only if visibility_timeout > 0)
|
|
173
|
+
if vis_timeout > 0:
|
|
174
|
+
for msg in messages_to_return:
|
|
175
|
+
msg["hidden_until"] = current_time + vis_timeout
|
|
176
|
+
|
|
177
|
+
# Save the updated queue with new hidden_until values
|
|
178
|
+
if messages_to_return:
|
|
179
|
+
self._save_queue(queue)
|
|
180
|
+
|
|
181
|
+
# Return as message objects (the bus will parse them)
|
|
182
|
+
class QueueMessage:
|
|
183
|
+
def __init__(self, msg_data: Dict[str, Any]):
|
|
184
|
+
self.body = msg_data["message"]
|
|
185
|
+
self._data = msg_data
|
|
186
|
+
|
|
187
|
+
def __repr__(self):
|
|
188
|
+
return self.body
|
|
189
|
+
|
|
190
|
+
return [QueueMessage(msg) for msg in messages_to_return]
|
|
191
|
+
finally:
|
|
192
|
+
self._release_lock()
|
|
193
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""In-memory command bus adapter for tests or in-process use."""
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from typing import Any, List
|
|
5
|
+
|
|
6
|
+
from ...interfaces import CommandBusAdapter, CommandMessage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _InMemoryMessage:
|
|
10
|
+
"""Message wrapper with .body and .delete() for bus compatibility."""
|
|
11
|
+
|
|
12
|
+
__slots__ = ("body",)
|
|
13
|
+
|
|
14
|
+
def __init__(self, body: str) -> None:
|
|
15
|
+
self.body = body
|
|
16
|
+
|
|
17
|
+
def delete(self) -> None:
|
|
18
|
+
"""No-op: message already removed from queue when get_messages() popped it."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InMemoryCommandBusAdapter(CommandBusAdapter):
|
|
23
|
+
"""
|
|
24
|
+
In-memory FIFO queue adapter. Useful for tests or single-process command dispatch.
|
|
25
|
+
delay_seconds is ignored (no native delay); messages are available immediately.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, queue_name: str = "default") -> None:
|
|
29
|
+
self.queue_name = queue_name
|
|
30
|
+
self._queue: deque[str] = deque()
|
|
31
|
+
|
|
32
|
+
def enqueue(
|
|
33
|
+
self,
|
|
34
|
+
message_instance: CommandMessage,
|
|
35
|
+
delay_seconds: int = 0,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Append message to the queue. delay_seconds is ignored."""
|
|
38
|
+
self._queue.append(str(message_instance))
|
|
39
|
+
|
|
40
|
+
def dequeue(self, message_instance: Any) -> None:
|
|
41
|
+
"""No-op: message was already removed when get_messages() popped it."""
|
|
42
|
+
if hasattr(message_instance, "delete"):
|
|
43
|
+
message_instance.delete()
|
|
44
|
+
|
|
45
|
+
def get_messages(
|
|
46
|
+
self,
|
|
47
|
+
max_messages: int = 1,
|
|
48
|
+
wait_seconds: int = 0,
|
|
49
|
+
**kwargs: Any,
|
|
50
|
+
) -> List[_InMemoryMessage]:
|
|
51
|
+
"""Pop up to max_messages from the queue. wait_seconds is ignored (no blocking)."""
|
|
52
|
+
out: List[_InMemoryMessage] = []
|
|
53
|
+
for _ in range(max_messages):
|
|
54
|
+
try:
|
|
55
|
+
body = self._queue.popleft()
|
|
56
|
+
except IndexError:
|
|
57
|
+
break
|
|
58
|
+
out.append(_InMemoryMessage(body=body))
|
|
59
|
+
return out
|