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.
Files changed (50) hide show
  1. deegzlibs_command_bus-1.2.0/PKG-INFO +94 -0
  2. deegzlibs_command_bus-1.2.0/README.md +54 -0
  3. deegzlibs_command_bus-1.2.0/pyproject.toml +84 -0
  4. deegzlibs_command_bus-1.2.0/setup.cfg +4 -0
  5. deegzlibs_command_bus-1.2.0/setup.py +43 -0
  6. deegzlibs_command_bus-1.2.0/src/command_bus/__init__.py +45 -0
  7. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/__init__.py +30 -0
  8. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/__init__.py +21 -0
  9. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/file.py +193 -0
  10. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/in_memory.py +59 -0
  11. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/rabbitmq.py +119 -0
  12. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/redis.py +67 -0
  13. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/queue/sqs.py +45 -0
  14. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/__init__.py +13 -0
  15. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/file.py +129 -0
  16. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/in_memory.py +42 -0
  17. deegzlibs_command_bus-1.2.0/src/command_bus/adapters/response/redis.py +56 -0
  18. deegzlibs_command_bus-1.2.0/src/command_bus/bus.py +159 -0
  19. deegzlibs_command_bus-1.2.0/src/command_bus/cli.py +345 -0
  20. deegzlibs_command_bus-1.2.0/src/command_bus/command_bus_group.py +105 -0
  21. deegzlibs_command_bus-1.2.0/src/command_bus/interfaces.py +111 -0
  22. deegzlibs_command_bus-1.2.0/src/command_bus/parsers/__init__.py +23 -0
  23. deegzlibs_command_bus-1.2.0/src/command_bus/parsers/base.py +17 -0
  24. deegzlibs_command_bus-1.2.0/src/command_bus/parsers/base64_parser.py +50 -0
  25. deegzlibs_command_bus-1.2.0/src/command_bus/parsers/json_parser.py +58 -0
  26. deegzlibs_command_bus-1.2.0/src/command_bus/parsers/repr_parser.py +95 -0
  27. deegzlibs_command_bus-1.2.0/src/command_bus/registry.py +163 -0
  28. deegzlibs_command_bus-1.2.0/src/command_bus/utils.py +45 -0
  29. deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/PKG-INFO +94 -0
  30. deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/SOURCES.txt +48 -0
  31. deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/dependency_links.txt +1 -0
  32. deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/entry_points.txt +2 -0
  33. deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/requires.txt +15 -0
  34. deegzlibs_command_bus-1.2.0/src/deegzlibs_command_bus.egg-info/top_level.txt +1 -0
  35. deegzlibs_command_bus-1.2.0/tests/test_base64_parser.py +82 -0
  36. deegzlibs_command_bus-1.2.0/tests/test_bus_event_decorator.py +85 -0
  37. deegzlibs_command_bus-1.2.0/tests/test_cli.py +109 -0
  38. deegzlibs_command_bus-1.2.0/tests/test_command_bus_group.py +90 -0
  39. deegzlibs_command_bus-1.2.0/tests/test_file_queue_adapter.py +398 -0
  40. deegzlibs_command_bus-1.2.0/tests/test_file_response_store.py +176 -0
  41. deegzlibs_command_bus-1.2.0/tests/test_handler_async.py +26 -0
  42. deegzlibs_command_bus-1.2.0/tests/test_in_memory_adapter.py +62 -0
  43. deegzlibs_command_bus-1.2.0/tests/test_interfaces.py +33 -0
  44. deegzlibs_command_bus-1.2.0/tests/test_json_parser.py +83 -0
  45. deegzlibs_command_bus-1.2.0/tests/test_parser.py +75 -0
  46. deegzlibs_command_bus-1.2.0/tests/test_rabbitmq_adapter.py +71 -0
  47. deegzlibs_command_bus-1.2.0/tests/test_redis_queue_adapter.py +65 -0
  48. deegzlibs_command_bus-1.2.0/tests/test_registry.py +150 -0
  49. deegzlibs_command_bus-1.2.0/tests/test_response_store.py +122 -0
  50. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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