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,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message Queue Service
|
|
3
|
+
|
|
4
|
+
High-level service for Quantum message queue operations.
|
|
5
|
+
Wraps the message broker abstraction for use in the ComponentRuntime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import json
|
|
12
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from quantum.runtime.message_broker import Message, QueueInfo, MessageBrokerError
|
|
16
|
+
from quantum.runtime.adapters import get_adapter, MemoryAdapter
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger('quantum.messaging')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageQueueError(Exception):
|
|
22
|
+
"""Raised when message queue operation fails"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MessageResult:
|
|
28
|
+
"""Result of a message operation"""
|
|
29
|
+
success: bool
|
|
30
|
+
message_id: Optional[str] = None
|
|
31
|
+
error: Optional[str] = None
|
|
32
|
+
data: Optional[Any] = None
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
'success': self.success,
|
|
37
|
+
'message_id': self.message_id,
|
|
38
|
+
'error': self.error,
|
|
39
|
+
'data': self.data
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MessageQueueService:
|
|
44
|
+
"""
|
|
45
|
+
Service for message queue operations in Quantum components.
|
|
46
|
+
|
|
47
|
+
Provides a high-level interface for:
|
|
48
|
+
- Publishing messages to topics
|
|
49
|
+
- Sending messages to queues
|
|
50
|
+
- Request/reply pattern
|
|
51
|
+
- Queue management
|
|
52
|
+
- Subscriptions
|
|
53
|
+
|
|
54
|
+
Configuration (environment variables):
|
|
55
|
+
MESSAGE_BROKER_TYPE: 'memory', 'sqlite', 'redis' or 'rabbitmq'
|
|
56
|
+
(default: memory — in ONE process only; nothing published
|
|
57
|
+
crosses a process boundary or survives a restart. 'sqlite'
|
|
58
|
+
is the durable option that needs no server.)
|
|
59
|
+
QUANTUM_MQ_PATH: sqlite database file (default: quantum_messages.db)
|
|
60
|
+
REDIS_HOST: Redis host (default: localhost)
|
|
61
|
+
REDIS_PORT: Redis port (default: 6379)
|
|
62
|
+
RABBITMQ_HOST: RabbitMQ host (default: localhost)
|
|
63
|
+
RABBITMQ_PORT: RabbitMQ port (default: 5672)
|
|
64
|
+
RABBITMQ_USER: RabbitMQ username (default: guest)
|
|
65
|
+
RABBITMQ_PASSWORD: RabbitMQ password (default: guest)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, config: Dict[str, Any] = None):
|
|
69
|
+
"""
|
|
70
|
+
Initialize the message queue service.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
config: Optional configuration (overrides environment)
|
|
74
|
+
"""
|
|
75
|
+
self._config = config or {}
|
|
76
|
+
self._broker = None
|
|
77
|
+
self._subscriptions: Dict[str, str] = {} # name -> subscription_id
|
|
78
|
+
self._handlers: Dict[str, Callable] = {} # name -> handler function
|
|
79
|
+
# A mensagem em processamento, POR THREAD.
|
|
80
|
+
#
|
|
81
|
+
# Era um atributo de instancia unico, escrito pelo callback de TODA
|
|
82
|
+
# assinatura — e cada assinatura roda na propria thread de polling.
|
|
83
|
+
# Com duas assinaturas ativas, o `ack()` chamado dentro do handler A
|
|
84
|
+
# confirmava a mensagem que o handler B tinha acabado de reivindicar:
|
|
85
|
+
# a de A voltava para a fila e a de B sumia sem ter sido processada.
|
|
86
|
+
# Quanto mais lento o handler, maior a janela.
|
|
87
|
+
self._local = threading.local()
|
|
88
|
+
|
|
89
|
+
def _get_broker_type(self) -> str:
|
|
90
|
+
"""Get broker type from config or environment.
|
|
91
|
+
|
|
92
|
+
O padrao e `sqlite`, nao `memory`. O broker em memoria vive dentro de
|
|
93
|
+
UM interpretador: uma publicacao neste processo e invisivel para
|
|
94
|
+
qualquer outro, e tudo enfileirado morre com ele — sob gunicorn, uma
|
|
95
|
+
fila privada por worker. Isso e perda silenciosa de dados como
|
|
96
|
+
default.
|
|
97
|
+
|
|
98
|
+
sqlite e o unico ponto do espectro que e duravel E sem servidor. Nao
|
|
99
|
+
adiciona dependencia (esta na stdlib) e nao adiciona processo; o
|
|
100
|
+
arquivo fica ao lado de `quantum_jobs.db`, que a fila de jobs ja usa
|
|
101
|
+
do mesmo jeito.
|
|
102
|
+
|
|
103
|
+
`MESSAGE_BROKER_TYPE=memory` volta ao anterior — legitimo para teste e
|
|
104
|
+
para um app de processo unico que nao quer arquivo nenhum.
|
|
105
|
+
"""
|
|
106
|
+
return self._config.get(
|
|
107
|
+
'broker_type',
|
|
108
|
+
os.getenv('MESSAGE_BROKER_TYPE', 'sqlite')
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _get_broker_config(self) -> Dict[str, Any]:
|
|
112
|
+
"""Build broker configuration from environment."""
|
|
113
|
+
broker_type = self._get_broker_type()
|
|
114
|
+
|
|
115
|
+
if broker_type == 'sqlite':
|
|
116
|
+
return {
|
|
117
|
+
'path': os.getenv('QUANTUM_MQ_PATH', 'quantum_messages.db'),
|
|
118
|
+
}
|
|
119
|
+
elif broker_type == 'redis':
|
|
120
|
+
return {
|
|
121
|
+
'host': os.getenv('REDIS_HOST', 'localhost'),
|
|
122
|
+
'port': int(os.getenv('REDIS_PORT', '6379')),
|
|
123
|
+
'db': int(os.getenv('REDIS_DB', '0')),
|
|
124
|
+
'password': os.getenv('REDIS_PASSWORD'),
|
|
125
|
+
'prefix': os.getenv('REDIS_PREFIX', 'quantum:')
|
|
126
|
+
}
|
|
127
|
+
elif broker_type == 'rabbitmq':
|
|
128
|
+
return {
|
|
129
|
+
'host': os.getenv('RABBITMQ_HOST', 'localhost'),
|
|
130
|
+
'port': int(os.getenv('RABBITMQ_PORT', '5672')),
|
|
131
|
+
'virtual_host': os.getenv('RABBITMQ_VHOST', '/'),
|
|
132
|
+
'username': os.getenv('RABBITMQ_USER', 'guest'),
|
|
133
|
+
'password': os.getenv('RABBITMQ_PASSWORD', 'guest'),
|
|
134
|
+
'exchange': os.getenv('RABBITMQ_EXCHANGE', 'quantum')
|
|
135
|
+
}
|
|
136
|
+
else:
|
|
137
|
+
return {}
|
|
138
|
+
|
|
139
|
+
def connect(self) -> None:
|
|
140
|
+
"""Connect to the message broker."""
|
|
141
|
+
if self._broker is not None:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
broker_type = self._get_broker_type()
|
|
145
|
+
broker_config = self._get_broker_config()
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
self._broker = get_adapter(broker_type)
|
|
149
|
+
self._broker.connect(broker_config)
|
|
150
|
+
except ImportError as e:
|
|
151
|
+
# Fall back to memory adapter if required package not available
|
|
152
|
+
print(f"[MessageQueue] {e}. Falling back to in-memory broker.")
|
|
153
|
+
self._broker = MemoryAdapter()
|
|
154
|
+
self._broker.connect({})
|
|
155
|
+
broker_type = 'memory'
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise MessageQueueError(f"Failed to connect to message broker: {e}")
|
|
158
|
+
|
|
159
|
+
if broker_type == 'memory':
|
|
160
|
+
# The in-memory broker lives inside ONE interpreter. A publish in
|
|
161
|
+
# this process is invisible to any other, and everything queued is
|
|
162
|
+
# gone when it exits — under gunicorn that means one private queue
|
|
163
|
+
# per worker. It is the right default for a single-process app and
|
|
164
|
+
# a silent data loss anywhere else, so say which one this is.
|
|
165
|
+
logger.warning(
|
|
166
|
+
"q:message is using the in-memory broker: messages do not "
|
|
167
|
+
"cross processes and do not survive a restart. Set "
|
|
168
|
+
"MESSAGE_BROKER_TYPE=sqlite (with QUANTUM_MQ_PATH) for a "
|
|
169
|
+
"durable one, or redis/rabbitmq."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def disconnect(self) -> None:
|
|
173
|
+
"""Disconnect from the message broker."""
|
|
174
|
+
if self._broker:
|
|
175
|
+
self._broker.disconnect()
|
|
176
|
+
self._broker = None
|
|
177
|
+
self._subscriptions.clear()
|
|
178
|
+
|
|
179
|
+
def _ensure_connected(self) -> None:
|
|
180
|
+
"""Ensure broker is connected."""
|
|
181
|
+
if self._broker is None or not self._broker.is_connected():
|
|
182
|
+
self.connect()
|
|
183
|
+
|
|
184
|
+
def publish(
|
|
185
|
+
self,
|
|
186
|
+
topic: str,
|
|
187
|
+
body: Any,
|
|
188
|
+
headers: Dict[str, str] = None
|
|
189
|
+
) -> MessageResult:
|
|
190
|
+
"""
|
|
191
|
+
Publish a message to a topic.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
topic: Topic name
|
|
195
|
+
body: Message body (will be JSON serialized if not string)
|
|
196
|
+
headers: Optional message headers
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
MessageResult with operation status
|
|
200
|
+
"""
|
|
201
|
+
self._ensure_connected()
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Serialize body if needed
|
|
205
|
+
if not isinstance(body, str):
|
|
206
|
+
body = json.dumps(body)
|
|
207
|
+
|
|
208
|
+
message = Message(
|
|
209
|
+
topic=topic,
|
|
210
|
+
body=body,
|
|
211
|
+
headers=headers or {}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self._broker.publish(topic, message)
|
|
215
|
+
|
|
216
|
+
return MessageResult(
|
|
217
|
+
success=True,
|
|
218
|
+
message_id=message.id
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
return MessageResult(
|
|
223
|
+
success=False,
|
|
224
|
+
error=str(e)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def send(
|
|
228
|
+
self,
|
|
229
|
+
queue: str,
|
|
230
|
+
body: Any,
|
|
231
|
+
headers: Dict[str, str] = None
|
|
232
|
+
) -> MessageResult:
|
|
233
|
+
"""
|
|
234
|
+
Send a message to a queue.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
queue: Queue name
|
|
238
|
+
body: Message body
|
|
239
|
+
headers: Optional message headers
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
MessageResult with operation status
|
|
243
|
+
"""
|
|
244
|
+
self._ensure_connected()
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if not isinstance(body, str):
|
|
248
|
+
body = json.dumps(body)
|
|
249
|
+
|
|
250
|
+
message = Message(
|
|
251
|
+
queue=queue,
|
|
252
|
+
body=body,
|
|
253
|
+
headers=headers or {}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
self._broker.send(queue, message)
|
|
257
|
+
|
|
258
|
+
return MessageResult(
|
|
259
|
+
success=True,
|
|
260
|
+
message_id=message.id
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return MessageResult(
|
|
265
|
+
success=False,
|
|
266
|
+
error=str(e)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def request(
|
|
270
|
+
self,
|
|
271
|
+
queue: str,
|
|
272
|
+
body: Any,
|
|
273
|
+
headers: Dict[str, str] = None,
|
|
274
|
+
timeout: int = 5000
|
|
275
|
+
) -> MessageResult:
|
|
276
|
+
"""
|
|
277
|
+
Send a request and wait for a reply.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
queue: Queue name
|
|
281
|
+
body: Request body
|
|
282
|
+
headers: Optional headers
|
|
283
|
+
timeout: Timeout in milliseconds
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
MessageResult with response data
|
|
287
|
+
"""
|
|
288
|
+
self._ensure_connected()
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
if not isinstance(body, str):
|
|
292
|
+
body = json.dumps(body)
|
|
293
|
+
|
|
294
|
+
message = Message(
|
|
295
|
+
queue=queue,
|
|
296
|
+
body=body,
|
|
297
|
+
headers=headers or {}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
response = self._broker.request(queue, message, timeout)
|
|
301
|
+
|
|
302
|
+
# Parse response body
|
|
303
|
+
response_body = response.body
|
|
304
|
+
if isinstance(response_body, str):
|
|
305
|
+
try:
|
|
306
|
+
response_body = json.loads(response_body)
|
|
307
|
+
except json.JSONDecodeError:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
return MessageResult(
|
|
311
|
+
success=True,
|
|
312
|
+
message_id=response.id,
|
|
313
|
+
data=response_body
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except TimeoutError as e:
|
|
317
|
+
return MessageResult(
|
|
318
|
+
success=False,
|
|
319
|
+
error=f"Request timed out: {e}"
|
|
320
|
+
)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
return MessageResult(
|
|
323
|
+
success=False,
|
|
324
|
+
error=str(e)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def subscribe(
|
|
328
|
+
self,
|
|
329
|
+
name: str,
|
|
330
|
+
topic: str = None,
|
|
331
|
+
topics: str = None,
|
|
332
|
+
queue: str = None,
|
|
333
|
+
handler: Callable = None,
|
|
334
|
+
ack: str = 'auto',
|
|
335
|
+
prefetch: int = 1
|
|
336
|
+
) -> str:
|
|
337
|
+
"""
|
|
338
|
+
Subscribe to a topic or start consuming from a queue.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
name: Subscription name
|
|
342
|
+
topic: Single topic pattern
|
|
343
|
+
topics: Comma-separated topic patterns
|
|
344
|
+
queue: Queue name (for consumption)
|
|
345
|
+
handler: Message handler function
|
|
346
|
+
ack: Ack mode ('auto' or 'manual')
|
|
347
|
+
prefetch: Number of messages to prefetch
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Subscription ID
|
|
351
|
+
"""
|
|
352
|
+
self._ensure_connected()
|
|
353
|
+
|
|
354
|
+
# Store handler for this subscription
|
|
355
|
+
self._handlers[name] = handler
|
|
356
|
+
|
|
357
|
+
def message_callback(msg: Message):
|
|
358
|
+
"""Internal callback that wraps the user handler."""
|
|
359
|
+
self._local.current_message = msg
|
|
360
|
+
|
|
361
|
+
# Parse message body
|
|
362
|
+
body = msg.body
|
|
363
|
+
if isinstance(body, str):
|
|
364
|
+
try:
|
|
365
|
+
body = json.loads(body)
|
|
366
|
+
except json.JSONDecodeError:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
# Create message context for handler
|
|
370
|
+
message_context = {
|
|
371
|
+
'id': msg.id,
|
|
372
|
+
'topic': msg.topic,
|
|
373
|
+
'queue': msg.queue,
|
|
374
|
+
'body': body,
|
|
375
|
+
'headers': msg.headers,
|
|
376
|
+
'timestamp': msg.timestamp
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
if handler:
|
|
381
|
+
handler(message_context)
|
|
382
|
+
|
|
383
|
+
# Auto-ack if configured
|
|
384
|
+
if ack == 'auto':
|
|
385
|
+
self._broker.ack(msg)
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
print(f"[MessageQueue] Handler error: {e}")
|
|
389
|
+
if ack == 'manual':
|
|
390
|
+
self._broker.nack(msg, requeue=True)
|
|
391
|
+
finally:
|
|
392
|
+
self._local.current_message = None
|
|
393
|
+
|
|
394
|
+
# Subscribe to topic(s) or consume from queue
|
|
395
|
+
if queue:
|
|
396
|
+
sub_id = self._broker.consume(queue, message_callback, prefetch)
|
|
397
|
+
elif topic:
|
|
398
|
+
sub_id = self._broker.subscribe(topic, message_callback)
|
|
399
|
+
elif topics:
|
|
400
|
+
# Subscribe to multiple topics
|
|
401
|
+
topic_list = [t.strip() for t in topics.split(',')]
|
|
402
|
+
sub_ids = []
|
|
403
|
+
for t in topic_list:
|
|
404
|
+
sid = self._broker.subscribe(t, message_callback)
|
|
405
|
+
sub_ids.append(sid)
|
|
406
|
+
sub_id = ','.join(sub_ids)
|
|
407
|
+
else:
|
|
408
|
+
raise MessageQueueError("Subscribe requires 'topic', 'topics', or 'queue'")
|
|
409
|
+
|
|
410
|
+
self._subscriptions[name] = sub_id
|
|
411
|
+
return sub_id
|
|
412
|
+
|
|
413
|
+
def unsubscribe(self, name: str) -> None:
|
|
414
|
+
"""
|
|
415
|
+
Unsubscribe from a topic or stop consuming.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
name: Subscription name
|
|
419
|
+
"""
|
|
420
|
+
if name in self._subscriptions:
|
|
421
|
+
sub_id = self._subscriptions[name]
|
|
422
|
+
|
|
423
|
+
# Handle multiple subscription IDs
|
|
424
|
+
for sid in sub_id.split(','):
|
|
425
|
+
self._broker.unsubscribe(sid.strip())
|
|
426
|
+
|
|
427
|
+
del self._subscriptions[name]
|
|
428
|
+
|
|
429
|
+
if name in self._handlers:
|
|
430
|
+
del self._handlers[name]
|
|
431
|
+
|
|
432
|
+
@property
|
|
433
|
+
def _current_message(self) -> Optional[Message]:
|
|
434
|
+
"""A mensagem desta thread. Mantido como propriedade porque o nome
|
|
435
|
+
e usado por testes e por codigo que le o estado do servico."""
|
|
436
|
+
return getattr(self._local, 'current_message', None)
|
|
437
|
+
|
|
438
|
+
def ack(self) -> None:
|
|
439
|
+
"""Acknowledge the current message (for manual ack mode)."""
|
|
440
|
+
mensagem = self._current_message
|
|
441
|
+
if mensagem and self._broker:
|
|
442
|
+
self._broker.ack(mensagem)
|
|
443
|
+
|
|
444
|
+
def nack(self, requeue: bool = True) -> None:
|
|
445
|
+
"""
|
|
446
|
+
Negative acknowledge the current message.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
requeue: If True, requeue the message
|
|
450
|
+
"""
|
|
451
|
+
mensagem = self._current_message
|
|
452
|
+
if mensagem and self._broker:
|
|
453
|
+
self._broker.nack(mensagem, requeue)
|
|
454
|
+
|
|
455
|
+
def declare_queue(
|
|
456
|
+
self,
|
|
457
|
+
name: str,
|
|
458
|
+
durable: bool = True,
|
|
459
|
+
exclusive: bool = False,
|
|
460
|
+
auto_delete: bool = False,
|
|
461
|
+
dead_letter_queue: str = None,
|
|
462
|
+
ttl: int = None
|
|
463
|
+
) -> MessageResult:
|
|
464
|
+
"""
|
|
465
|
+
Declare a queue.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
name: Queue name
|
|
469
|
+
durable: Survive broker restart
|
|
470
|
+
exclusive: Only this connection can access
|
|
471
|
+
auto_delete: Delete when last consumer disconnects
|
|
472
|
+
dead_letter_queue: Queue for rejected messages
|
|
473
|
+
ttl: Message TTL in milliseconds
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
MessageResult with operation status
|
|
477
|
+
"""
|
|
478
|
+
self._ensure_connected()
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
self._broker.declare_queue(
|
|
482
|
+
name=name,
|
|
483
|
+
durable=durable,
|
|
484
|
+
exclusive=exclusive,
|
|
485
|
+
auto_delete=auto_delete,
|
|
486
|
+
dead_letter_queue=dead_letter_queue,
|
|
487
|
+
ttl=ttl
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
return MessageResult(success=True)
|
|
491
|
+
|
|
492
|
+
except Exception as e:
|
|
493
|
+
return MessageResult(
|
|
494
|
+
success=False,
|
|
495
|
+
error=str(e)
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def purge_queue(self, name: str) -> MessageResult:
|
|
499
|
+
"""
|
|
500
|
+
Remove all messages from a queue.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
name: Queue name
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
MessageResult with count of purged messages
|
|
507
|
+
"""
|
|
508
|
+
self._ensure_connected()
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
count = self._broker.purge_queue(name)
|
|
512
|
+
return MessageResult(
|
|
513
|
+
success=True,
|
|
514
|
+
data={'purged_count': count}
|
|
515
|
+
)
|
|
516
|
+
except Exception as e:
|
|
517
|
+
return MessageResult(
|
|
518
|
+
success=False,
|
|
519
|
+
error=str(e)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def delete_queue(self, name: str) -> MessageResult:
|
|
523
|
+
"""
|
|
524
|
+
Delete a queue.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
name: Queue name
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
MessageResult with operation status
|
|
531
|
+
"""
|
|
532
|
+
self._ensure_connected()
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
self._broker.delete_queue(name)
|
|
536
|
+
return MessageResult(success=True)
|
|
537
|
+
except Exception as e:
|
|
538
|
+
return MessageResult(
|
|
539
|
+
success=False,
|
|
540
|
+
error=str(e)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
def get_queue_info(self, name: str) -> MessageResult:
|
|
544
|
+
"""
|
|
545
|
+
Get queue information.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
name: Queue name
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
MessageResult with queue info
|
|
552
|
+
"""
|
|
553
|
+
self._ensure_connected()
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
info = self._broker.get_queue_info(name)
|
|
557
|
+
return MessageResult(
|
|
558
|
+
success=True,
|
|
559
|
+
data={
|
|
560
|
+
'name': info.name,
|
|
561
|
+
'message_count': info.message_count,
|
|
562
|
+
'consumer_count': info.consumer_count,
|
|
563
|
+
'durable': info.durable,
|
|
564
|
+
'auto_delete': info.auto_delete
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
return MessageResult(
|
|
569
|
+
success=False,
|
|
570
|
+
error=str(e)
|
|
571
|
+
)
|