quantum-framework 0.9.0__py3-none-any.whl
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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-Memory Message Broker Adapter
|
|
3
|
+
|
|
4
|
+
Provides a simple in-memory implementation of the MessageBroker interface
|
|
5
|
+
for testing and development purposes.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Topic-based pub/sub with pattern matching
|
|
9
|
+
- Queue-based point-to-point messaging
|
|
10
|
+
- Request/reply pattern support
|
|
11
|
+
- Message acknowledgment (simulated)
|
|
12
|
+
- No external dependencies
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
import queue
|
|
17
|
+
import fnmatch
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# Support both direct execution and import from parent
|
|
28
|
+
try:
|
|
29
|
+
from quantum.runtime.message_broker import (
|
|
30
|
+
MessageBroker, Message, QueueInfo, MessageHandler,
|
|
31
|
+
MessageBrokerError, ConnectionError, PublishError,
|
|
32
|
+
SubscribeError, QueueError
|
|
33
|
+
)
|
|
34
|
+
except ImportError:
|
|
35
|
+
from message_broker import (
|
|
36
|
+
MessageBroker, Message, QueueInfo, MessageHandler,
|
|
37
|
+
MessageBrokerError, ConnectionError, PublishError,
|
|
38
|
+
SubscribeError, QueueError
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class QueueState:
|
|
44
|
+
"""Internal state for a queue"""
|
|
45
|
+
messages: queue.Queue = field(default_factory=queue.Queue)
|
|
46
|
+
durable: bool = True
|
|
47
|
+
exclusive: bool = False
|
|
48
|
+
auto_delete: bool = False
|
|
49
|
+
dead_letter_queue: Optional[str] = None
|
|
50
|
+
ttl: Optional[int] = None
|
|
51
|
+
consumers: List[str] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Subscription:
|
|
56
|
+
"""Internal state for a subscription"""
|
|
57
|
+
id: str
|
|
58
|
+
pattern: str # topic pattern or queue name
|
|
59
|
+
handler: MessageHandler
|
|
60
|
+
is_queue: bool = False
|
|
61
|
+
prefetch: int = 1
|
|
62
|
+
active: bool = True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MemoryAdapter(MessageBroker):
|
|
66
|
+
"""
|
|
67
|
+
In-memory implementation of MessageBroker.
|
|
68
|
+
|
|
69
|
+
Suitable for:
|
|
70
|
+
- Unit testing
|
|
71
|
+
- Development environments
|
|
72
|
+
- Single-process applications
|
|
73
|
+
- Prototyping
|
|
74
|
+
|
|
75
|
+
Limitations:
|
|
76
|
+
- Messages are lost on process restart
|
|
77
|
+
- No persistence
|
|
78
|
+
- Single process only (no IPC)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self):
|
|
82
|
+
self._connected = False
|
|
83
|
+
self._lock = threading.RLock()
|
|
84
|
+
|
|
85
|
+
# Queues: name -> QueueState
|
|
86
|
+
self._queues: Dict[str, QueueState] = {}
|
|
87
|
+
|
|
88
|
+
# Topic subscriptions: subscription_id -> Subscription
|
|
89
|
+
self._subscriptions: Dict[str, Subscription] = {}
|
|
90
|
+
|
|
91
|
+
# Pending acknowledgments: message_id -> Message
|
|
92
|
+
self._pending_acks: Dict[str, Message] = {}
|
|
93
|
+
|
|
94
|
+
# Reply queues for request/reply pattern
|
|
95
|
+
self._reply_queues: Dict[str, queue.Queue] = {}
|
|
96
|
+
|
|
97
|
+
# Background worker threads
|
|
98
|
+
self._workers: List[threading.Thread] = []
|
|
99
|
+
self._shutdown = threading.Event()
|
|
100
|
+
|
|
101
|
+
def connect(self, config: Dict[str, Any] = None) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Initialize the in-memory broker.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Optional configuration (ignored for memory adapter)
|
|
107
|
+
"""
|
|
108
|
+
with self._lock:
|
|
109
|
+
if self._connected:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
self._connected = True
|
|
113
|
+
self._shutdown.clear()
|
|
114
|
+
|
|
115
|
+
# Start background worker for queue consumption
|
|
116
|
+
worker = threading.Thread(
|
|
117
|
+
target=self._consume_worker,
|
|
118
|
+
daemon=True,
|
|
119
|
+
name="MemoryBroker-Worker"
|
|
120
|
+
)
|
|
121
|
+
worker.start()
|
|
122
|
+
self._workers.append(worker)
|
|
123
|
+
|
|
124
|
+
def disconnect(self) -> None:
|
|
125
|
+
"""Shutdown the in-memory broker and clean up resources."""
|
|
126
|
+
with self._lock:
|
|
127
|
+
if not self._connected:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Signal workers to stop
|
|
131
|
+
self._shutdown.set()
|
|
132
|
+
|
|
133
|
+
# Wait for workers to finish
|
|
134
|
+
for worker in self._workers:
|
|
135
|
+
worker.join(timeout=1.0)
|
|
136
|
+
|
|
137
|
+
self._workers.clear()
|
|
138
|
+
|
|
139
|
+
# Clear all state
|
|
140
|
+
self._queues.clear()
|
|
141
|
+
self._subscriptions.clear()
|
|
142
|
+
self._pending_acks.clear()
|
|
143
|
+
self._reply_queues.clear()
|
|
144
|
+
|
|
145
|
+
self._connected = False
|
|
146
|
+
|
|
147
|
+
def is_connected(self) -> bool:
|
|
148
|
+
"""Check if broker is initialized."""
|
|
149
|
+
return self._connected
|
|
150
|
+
|
|
151
|
+
def publish(self, topic: str, message: Message) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Publish a message to a topic.
|
|
154
|
+
|
|
155
|
+
All matching subscriptions will receive the message.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
topic: Topic name
|
|
159
|
+
message: Message to publish
|
|
160
|
+
"""
|
|
161
|
+
if not self._connected:
|
|
162
|
+
raise ConnectionError("Broker not connected")
|
|
163
|
+
|
|
164
|
+
message.topic = topic
|
|
165
|
+
|
|
166
|
+
with self._lock:
|
|
167
|
+
# Find all matching subscriptions
|
|
168
|
+
for sub in self._subscriptions.values():
|
|
169
|
+
if sub.active and not sub.is_queue:
|
|
170
|
+
if self._match_topic(topic, sub.pattern):
|
|
171
|
+
try:
|
|
172
|
+
# Create a copy of the message for each subscriber
|
|
173
|
+
sub.handler(Message.from_dict(message.to_dict()))
|
|
174
|
+
except Exception as e:
|
|
175
|
+
# Log error but don't fail publish
|
|
176
|
+
print(f"[MemoryBroker] Handler error: {e}")
|
|
177
|
+
|
|
178
|
+
def send(self, queue_name: str, message: Message) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Send a message to a queue.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
queue_name: Queue name
|
|
184
|
+
message: Message to send
|
|
185
|
+
"""
|
|
186
|
+
if not self._connected:
|
|
187
|
+
raise ConnectionError("Broker not connected")
|
|
188
|
+
|
|
189
|
+
message.queue = queue_name
|
|
190
|
+
|
|
191
|
+
with self._lock:
|
|
192
|
+
# Ensure queue exists
|
|
193
|
+
if queue_name not in self._queues:
|
|
194
|
+
self._queues[queue_name] = QueueState()
|
|
195
|
+
|
|
196
|
+
q = self._queues[queue_name]
|
|
197
|
+
|
|
198
|
+
# Check TTL
|
|
199
|
+
if q.ttl is not None:
|
|
200
|
+
message.headers['_expires'] = str(time.time() * 1000 + q.ttl)
|
|
201
|
+
|
|
202
|
+
q.messages.put(message)
|
|
203
|
+
|
|
204
|
+
def request(self, queue_name: str, message: Message, timeout: int = 5000) -> Message:
|
|
205
|
+
"""
|
|
206
|
+
Send a request and wait for a reply.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
queue_name: Queue to send request to
|
|
210
|
+
message: Request message
|
|
211
|
+
timeout: Timeout in milliseconds
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Response message
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
TimeoutError: If no response within timeout
|
|
218
|
+
"""
|
|
219
|
+
if not self._connected:
|
|
220
|
+
raise ConnectionError("Broker not connected")
|
|
221
|
+
|
|
222
|
+
# Create unique reply queue
|
|
223
|
+
reply_queue_name = f"_reply_{uuid.uuid4().hex}"
|
|
224
|
+
correlation_id = str(uuid.uuid4())
|
|
225
|
+
|
|
226
|
+
# Set up reply queue
|
|
227
|
+
reply_queue = queue.Queue()
|
|
228
|
+
with self._lock:
|
|
229
|
+
self._reply_queues[reply_queue_name] = reply_queue
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
# Set message properties for reply
|
|
233
|
+
message.reply_to = reply_queue_name
|
|
234
|
+
message.correlation_id = correlation_id
|
|
235
|
+
|
|
236
|
+
# Send request
|
|
237
|
+
self.send(queue_name, message)
|
|
238
|
+
|
|
239
|
+
# Wait for reply
|
|
240
|
+
try:
|
|
241
|
+
reply = reply_queue.get(timeout=timeout / 1000.0)
|
|
242
|
+
return reply
|
|
243
|
+
except queue.Empty:
|
|
244
|
+
raise TimeoutError(f"Request timed out after {timeout}ms")
|
|
245
|
+
|
|
246
|
+
finally:
|
|
247
|
+
# Clean up reply queue
|
|
248
|
+
with self._lock:
|
|
249
|
+
self._reply_queues.pop(reply_queue_name, None)
|
|
250
|
+
|
|
251
|
+
def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Subscribe to a topic pattern.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
topic: Topic pattern (supports * and # wildcards)
|
|
257
|
+
handler: Callback for received messages
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Subscription ID
|
|
261
|
+
"""
|
|
262
|
+
if not self._connected:
|
|
263
|
+
raise ConnectionError("Broker not connected")
|
|
264
|
+
|
|
265
|
+
sub_id = f"sub_{uuid.uuid4().hex}"
|
|
266
|
+
|
|
267
|
+
with self._lock:
|
|
268
|
+
self._subscriptions[sub_id] = Subscription(
|
|
269
|
+
id=sub_id,
|
|
270
|
+
pattern=topic,
|
|
271
|
+
handler=handler,
|
|
272
|
+
is_queue=False
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return sub_id
|
|
276
|
+
|
|
277
|
+
def consume(self, queue_name: str, handler: MessageHandler, prefetch: int = 1) -> str:
|
|
278
|
+
"""
|
|
279
|
+
Start consuming messages from a queue.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
queue_name: Queue name
|
|
283
|
+
handler: Callback for received messages
|
|
284
|
+
prefetch: Number of messages to prefetch
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Consumer ID
|
|
288
|
+
"""
|
|
289
|
+
if not self._connected:
|
|
290
|
+
raise ConnectionError("Broker not connected")
|
|
291
|
+
|
|
292
|
+
consumer_id = f"consumer_{uuid.uuid4().hex}"
|
|
293
|
+
|
|
294
|
+
with self._lock:
|
|
295
|
+
# Ensure queue exists
|
|
296
|
+
if queue_name not in self._queues:
|
|
297
|
+
self._queues[queue_name] = QueueState()
|
|
298
|
+
|
|
299
|
+
self._queues[queue_name].consumers.append(consumer_id)
|
|
300
|
+
|
|
301
|
+
self._subscriptions[consumer_id] = Subscription(
|
|
302
|
+
id=consumer_id,
|
|
303
|
+
pattern=queue_name,
|
|
304
|
+
handler=handler,
|
|
305
|
+
is_queue=True,
|
|
306
|
+
prefetch=prefetch
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return consumer_id
|
|
310
|
+
|
|
311
|
+
def unsubscribe(self, subscription_id: str) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Unsubscribe from a topic or stop consuming.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
subscription_id: Subscription or consumer ID
|
|
317
|
+
"""
|
|
318
|
+
with self._lock:
|
|
319
|
+
if subscription_id in self._subscriptions:
|
|
320
|
+
sub = self._subscriptions[subscription_id]
|
|
321
|
+
sub.active = False
|
|
322
|
+
|
|
323
|
+
# Remove from queue consumers if applicable
|
|
324
|
+
if sub.is_queue and sub.pattern in self._queues:
|
|
325
|
+
consumers = self._queues[sub.pattern].consumers
|
|
326
|
+
if subscription_id in consumers:
|
|
327
|
+
consumers.remove(subscription_id)
|
|
328
|
+
|
|
329
|
+
del self._subscriptions[subscription_id]
|
|
330
|
+
|
|
331
|
+
def ack(self, message: Message) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Acknowledge message processing.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
message: Message to acknowledge
|
|
337
|
+
"""
|
|
338
|
+
with self._lock:
|
|
339
|
+
self._pending_acks.pop(message.id, None)
|
|
340
|
+
|
|
341
|
+
def nack(self, message: Message, requeue: bool = True) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Negative acknowledge - reject a message.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
message: Message to reject
|
|
347
|
+
requeue: If True, requeue; if False, send to DLQ
|
|
348
|
+
"""
|
|
349
|
+
with self._lock:
|
|
350
|
+
self._pending_acks.pop(message.id, None)
|
|
351
|
+
|
|
352
|
+
if message.queue:
|
|
353
|
+
q = self._queues.get(message.queue)
|
|
354
|
+
|
|
355
|
+
if requeue and q:
|
|
356
|
+
# Requeue the message
|
|
357
|
+
q.messages.put(message)
|
|
358
|
+
elif q and q.dead_letter_queue:
|
|
359
|
+
# Send to dead letter queue
|
|
360
|
+
if q.dead_letter_queue not in self._queues:
|
|
361
|
+
self._queues[q.dead_letter_queue] = QueueState()
|
|
362
|
+
self._queues[q.dead_letter_queue].messages.put(message)
|
|
363
|
+
|
|
364
|
+
def declare_queue(
|
|
365
|
+
self,
|
|
366
|
+
name: str,
|
|
367
|
+
durable: bool = True,
|
|
368
|
+
exclusive: bool = False,
|
|
369
|
+
auto_delete: bool = False,
|
|
370
|
+
dead_letter_queue: Optional[str] = None,
|
|
371
|
+
ttl: Optional[int] = None
|
|
372
|
+
) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Declare a queue.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
name: Queue name
|
|
378
|
+
durable: (ignored in memory adapter)
|
|
379
|
+
exclusive: If True, only this connection can access
|
|
380
|
+
auto_delete: If True, delete when last consumer disconnects
|
|
381
|
+
dead_letter_queue: Queue for rejected messages
|
|
382
|
+
ttl: Message TTL in milliseconds
|
|
383
|
+
"""
|
|
384
|
+
if not self._connected:
|
|
385
|
+
raise ConnectionError("Broker not connected")
|
|
386
|
+
|
|
387
|
+
with self._lock:
|
|
388
|
+
if name not in self._queues:
|
|
389
|
+
self._queues[name] = QueueState(
|
|
390
|
+
durable=durable,
|
|
391
|
+
exclusive=exclusive,
|
|
392
|
+
auto_delete=auto_delete,
|
|
393
|
+
dead_letter_queue=dead_letter_queue,
|
|
394
|
+
ttl=ttl
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Also declare DLQ if specified
|
|
398
|
+
if dead_letter_queue and dead_letter_queue not in self._queues:
|
|
399
|
+
self._queues[dead_letter_queue] = QueueState(durable=True)
|
|
400
|
+
|
|
401
|
+
def purge_queue(self, name: str) -> int:
|
|
402
|
+
"""
|
|
403
|
+
Remove all messages from a queue.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
name: Queue name
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Number of messages purged
|
|
410
|
+
"""
|
|
411
|
+
if not self._connected:
|
|
412
|
+
raise ConnectionError("Broker not connected")
|
|
413
|
+
|
|
414
|
+
with self._lock:
|
|
415
|
+
if name not in self._queues:
|
|
416
|
+
raise QueueError(f"Queue '{name}' not found")
|
|
417
|
+
|
|
418
|
+
q = self._queues[name]
|
|
419
|
+
count = q.messages.qsize()
|
|
420
|
+
|
|
421
|
+
# Clear the queue
|
|
422
|
+
while not q.messages.empty():
|
|
423
|
+
try:
|
|
424
|
+
q.messages.get_nowait()
|
|
425
|
+
except queue.Empty:
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
return count
|
|
429
|
+
|
|
430
|
+
def delete_queue(self, name: str) -> None:
|
|
431
|
+
"""
|
|
432
|
+
Delete a queue.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
name: Queue name
|
|
436
|
+
"""
|
|
437
|
+
if not self._connected:
|
|
438
|
+
raise ConnectionError("Broker not connected")
|
|
439
|
+
|
|
440
|
+
with self._lock:
|
|
441
|
+
if name in self._queues:
|
|
442
|
+
del self._queues[name]
|
|
443
|
+
|
|
444
|
+
def get_queue_info(self, name: str) -> QueueInfo:
|
|
445
|
+
"""
|
|
446
|
+
Get queue statistics.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
name: Queue name
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
QueueInfo with statistics
|
|
453
|
+
"""
|
|
454
|
+
if not self._connected:
|
|
455
|
+
raise ConnectionError("Broker not connected")
|
|
456
|
+
|
|
457
|
+
with self._lock:
|
|
458
|
+
if name not in self._queues:
|
|
459
|
+
raise QueueError(f"Queue '{name}' not found")
|
|
460
|
+
|
|
461
|
+
q = self._queues[name]
|
|
462
|
+
return QueueInfo(
|
|
463
|
+
name=name,
|
|
464
|
+
message_count=q.messages.qsize(),
|
|
465
|
+
consumer_count=len(q.consumers),
|
|
466
|
+
durable=q.durable,
|
|
467
|
+
auto_delete=q.auto_delete
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
def _match_topic(self, topic: str, pattern: str) -> bool:
|
|
471
|
+
"""
|
|
472
|
+
Match a topic against a pattern.
|
|
473
|
+
|
|
474
|
+
Supports MQTT-style wildcards:
|
|
475
|
+
- * matches exactly one level
|
|
476
|
+
- # matches zero or more levels
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
topic: Actual topic name
|
|
480
|
+
pattern: Pattern to match against
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
True if topic matches pattern
|
|
484
|
+
"""
|
|
485
|
+
if pattern == topic:
|
|
486
|
+
return True
|
|
487
|
+
|
|
488
|
+
# Convert MQTT-style wildcards to fnmatch
|
|
489
|
+
# * -> [^.]* (match one level)
|
|
490
|
+
# # -> * (match any)
|
|
491
|
+
if '#' in pattern or '*' in pattern:
|
|
492
|
+
# Simple wildcard matching
|
|
493
|
+
regex_pattern = pattern.replace('.', r'\.').replace('#', '*').replace('*', '[^.]*')
|
|
494
|
+
import re
|
|
495
|
+
return bool(re.match(f'^{regex_pattern}$', topic))
|
|
496
|
+
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
def _consume_worker(self) -> None:
|
|
500
|
+
"""Background worker for delivering queue messages to consumers."""
|
|
501
|
+
while not self._shutdown.is_set():
|
|
502
|
+
try:
|
|
503
|
+
with self._lock:
|
|
504
|
+
# Find all queue subscriptions
|
|
505
|
+
queue_subs = [
|
|
506
|
+
s for s in self._subscriptions.values()
|
|
507
|
+
if s.is_queue and s.active
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
for sub in queue_subs:
|
|
511
|
+
queue_name = sub.pattern
|
|
512
|
+
with self._lock:
|
|
513
|
+
if queue_name not in self._queues:
|
|
514
|
+
continue
|
|
515
|
+
|
|
516
|
+
q = self._queues[queue_name]
|
|
517
|
+
if q.messages.empty():
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
# Get message
|
|
521
|
+
try:
|
|
522
|
+
msg = q.messages.get_nowait()
|
|
523
|
+
except queue.Empty:
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
# Check if message expired
|
|
527
|
+
expires = msg.headers.get('_expires')
|
|
528
|
+
if expires and float(expires) < time.time() * 1000:
|
|
529
|
+
# Message expired - send to DLQ
|
|
530
|
+
with self._lock:
|
|
531
|
+
if q.dead_letter_queue:
|
|
532
|
+
dlq = self._queues.get(q.dead_letter_queue)
|
|
533
|
+
if dlq:
|
|
534
|
+
dlq.messages.put(msg)
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
# Track pending ack
|
|
538
|
+
with self._lock:
|
|
539
|
+
self._pending_acks[msg.id] = msg
|
|
540
|
+
|
|
541
|
+
# Call handler
|
|
542
|
+
try:
|
|
543
|
+
sub.handler(msg)
|
|
544
|
+
|
|
545
|
+
# Auto-ack if not in pending (handler didn't manually ack)
|
|
546
|
+
# In real implementation, this would depend on ack mode
|
|
547
|
+
except Exception as e:
|
|
548
|
+
print(f"[MemoryBroker] Consumer error: {e}")
|
|
549
|
+
# Requeue on error
|
|
550
|
+
self.nack(msg, requeue=True)
|
|
551
|
+
|
|
552
|
+
# Check for reply (request/reply pattern)
|
|
553
|
+
if msg.reply_to and msg.reply_to in self._reply_queues:
|
|
554
|
+
# The handler should have put a reply in the reply queue
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
# Small sleep to prevent busy-waiting
|
|
558
|
+
time.sleep(0.01)
|
|
559
|
+
|
|
560
|
+
except Exception as e:
|
|
561
|
+
print(f"[MemoryBroker] Worker error: {e}")
|
|
562
|
+
time.sleep(0.1)
|
|
563
|
+
|
|
564
|
+
def reply(self, original_message: Message, response: Message) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Send a reply to a request message.
|
|
567
|
+
|
|
568
|
+
Used by consumers to respond in request/reply pattern.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
original_message: The request message
|
|
572
|
+
response: The response message
|
|
573
|
+
"""
|
|
574
|
+
if not original_message.reply_to:
|
|
575
|
+
raise PublishError("Original message has no reply_to queue")
|
|
576
|
+
|
|
577
|
+
response.correlation_id = original_message.correlation_id
|
|
578
|
+
|
|
579
|
+
with self._lock:
|
|
580
|
+
reply_queue = self._reply_queues.get(original_message.reply_to)
|
|
581
|
+
if reply_queue:
|
|
582
|
+
reply_queue.put(response)
|
|
583
|
+
|
|
584
|
+
# ========================================
|
|
585
|
+
# CLI Helper Methods
|
|
586
|
+
# ========================================
|
|
587
|
+
|
|
588
|
+
def list_queues(self) -> List[Dict[str, Any]]:
|
|
589
|
+
"""List all queues with statistics."""
|
|
590
|
+
with self._lock:
|
|
591
|
+
return [
|
|
592
|
+
{
|
|
593
|
+
'name': name,
|
|
594
|
+
'messages': q.messages.qsize(),
|
|
595
|
+
'consumers': len(q.consumers),
|
|
596
|
+
'durable': q.durable,
|
|
597
|
+
}
|
|
598
|
+
for name, q in self._queues.items()
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
def list_topics(self) -> List[str]:
|
|
602
|
+
"""List all subscribed topics."""
|
|
603
|
+
with self._lock:
|
|
604
|
+
return list(set(
|
|
605
|
+
s.pattern for s in self._subscriptions.values()
|
|
606
|
+
if not s.is_queue and s.active
|
|
607
|
+
))
|
|
608
|
+
|
|
609
|
+
def peek_queue(self, name: str, limit: int = 10) -> List[Message]:
|
|
610
|
+
"""
|
|
611
|
+
Peek at messages in a queue without consuming them.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
name: Queue name
|
|
615
|
+
limit: Maximum messages to return
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
List of messages
|
|
619
|
+
"""
|
|
620
|
+
with self._lock:
|
|
621
|
+
if name not in self._queues:
|
|
622
|
+
return []
|
|
623
|
+
|
|
624
|
+
q = self._queues[name]
|
|
625
|
+
messages = []
|
|
626
|
+
|
|
627
|
+
# Convert queue to list temporarily
|
|
628
|
+
temp_list = []
|
|
629
|
+
while not q.messages.empty() and len(temp_list) < 1000:
|
|
630
|
+
try:
|
|
631
|
+
temp_list.append(q.messages.get_nowait())
|
|
632
|
+
except queue.Empty:
|
|
633
|
+
break
|
|
634
|
+
|
|
635
|
+
# Get first N messages
|
|
636
|
+
messages = temp_list[:limit]
|
|
637
|
+
|
|
638
|
+
# Put all messages back
|
|
639
|
+
for msg in temp_list:
|
|
640
|
+
q.messages.put(msg)
|
|
641
|
+
|
|
642
|
+
return messages
|
|
643
|
+
|
|
644
|
+
def delete_queue(self, name: str, force: bool = False) -> None:
|
|
645
|
+
"""
|
|
646
|
+
Delete a queue.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
name: Queue name
|
|
650
|
+
force: Force delete even if not empty
|
|
651
|
+
"""
|
|
652
|
+
with self._lock:
|
|
653
|
+
if name in self._queues:
|
|
654
|
+
q = self._queues[name]
|
|
655
|
+
if not force and not q.messages.empty():
|
|
656
|
+
raise QueueError(f"Queue '{name}' is not empty. Use force=True to delete anyway.")
|
|
657
|
+
del self._queues[name]
|
|
658
|
+
|
|
659
|
+
def cancel_consumer(self, consumer_id: str) -> None:
|
|
660
|
+
"""Cancel a consumer. Alias for unsubscribe."""
|
|
661
|
+
self.unsubscribe(consumer_id)
|
|
662
|
+
|
|
663
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
664
|
+
"""Get broker statistics."""
|
|
665
|
+
with self._lock:
|
|
666
|
+
total_messages = sum(q.messages.qsize() for q in self._queues.values())
|
|
667
|
+
total_consumers = sum(len(q.consumers) for q in self._queues.values())
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
'broker_type': 'memory',
|
|
671
|
+
'connected': self._connected,
|
|
672
|
+
'queues': len(self._queues),
|
|
673
|
+
'subscriptions': len(self._subscriptions),
|
|
674
|
+
'total_messages': total_messages,
|
|
675
|
+
'total_consumers': total_consumers,
|
|
676
|
+
'pending_acks': len(self._pending_acks),
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
def get_info(self) -> Dict[str, Any]:
|
|
680
|
+
"""Get broker connection info."""
|
|
681
|
+
return {
|
|
682
|
+
'type': 'InMemory',
|
|
683
|
+
'connected': self._connected,
|
|
684
|
+
'persistent': False,
|
|
685
|
+
'note': 'Messages are lost on restart',
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# Alias for backwards compatibility
|
|
690
|
+
InMemoryAdapter = MemoryAdapter
|