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,715 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RabbitMQ Message Broker Adapter
|
|
3
|
+
|
|
4
|
+
Provides a full-featured AMQP implementation of the MessageBroker interface
|
|
5
|
+
using the pika library.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Topic exchanges for pub/sub
|
|
9
|
+
- Direct queues for point-to-point
|
|
10
|
+
- Durable queues and messages
|
|
11
|
+
- Dead letter exchanges
|
|
12
|
+
- Message TTL
|
|
13
|
+
- Consumer prefetch
|
|
14
|
+
- Acknowledgments
|
|
15
|
+
|
|
16
|
+
Requires: pika package (pip install pika)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
import json
|
|
21
|
+
import time
|
|
22
|
+
import uuid
|
|
23
|
+
from typing import Dict, Any, Optional, List
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import pika
|
|
31
|
+
from pika.exceptions import AMQPConnectionError, AMQPChannelError
|
|
32
|
+
PIKA_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
PIKA_AVAILABLE = False
|
|
35
|
+
pika = None
|
|
36
|
+
AMQPConnectionError = Exception
|
|
37
|
+
AMQPChannelError = Exception
|
|
38
|
+
|
|
39
|
+
# Support both direct execution and import from parent
|
|
40
|
+
try:
|
|
41
|
+
from quantum.runtime.message_broker import (
|
|
42
|
+
MessageBroker, Message, QueueInfo, MessageHandler,
|
|
43
|
+
MessageBrokerError, ConnectionError, PublishError,
|
|
44
|
+
SubscribeError, QueueError
|
|
45
|
+
)
|
|
46
|
+
except ImportError:
|
|
47
|
+
from message_broker import (
|
|
48
|
+
MessageBroker, Message, QueueInfo, MessageHandler,
|
|
49
|
+
MessageBrokerError, ConnectionError, PublishError,
|
|
50
|
+
SubscribeError, QueueError
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RabbitSubscription:
|
|
56
|
+
"""Internal state for a RabbitMQ subscription"""
|
|
57
|
+
id: str
|
|
58
|
+
pattern: str
|
|
59
|
+
handler: MessageHandler
|
|
60
|
+
is_queue: bool = False
|
|
61
|
+
prefetch: int = 1
|
|
62
|
+
active: bool = True
|
|
63
|
+
consumer_tag: Optional[str] = None
|
|
64
|
+
thread: Optional[threading.Thread] = None
|
|
65
|
+
channel: Any = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RabbitMQAdapter(MessageBroker):
|
|
69
|
+
"""
|
|
70
|
+
RabbitMQ-based implementation of MessageBroker.
|
|
71
|
+
|
|
72
|
+
Features:
|
|
73
|
+
- Full AMQP 0-9-1 support
|
|
74
|
+
- Durable queues and exchanges
|
|
75
|
+
- Topic exchange for pub/sub with wildcards (* and #)
|
|
76
|
+
- Dead letter exchanges
|
|
77
|
+
- Consumer acknowledgments
|
|
78
|
+
- Request/reply pattern with reply queues
|
|
79
|
+
|
|
80
|
+
Configuration:
|
|
81
|
+
{
|
|
82
|
+
'host': 'localhost',
|
|
83
|
+
'port': 5672,
|
|
84
|
+
'virtual_host': '/',
|
|
85
|
+
'username': 'guest',
|
|
86
|
+
'password': 'guest',
|
|
87
|
+
'heartbeat': 600,
|
|
88
|
+
'exchange': 'quantum' # Default exchange name
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self):
|
|
93
|
+
if not PIKA_AVAILABLE:
|
|
94
|
+
raise ImportError(
|
|
95
|
+
"RabbitMQ adapter requires 'pika' package. "
|
|
96
|
+
"Install with: pip install pika"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self._connection: Any = None
|
|
100
|
+
self._channel: Any = None
|
|
101
|
+
self._connected = False
|
|
102
|
+
self._config: Dict[str, Any] = {}
|
|
103
|
+
self._exchange = 'quantum'
|
|
104
|
+
|
|
105
|
+
self._lock = threading.RLock()
|
|
106
|
+
self._subscriptions: Dict[str, RabbitSubscription] = {}
|
|
107
|
+
self._pending_acks: Dict[str, Any] = {} # message_id -> delivery_tag
|
|
108
|
+
self._reply_queues: Dict[str, Any] = {}
|
|
109
|
+
|
|
110
|
+
self._shutdown = threading.Event()
|
|
111
|
+
|
|
112
|
+
def connect(self, config: Dict[str, Any] = None) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Connect to RabbitMQ.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
config: RabbitMQ connection configuration
|
|
118
|
+
"""
|
|
119
|
+
if config is None:
|
|
120
|
+
config = {}
|
|
121
|
+
|
|
122
|
+
self._config = config
|
|
123
|
+
self._exchange = config.get('exchange', 'quantum')
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
credentials = pika.PlainCredentials(
|
|
127
|
+
config.get('username', 'guest'),
|
|
128
|
+
config.get('password', 'guest')
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
parameters = pika.ConnectionParameters(
|
|
132
|
+
host=config.get('host', 'localhost'),
|
|
133
|
+
port=config.get('port', 5672),
|
|
134
|
+
virtual_host=config.get('virtual_host', '/'),
|
|
135
|
+
credentials=credentials,
|
|
136
|
+
heartbeat=config.get('heartbeat', 600)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self._connection = pika.BlockingConnection(parameters)
|
|
140
|
+
self._channel = self._connection.channel()
|
|
141
|
+
|
|
142
|
+
# Declare the main topic exchange
|
|
143
|
+
self._channel.exchange_declare(
|
|
144
|
+
exchange=self._exchange,
|
|
145
|
+
exchange_type='topic',
|
|
146
|
+
durable=True
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self._connected = True
|
|
150
|
+
self._shutdown.clear()
|
|
151
|
+
|
|
152
|
+
except AMQPConnectionError as e:
|
|
153
|
+
raise ConnectionError(f"Failed to connect to RabbitMQ: {e}")
|
|
154
|
+
|
|
155
|
+
def disconnect(self) -> None:
|
|
156
|
+
"""Disconnect from RabbitMQ and clean up resources."""
|
|
157
|
+
with self._lock:
|
|
158
|
+
if not self._connected:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
self._shutdown.set()
|
|
162
|
+
|
|
163
|
+
# Stop all subscriptions
|
|
164
|
+
for sub in list(self._subscriptions.values()):
|
|
165
|
+
sub.active = False
|
|
166
|
+
if sub.consumer_tag and sub.channel:
|
|
167
|
+
try:
|
|
168
|
+
sub.channel.basic_cancel(sub.consumer_tag)
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
if sub.thread and sub.thread.is_alive():
|
|
172
|
+
sub.thread.join(timeout=1.0)
|
|
173
|
+
|
|
174
|
+
self._subscriptions.clear()
|
|
175
|
+
|
|
176
|
+
# Close channel and connection
|
|
177
|
+
try:
|
|
178
|
+
if self._channel and self._channel.is_open:
|
|
179
|
+
self._channel.close()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if self._connection and self._connection.is_open:
|
|
185
|
+
self._connection.close()
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
self._channel = None
|
|
190
|
+
self._connection = None
|
|
191
|
+
self._connected = False
|
|
192
|
+
|
|
193
|
+
def is_connected(self) -> bool:
|
|
194
|
+
"""Check if connected to RabbitMQ."""
|
|
195
|
+
return (
|
|
196
|
+
self._connected and
|
|
197
|
+
self._connection is not None and
|
|
198
|
+
self._connection.is_open and
|
|
199
|
+
self._channel is not None and
|
|
200
|
+
self._channel.is_open
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def publish(self, topic: str, message: Message) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Publish a message to a topic.
|
|
206
|
+
|
|
207
|
+
Uses the topic exchange with routing key = topic.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
topic: Topic name (routing key)
|
|
211
|
+
message: Message to publish
|
|
212
|
+
"""
|
|
213
|
+
if not self.is_connected():
|
|
214
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
215
|
+
|
|
216
|
+
message.topic = topic
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
properties = pika.BasicProperties(
|
|
220
|
+
message_id=message.id,
|
|
221
|
+
timestamp=int(message.timestamp),
|
|
222
|
+
headers=message.headers,
|
|
223
|
+
content_type='application/json',
|
|
224
|
+
delivery_mode=2 # Persistent
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self._channel.basic_publish(
|
|
228
|
+
exchange=self._exchange,
|
|
229
|
+
routing_key=topic,
|
|
230
|
+
body=message.to_json(),
|
|
231
|
+
properties=properties
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
except AMQPChannelError as e:
|
|
235
|
+
raise PublishError(f"Failed to publish message: {e}")
|
|
236
|
+
|
|
237
|
+
def send(self, queue_name: str, message: Message) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Send a message directly to a queue.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
queue_name: Queue name
|
|
243
|
+
message: Message to send
|
|
244
|
+
"""
|
|
245
|
+
if not self.is_connected():
|
|
246
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
247
|
+
|
|
248
|
+
message.queue = queue_name
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
properties = pika.BasicProperties(
|
|
252
|
+
message_id=message.id,
|
|
253
|
+
timestamp=int(message.timestamp),
|
|
254
|
+
headers=message.headers,
|
|
255
|
+
content_type='application/json',
|
|
256
|
+
delivery_mode=2, # Persistent
|
|
257
|
+
reply_to=message.reply_to,
|
|
258
|
+
correlation_id=message.correlation_id
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
self._channel.basic_publish(
|
|
262
|
+
exchange='', # Default exchange
|
|
263
|
+
routing_key=queue_name,
|
|
264
|
+
body=message.to_json(),
|
|
265
|
+
properties=properties
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
except AMQPChannelError as e:
|
|
269
|
+
raise PublishError(f"Failed to send message: {e}")
|
|
270
|
+
|
|
271
|
+
def request(self, queue_name: str, message: Message, timeout: int = 5000) -> Message:
|
|
272
|
+
"""
|
|
273
|
+
Send a request and wait for a reply.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
queue_name: Queue to send request to
|
|
277
|
+
message: Request message
|
|
278
|
+
timeout: Timeout in milliseconds
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Response message
|
|
282
|
+
"""
|
|
283
|
+
if not self.is_connected():
|
|
284
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
285
|
+
|
|
286
|
+
# Create exclusive reply queue
|
|
287
|
+
result = self._channel.queue_declare(queue='', exclusive=True)
|
|
288
|
+
reply_queue = result.method.queue
|
|
289
|
+
correlation_id = str(uuid.uuid4())
|
|
290
|
+
|
|
291
|
+
message.reply_to = reply_queue
|
|
292
|
+
message.correlation_id = correlation_id
|
|
293
|
+
|
|
294
|
+
response = [None]
|
|
295
|
+
response_received = threading.Event()
|
|
296
|
+
|
|
297
|
+
def on_response(ch, method, props, body):
|
|
298
|
+
if props.correlation_id == correlation_id:
|
|
299
|
+
response[0] = Message.from_json(body.decode())
|
|
300
|
+
response_received.set()
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
self._channel.basic_consume(
|
|
304
|
+
queue=reply_queue,
|
|
305
|
+
on_message_callback=on_response,
|
|
306
|
+
auto_ack=True
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Send request
|
|
310
|
+
self.send(queue_name, message)
|
|
311
|
+
|
|
312
|
+
# Wait for response
|
|
313
|
+
start_time = time.time()
|
|
314
|
+
while not response_received.is_set():
|
|
315
|
+
self._connection.process_data_events(time_limit=0.1)
|
|
316
|
+
if time.time() - start_time > timeout / 1000.0:
|
|
317
|
+
raise TimeoutError(f"Request timed out after {timeout}ms")
|
|
318
|
+
|
|
319
|
+
return response[0]
|
|
320
|
+
|
|
321
|
+
finally:
|
|
322
|
+
# Clean up reply queue
|
|
323
|
+
try:
|
|
324
|
+
self._channel.queue_delete(queue=reply_queue)
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
329
|
+
"""
|
|
330
|
+
Subscribe to a topic pattern.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
topic: Topic pattern (supports * and # wildcards)
|
|
334
|
+
handler: Callback for received messages
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Subscription ID
|
|
338
|
+
"""
|
|
339
|
+
if not self.is_connected():
|
|
340
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
341
|
+
|
|
342
|
+
sub_id = f"sub_{uuid.uuid4().hex}"
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
# Create exclusive queue for this subscription
|
|
346
|
+
result = self._channel.queue_declare(queue='', exclusive=True)
|
|
347
|
+
queue_name = result.method.queue
|
|
348
|
+
|
|
349
|
+
# Bind to topic exchange
|
|
350
|
+
self._channel.queue_bind(
|
|
351
|
+
exchange=self._exchange,
|
|
352
|
+
queue=queue_name,
|
|
353
|
+
routing_key=topic
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
subscription = RabbitSubscription(
|
|
357
|
+
id=sub_id,
|
|
358
|
+
pattern=topic,
|
|
359
|
+
handler=handler,
|
|
360
|
+
is_queue=False
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Start consumer in separate thread
|
|
364
|
+
thread = threading.Thread(
|
|
365
|
+
target=self._consume_thread,
|
|
366
|
+
args=(subscription, queue_name, True),
|
|
367
|
+
daemon=True,
|
|
368
|
+
name=f"RabbitMQ-Sub-{sub_id}"
|
|
369
|
+
)
|
|
370
|
+
subscription.thread = thread
|
|
371
|
+
|
|
372
|
+
with self._lock:
|
|
373
|
+
self._subscriptions[sub_id] = subscription
|
|
374
|
+
|
|
375
|
+
thread.start()
|
|
376
|
+
|
|
377
|
+
return sub_id
|
|
378
|
+
|
|
379
|
+
except AMQPChannelError as e:
|
|
380
|
+
raise SubscribeError(f"Failed to subscribe: {e}")
|
|
381
|
+
|
|
382
|
+
def consume(self, queue_name: str, handler: MessageHandler, prefetch: int = 1) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Start consuming messages from a queue.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
queue_name: Queue name
|
|
388
|
+
handler: Callback for received messages
|
|
389
|
+
prefetch: Number of messages to prefetch
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Consumer ID
|
|
393
|
+
"""
|
|
394
|
+
if not self.is_connected():
|
|
395
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
396
|
+
|
|
397
|
+
consumer_id = f"consumer_{uuid.uuid4().hex}"
|
|
398
|
+
|
|
399
|
+
subscription = RabbitSubscription(
|
|
400
|
+
id=consumer_id,
|
|
401
|
+
pattern=queue_name,
|
|
402
|
+
handler=handler,
|
|
403
|
+
is_queue=True,
|
|
404
|
+
prefetch=prefetch
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Start consumer in separate thread
|
|
408
|
+
thread = threading.Thread(
|
|
409
|
+
target=self._consume_thread,
|
|
410
|
+
args=(subscription, queue_name, False),
|
|
411
|
+
daemon=True,
|
|
412
|
+
name=f"RabbitMQ-Consumer-{consumer_id}"
|
|
413
|
+
)
|
|
414
|
+
subscription.thread = thread
|
|
415
|
+
|
|
416
|
+
with self._lock:
|
|
417
|
+
self._subscriptions[consumer_id] = subscription
|
|
418
|
+
|
|
419
|
+
thread.start()
|
|
420
|
+
|
|
421
|
+
return consumer_id
|
|
422
|
+
|
|
423
|
+
def unsubscribe(self, subscription_id: str) -> None:
|
|
424
|
+
"""
|
|
425
|
+
Unsubscribe or stop consuming.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
subscription_id: Subscription or consumer ID
|
|
429
|
+
"""
|
|
430
|
+
with self._lock:
|
|
431
|
+
if subscription_id in self._subscriptions:
|
|
432
|
+
sub = self._subscriptions[subscription_id]
|
|
433
|
+
sub.active = False
|
|
434
|
+
|
|
435
|
+
if sub.consumer_tag and sub.channel:
|
|
436
|
+
try:
|
|
437
|
+
sub.channel.basic_cancel(sub.consumer_tag)
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
if sub.thread and sub.thread.is_alive():
|
|
442
|
+
sub.thread.join(timeout=1.0)
|
|
443
|
+
|
|
444
|
+
del self._subscriptions[subscription_id]
|
|
445
|
+
|
|
446
|
+
def ack(self, message: Message) -> None:
|
|
447
|
+
"""
|
|
448
|
+
Acknowledge message processing.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
message: Message to acknowledge
|
|
452
|
+
"""
|
|
453
|
+
with self._lock:
|
|
454
|
+
delivery_tag = self._pending_acks.pop(message.id, None)
|
|
455
|
+
if delivery_tag:
|
|
456
|
+
try:
|
|
457
|
+
self._channel.basic_ack(delivery_tag=delivery_tag)
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
def nack(self, message: Message, requeue: bool = True) -> None:
|
|
462
|
+
"""
|
|
463
|
+
Negative acknowledge - reject a message.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
message: Message to reject
|
|
467
|
+
requeue: If True, requeue; if False, send to DLX
|
|
468
|
+
"""
|
|
469
|
+
with self._lock:
|
|
470
|
+
delivery_tag = self._pending_acks.pop(message.id, None)
|
|
471
|
+
if delivery_tag:
|
|
472
|
+
try:
|
|
473
|
+
self._channel.basic_nack(
|
|
474
|
+
delivery_tag=delivery_tag,
|
|
475
|
+
requeue=requeue
|
|
476
|
+
)
|
|
477
|
+
except Exception:
|
|
478
|
+
pass
|
|
479
|
+
|
|
480
|
+
def declare_queue(
|
|
481
|
+
self,
|
|
482
|
+
name: str,
|
|
483
|
+
durable: bool = True,
|
|
484
|
+
exclusive: bool = False,
|
|
485
|
+
auto_delete: bool = False,
|
|
486
|
+
dead_letter_queue: Optional[str] = None,
|
|
487
|
+
ttl: Optional[int] = None
|
|
488
|
+
) -> None:
|
|
489
|
+
"""
|
|
490
|
+
Declare a queue.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
name: Queue name
|
|
494
|
+
durable: Survive broker restart
|
|
495
|
+
exclusive: Only this connection can access
|
|
496
|
+
auto_delete: Delete when last consumer disconnects
|
|
497
|
+
dead_letter_queue: Queue for rejected messages
|
|
498
|
+
ttl: Message TTL in milliseconds
|
|
499
|
+
"""
|
|
500
|
+
if not self.is_connected():
|
|
501
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
arguments = {}
|
|
505
|
+
|
|
506
|
+
if dead_letter_queue:
|
|
507
|
+
# Create dead letter exchange
|
|
508
|
+
dlx_name = f"{name}_dlx"
|
|
509
|
+
self._channel.exchange_declare(
|
|
510
|
+
exchange=dlx_name,
|
|
511
|
+
exchange_type='direct',
|
|
512
|
+
durable=True
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Create dead letter queue
|
|
516
|
+
self._channel.queue_declare(
|
|
517
|
+
queue=dead_letter_queue,
|
|
518
|
+
durable=True
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Bind DLQ to DLX
|
|
522
|
+
self._channel.queue_bind(
|
|
523
|
+
exchange=dlx_name,
|
|
524
|
+
queue=dead_letter_queue,
|
|
525
|
+
routing_key=dead_letter_queue
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
arguments['x-dead-letter-exchange'] = dlx_name
|
|
529
|
+
arguments['x-dead-letter-routing-key'] = dead_letter_queue
|
|
530
|
+
|
|
531
|
+
if ttl:
|
|
532
|
+
arguments['x-message-ttl'] = ttl
|
|
533
|
+
|
|
534
|
+
self._channel.queue_declare(
|
|
535
|
+
queue=name,
|
|
536
|
+
durable=durable,
|
|
537
|
+
exclusive=exclusive,
|
|
538
|
+
auto_delete=auto_delete,
|
|
539
|
+
arguments=arguments if arguments else None
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
except AMQPChannelError as e:
|
|
543
|
+
raise QueueError(f"Failed to declare queue: {e}")
|
|
544
|
+
|
|
545
|
+
def purge_queue(self, name: str) -> int:
|
|
546
|
+
"""
|
|
547
|
+
Remove all messages from a queue.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
name: Queue name
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Number of messages purged
|
|
554
|
+
"""
|
|
555
|
+
if not self.is_connected():
|
|
556
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
result = self._channel.queue_purge(queue=name)
|
|
560
|
+
return result.method.message_count
|
|
561
|
+
except AMQPChannelError as e:
|
|
562
|
+
raise QueueError(f"Failed to purge queue: {e}")
|
|
563
|
+
|
|
564
|
+
def delete_queue(self, name: str) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Delete a queue.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
name: Queue name
|
|
570
|
+
"""
|
|
571
|
+
if not self.is_connected():
|
|
572
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
self._channel.queue_delete(queue=name)
|
|
576
|
+
except AMQPChannelError as e:
|
|
577
|
+
raise QueueError(f"Failed to delete queue: {e}")
|
|
578
|
+
|
|
579
|
+
def get_queue_info(self, name: str) -> QueueInfo:
|
|
580
|
+
"""
|
|
581
|
+
Get queue statistics.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
name: Queue name
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
QueueInfo with statistics
|
|
588
|
+
"""
|
|
589
|
+
if not self.is_connected():
|
|
590
|
+
raise ConnectionError("Not connected to RabbitMQ")
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
result = self._channel.queue_declare(
|
|
594
|
+
queue=name,
|
|
595
|
+
passive=True # Only check, don't create
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return QueueInfo(
|
|
599
|
+
name=name,
|
|
600
|
+
message_count=result.method.message_count,
|
|
601
|
+
consumer_count=result.method.consumer_count,
|
|
602
|
+
durable=True, # Cannot determine from passive declare
|
|
603
|
+
auto_delete=False
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
except AMQPChannelError as e:
|
|
607
|
+
raise QueueError(f"Queue '{name}' not found: {e}")
|
|
608
|
+
|
|
609
|
+
def _consume_thread(
|
|
610
|
+
self,
|
|
611
|
+
subscription: RabbitSubscription,
|
|
612
|
+
queue_name: str,
|
|
613
|
+
auto_ack: bool
|
|
614
|
+
) -> None:
|
|
615
|
+
"""Background thread for consuming messages."""
|
|
616
|
+
try:
|
|
617
|
+
# Create new connection for this thread
|
|
618
|
+
credentials = pika.PlainCredentials(
|
|
619
|
+
self._config.get('username', 'guest'),
|
|
620
|
+
self._config.get('password', 'guest')
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
parameters = pika.ConnectionParameters(
|
|
624
|
+
host=self._config.get('host', 'localhost'),
|
|
625
|
+
port=self._config.get('port', 5672),
|
|
626
|
+
virtual_host=self._config.get('virtual_host', '/'),
|
|
627
|
+
credentials=credentials,
|
|
628
|
+
heartbeat=self._config.get('heartbeat', 600)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
connection = pika.BlockingConnection(parameters)
|
|
632
|
+
channel = connection.channel()
|
|
633
|
+
subscription.channel = channel
|
|
634
|
+
|
|
635
|
+
# Set prefetch
|
|
636
|
+
channel.basic_qos(prefetch_count=subscription.prefetch)
|
|
637
|
+
|
|
638
|
+
def callback(ch, method, properties, body):
|
|
639
|
+
if not subscription.active:
|
|
640
|
+
return
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
msg = Message.from_json(body.decode())
|
|
644
|
+
|
|
645
|
+
if not auto_ack:
|
|
646
|
+
with self._lock:
|
|
647
|
+
self._pending_acks[msg.id] = method.delivery_tag
|
|
648
|
+
|
|
649
|
+
subscription.handler(msg)
|
|
650
|
+
|
|
651
|
+
if auto_ack:
|
|
652
|
+
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
653
|
+
|
|
654
|
+
except Exception as e:
|
|
655
|
+
print(f"[RabbitMQ] Handler error: {e}")
|
|
656
|
+
if not auto_ack:
|
|
657
|
+
ch.basic_nack(
|
|
658
|
+
delivery_tag=method.delivery_tag,
|
|
659
|
+
requeue=True
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
consumer_tag = channel.basic_consume(
|
|
663
|
+
queue=queue_name,
|
|
664
|
+
on_message_callback=callback,
|
|
665
|
+
auto_ack=False
|
|
666
|
+
)
|
|
667
|
+
subscription.consumer_tag = consumer_tag
|
|
668
|
+
|
|
669
|
+
while subscription.active and not self._shutdown.is_set():
|
|
670
|
+
try:
|
|
671
|
+
connection.process_data_events(time_limit=1.0)
|
|
672
|
+
except Exception:
|
|
673
|
+
if subscription.active:
|
|
674
|
+
time.sleep(1)
|
|
675
|
+
|
|
676
|
+
# Cleanup
|
|
677
|
+
try:
|
|
678
|
+
channel.close()
|
|
679
|
+
connection.close()
|
|
680
|
+
except Exception:
|
|
681
|
+
pass
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
print(f"[RabbitMQ] Consumer thread error: {e}")
|
|
685
|
+
|
|
686
|
+
def reply(self, original_message: Message, response: Message) -> None:
|
|
687
|
+
"""
|
|
688
|
+
Send a reply to a request message.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
original_message: The request message
|
|
692
|
+
response: The response message
|
|
693
|
+
"""
|
|
694
|
+
if not original_message.reply_to:
|
|
695
|
+
raise PublishError("Original message has no reply_to queue")
|
|
696
|
+
|
|
697
|
+
response.correlation_id = original_message.correlation_id
|
|
698
|
+
response.queue = original_message.reply_to
|
|
699
|
+
|
|
700
|
+
try:
|
|
701
|
+
properties = pika.BasicProperties(
|
|
702
|
+
message_id=response.id,
|
|
703
|
+
correlation_id=response.correlation_id,
|
|
704
|
+
content_type='application/json'
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
self._channel.basic_publish(
|
|
708
|
+
exchange='',
|
|
709
|
+
routing_key=original_message.reply_to,
|
|
710
|
+
body=response.to_json(),
|
|
711
|
+
properties=properties
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
except AMQPChannelError as e:
|
|
715
|
+
raise PublishError(f"Failed to send reply: {e}")
|