quantum-framework 0.18.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/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -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 +548 -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 +565 -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 +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -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_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -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 +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -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/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -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 +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -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 +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -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 +79 -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 +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -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/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 +164 -0
- quantum/core/xml_lines.py +81 -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 +25 -0
- quantum/runtime/action_handler.py +532 -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/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -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 +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -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 +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -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 +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -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/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -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/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis Message Broker Adapter
|
|
3
|
+
|
|
4
|
+
Provides a Redis-based implementation of the MessageBroker interface.
|
|
5
|
+
|
|
6
|
+
Uses:
|
|
7
|
+
- Redis Pub/Sub for topic-based messaging
|
|
8
|
+
- Redis Lists for queue-based messaging (LPUSH/BRPOP)
|
|
9
|
+
- JSON serialization for messages
|
|
10
|
+
|
|
11
|
+
Requires: redis package (pip install redis)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
from typing import Dict, Any, Optional, List
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import redis
|
|
26
|
+
REDIS_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
REDIS_AVAILABLE = False
|
|
29
|
+
|
|
30
|
+
from message_broker import (
|
|
31
|
+
MessageBroker, Message, QueueInfo, MessageHandler,
|
|
32
|
+
MessageBrokerError, ConnectionError, PublishError,
|
|
33
|
+
SubscribeError, QueueError
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class RedisSubscription:
|
|
39
|
+
"""Internal state for a Redis subscription"""
|
|
40
|
+
id: str
|
|
41
|
+
pattern: str
|
|
42
|
+
handler: MessageHandler
|
|
43
|
+
is_queue: bool = False
|
|
44
|
+
prefetch: int = 1
|
|
45
|
+
active: bool = True
|
|
46
|
+
thread: Optional[threading.Thread] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RedisAdapter(MessageBroker):
|
|
50
|
+
"""
|
|
51
|
+
Redis-based implementation of MessageBroker.
|
|
52
|
+
|
|
53
|
+
Features:
|
|
54
|
+
- Redis Pub/Sub for topic messaging
|
|
55
|
+
- Redis Lists (LPUSH/BRPOP) for queue messaging
|
|
56
|
+
- Pattern-based topic subscriptions
|
|
57
|
+
- Persistent queues (Redis persistence settings apply)
|
|
58
|
+
|
|
59
|
+
Configuration:
|
|
60
|
+
{
|
|
61
|
+
'host': 'localhost',
|
|
62
|
+
'port': 6379,
|
|
63
|
+
'db': 0,
|
|
64
|
+
'password': None,
|
|
65
|
+
'decode_responses': True,
|
|
66
|
+
'prefix': 'quantum:' # Key prefix for namespacing
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self):
|
|
71
|
+
if not REDIS_AVAILABLE:
|
|
72
|
+
raise ImportError(
|
|
73
|
+
"Redis adapter requires 'redis' package. "
|
|
74
|
+
"Install with: pip install redis"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self._client: Optional[redis.Redis] = None
|
|
78
|
+
self._pubsub: Optional[redis.client.PubSub] = None
|
|
79
|
+
self._connected = False
|
|
80
|
+
self._config: Dict[str, Any] = {}
|
|
81
|
+
self._prefix = 'quantum:'
|
|
82
|
+
|
|
83
|
+
self._lock = threading.RLock()
|
|
84
|
+
self._subscriptions: Dict[str, RedisSubscription] = {}
|
|
85
|
+
self._pending_acks: Dict[str, Message] = {}
|
|
86
|
+
self._reply_queues: Dict[str, Any] = {}
|
|
87
|
+
|
|
88
|
+
self._shutdown = threading.Event()
|
|
89
|
+
self._workers: List[threading.Thread] = []
|
|
90
|
+
|
|
91
|
+
def connect(self, config: Dict[str, Any] = None) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Connect to Redis.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
config: Redis connection configuration
|
|
97
|
+
"""
|
|
98
|
+
if config is None:
|
|
99
|
+
config = {}
|
|
100
|
+
|
|
101
|
+
self._config = config
|
|
102
|
+
self._prefix = config.get('prefix', 'quantum:')
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
self._client = redis.Redis(
|
|
106
|
+
host=config.get('host', 'localhost'),
|
|
107
|
+
port=config.get('port', 6379),
|
|
108
|
+
db=config.get('db', 0),
|
|
109
|
+
password=config.get('password'),
|
|
110
|
+
decode_responses=True
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Test connection
|
|
114
|
+
self._client.ping()
|
|
115
|
+
|
|
116
|
+
self._pubsub = self._client.pubsub()
|
|
117
|
+
self._connected = True
|
|
118
|
+
self._shutdown.clear()
|
|
119
|
+
|
|
120
|
+
except redis.ConnectionError as e:
|
|
121
|
+
raise ConnectionError(f"Failed to connect to Redis: {e}")
|
|
122
|
+
|
|
123
|
+
def disconnect(self) -> None:
|
|
124
|
+
"""Disconnect from Redis and clean up resources."""
|
|
125
|
+
with self._lock:
|
|
126
|
+
if not self._connected:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
self._shutdown.set()
|
|
130
|
+
|
|
131
|
+
# Stop subscription threads
|
|
132
|
+
for sub in self._subscriptions.values():
|
|
133
|
+
sub.active = False
|
|
134
|
+
if sub.thread and sub.thread.is_alive():
|
|
135
|
+
sub.thread.join(timeout=1.0)
|
|
136
|
+
|
|
137
|
+
self._subscriptions.clear()
|
|
138
|
+
self._workers.clear()
|
|
139
|
+
|
|
140
|
+
if self._pubsub:
|
|
141
|
+
self._pubsub.close()
|
|
142
|
+
self._pubsub = None
|
|
143
|
+
|
|
144
|
+
if self._client:
|
|
145
|
+
self._client.close()
|
|
146
|
+
self._client = None
|
|
147
|
+
|
|
148
|
+
self._connected = False
|
|
149
|
+
|
|
150
|
+
def is_connected(self) -> bool:
|
|
151
|
+
"""Check if connected to Redis."""
|
|
152
|
+
if not self._connected or not self._client:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
self._client.ping()
|
|
157
|
+
return True
|
|
158
|
+
except redis.ConnectionError:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
def publish(self, topic: str, message: Message) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Publish a message to a topic using Redis Pub/Sub.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
topic: Topic name
|
|
167
|
+
message: Message to publish
|
|
168
|
+
"""
|
|
169
|
+
if not self._connected:
|
|
170
|
+
raise ConnectionError("Not connected to Redis")
|
|
171
|
+
|
|
172
|
+
message.topic = topic
|
|
173
|
+
channel = f"{self._prefix}topic:{topic}"
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
self._client.publish(channel, message.to_json())
|
|
177
|
+
except redis.RedisError as e:
|
|
178
|
+
raise PublishError(f"Failed to publish message: {e}")
|
|
179
|
+
|
|
180
|
+
def send(self, queue_name: str, message: Message) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Send a message to a queue using Redis Lists.
|
|
183
|
+
|
|
184
|
+
Uses LPUSH to add message to the left of the list.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
queue_name: Queue name
|
|
188
|
+
message: Message to send
|
|
189
|
+
"""
|
|
190
|
+
if not self._connected:
|
|
191
|
+
raise ConnectionError("Not connected to Redis")
|
|
192
|
+
|
|
193
|
+
message.queue = queue_name
|
|
194
|
+
key = f"{self._prefix}queue:{queue_name}"
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# Store message with metadata
|
|
198
|
+
self._client.lpush(key, message.to_json())
|
|
199
|
+
|
|
200
|
+
# Set TTL if queue has one configured
|
|
201
|
+
ttl_key = f"{self._prefix}queue_ttl:{queue_name}"
|
|
202
|
+
ttl = self._client.get(ttl_key)
|
|
203
|
+
if ttl:
|
|
204
|
+
# Set expiry on individual message (stored separately)
|
|
205
|
+
msg_key = f"{self._prefix}msg:{message.id}"
|
|
206
|
+
self._client.setex(msg_key, int(int(ttl) / 1000), message.to_json())
|
|
207
|
+
|
|
208
|
+
except redis.RedisError as e:
|
|
209
|
+
raise PublishError(f"Failed to send message: {e}")
|
|
210
|
+
|
|
211
|
+
def request(self, queue_name: str, message: Message, timeout: int = 5000) -> Message:
|
|
212
|
+
"""
|
|
213
|
+
Send a request and wait for a reply.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
queue_name: Queue to send request to
|
|
217
|
+
message: Request message
|
|
218
|
+
timeout: Timeout in milliseconds
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Response message
|
|
222
|
+
"""
|
|
223
|
+
if not self._connected:
|
|
224
|
+
raise ConnectionError("Not connected to Redis")
|
|
225
|
+
|
|
226
|
+
# Create unique reply queue
|
|
227
|
+
reply_queue = f"_reply_{uuid.uuid4().hex}"
|
|
228
|
+
correlation_id = str(uuid.uuid4())
|
|
229
|
+
|
|
230
|
+
message.reply_to = reply_queue
|
|
231
|
+
message.correlation_id = correlation_id
|
|
232
|
+
|
|
233
|
+
reply_key = f"{self._prefix}queue:{reply_queue}"
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# Send request
|
|
237
|
+
self.send(queue_name, message)
|
|
238
|
+
|
|
239
|
+
# Wait for reply using BRPOP
|
|
240
|
+
timeout_seconds = timeout / 1000.0
|
|
241
|
+
result = self._client.brpop(reply_key, timeout=timeout_seconds)
|
|
242
|
+
|
|
243
|
+
if result is None:
|
|
244
|
+
raise TimeoutError(f"Request timed out after {timeout}ms")
|
|
245
|
+
|
|
246
|
+
_, reply_json = result
|
|
247
|
+
reply = Message.from_json(reply_json)
|
|
248
|
+
|
|
249
|
+
return reply
|
|
250
|
+
|
|
251
|
+
except redis.RedisError as e:
|
|
252
|
+
raise PublishError(f"Request failed: {e}")
|
|
253
|
+
finally:
|
|
254
|
+
# Clean up reply queue
|
|
255
|
+
self._client.delete(reply_key)
|
|
256
|
+
|
|
257
|
+
def subscribe(self, topic: str, handler: MessageHandler) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Subscribe to a topic pattern.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
topic: Topic pattern (supports * wildcards)
|
|
263
|
+
handler: Callback for received messages
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Subscription ID
|
|
267
|
+
"""
|
|
268
|
+
if not self._connected:
|
|
269
|
+
raise ConnectionError("Not connected to Redis")
|
|
270
|
+
|
|
271
|
+
sub_id = f"sub_{uuid.uuid4().hex}"
|
|
272
|
+
pattern = f"{self._prefix}topic:{topic}"
|
|
273
|
+
|
|
274
|
+
subscription = RedisSubscription(
|
|
275
|
+
id=sub_id,
|
|
276
|
+
pattern=topic,
|
|
277
|
+
handler=handler,
|
|
278
|
+
is_queue=False
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
with self._lock:
|
|
282
|
+
self._subscriptions[sub_id] = subscription
|
|
283
|
+
|
|
284
|
+
# Start subscription thread
|
|
285
|
+
thread = threading.Thread(
|
|
286
|
+
target=self._pubsub_worker,
|
|
287
|
+
args=(subscription, pattern),
|
|
288
|
+
daemon=True,
|
|
289
|
+
name=f"Redis-Sub-{sub_id}"
|
|
290
|
+
)
|
|
291
|
+
subscription.thread = thread
|
|
292
|
+
thread.start()
|
|
293
|
+
|
|
294
|
+
return sub_id
|
|
295
|
+
|
|
296
|
+
def consume(self, queue_name: str, handler: MessageHandler, prefetch: int = 1) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Start consuming messages from a queue.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
queue_name: Queue name
|
|
302
|
+
handler: Callback for received messages
|
|
303
|
+
prefetch: Number of messages to prefetch (not fully supported in Redis)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Consumer ID
|
|
307
|
+
"""
|
|
308
|
+
if not self._connected:
|
|
309
|
+
raise ConnectionError("Not connected to Redis")
|
|
310
|
+
|
|
311
|
+
consumer_id = f"consumer_{uuid.uuid4().hex}"
|
|
312
|
+
|
|
313
|
+
subscription = RedisSubscription(
|
|
314
|
+
id=consumer_id,
|
|
315
|
+
pattern=queue_name,
|
|
316
|
+
handler=handler,
|
|
317
|
+
is_queue=True,
|
|
318
|
+
prefetch=prefetch
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
with self._lock:
|
|
322
|
+
self._subscriptions[consumer_id] = subscription
|
|
323
|
+
|
|
324
|
+
# Start consumer thread
|
|
325
|
+
thread = threading.Thread(
|
|
326
|
+
target=self._queue_worker,
|
|
327
|
+
args=(subscription,),
|
|
328
|
+
daemon=True,
|
|
329
|
+
name=f"Redis-Consumer-{consumer_id}"
|
|
330
|
+
)
|
|
331
|
+
subscription.thread = thread
|
|
332
|
+
thread.start()
|
|
333
|
+
|
|
334
|
+
return consumer_id
|
|
335
|
+
|
|
336
|
+
def unsubscribe(self, subscription_id: str) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Unsubscribe or stop consuming.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
subscription_id: Subscription or consumer ID
|
|
342
|
+
"""
|
|
343
|
+
with self._lock:
|
|
344
|
+
if subscription_id in self._subscriptions:
|
|
345
|
+
sub = self._subscriptions[subscription_id]
|
|
346
|
+
sub.active = False
|
|
347
|
+
|
|
348
|
+
if sub.thread and sub.thread.is_alive():
|
|
349
|
+
sub.thread.join(timeout=1.0)
|
|
350
|
+
|
|
351
|
+
del self._subscriptions[subscription_id]
|
|
352
|
+
|
|
353
|
+
def ack(self, message: Message) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Acknowledge message processing.
|
|
356
|
+
|
|
357
|
+
For Redis, this removes the message from pending.
|
|
358
|
+
"""
|
|
359
|
+
with self._lock:
|
|
360
|
+
self._pending_acks.pop(message.id, None)
|
|
361
|
+
|
|
362
|
+
def nack(self, message: Message, requeue: bool = True) -> None:
|
|
363
|
+
"""
|
|
364
|
+
Negative acknowledge - reject a message.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
message: Message to reject
|
|
368
|
+
requeue: If True, requeue the message
|
|
369
|
+
"""
|
|
370
|
+
with self._lock:
|
|
371
|
+
self._pending_acks.pop(message.id, None)
|
|
372
|
+
|
|
373
|
+
if requeue and message.queue:
|
|
374
|
+
# Requeue by pushing back to the queue
|
|
375
|
+
key = f"{self._prefix}queue:{message.queue}"
|
|
376
|
+
try:
|
|
377
|
+
self._client.rpush(key, message.to_json())
|
|
378
|
+
except redis.RedisError:
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
def declare_queue(
|
|
382
|
+
self,
|
|
383
|
+
name: str,
|
|
384
|
+
durable: bool = True,
|
|
385
|
+
exclusive: bool = False,
|
|
386
|
+
auto_delete: bool = False,
|
|
387
|
+
dead_letter_queue: Optional[str] = None,
|
|
388
|
+
ttl: Optional[int] = None
|
|
389
|
+
) -> None:
|
|
390
|
+
"""
|
|
391
|
+
Declare a queue.
|
|
392
|
+
|
|
393
|
+
For Redis, queues are created implicitly. This stores metadata.
|
|
394
|
+
"""
|
|
395
|
+
if not self._connected:
|
|
396
|
+
raise ConnectionError("Not connected to Redis")
|
|
397
|
+
|
|
398
|
+
meta_key = f"{self._prefix}queue_meta:{name}"
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
meta = {
|
|
402
|
+
'durable': durable,
|
|
403
|
+
'exclusive': exclusive,
|
|
404
|
+
'auto_delete': auto_delete,
|
|
405
|
+
'dead_letter_queue': dead_letter_queue,
|
|
406
|
+
'ttl': ttl
|
|
407
|
+
}
|
|
408
|
+
self._client.hset(meta_key, mapping={
|
|
409
|
+
k: json.dumps(v) for k, v in meta.items()
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
if ttl:
|
|
413
|
+
ttl_key = f"{self._prefix}queue_ttl:{name}"
|
|
414
|
+
self._client.set(ttl_key, str(ttl))
|
|
415
|
+
|
|
416
|
+
except redis.RedisError as e:
|
|
417
|
+
raise QueueError(f"Failed to declare queue: {e}")
|
|
418
|
+
|
|
419
|
+
def purge_queue(self, name: str) -> int:
|
|
420
|
+
"""
|
|
421
|
+
Remove all messages from a queue.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
name: Queue name
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Number of messages purged
|
|
428
|
+
"""
|
|
429
|
+
if not self._connected:
|
|
430
|
+
raise ConnectionError("Not connected to Redis")
|
|
431
|
+
|
|
432
|
+
key = f"{self._prefix}queue:{name}"
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
count = self._client.llen(key)
|
|
436
|
+
self._client.delete(key)
|
|
437
|
+
return count
|
|
438
|
+
except redis.RedisError as e:
|
|
439
|
+
raise QueueError(f"Failed to purge queue: {e}")
|
|
440
|
+
|
|
441
|
+
def delete_queue(self, name: str) -> None:
|
|
442
|
+
"""
|
|
443
|
+
Delete a queue and its metadata.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
name: Queue name
|
|
447
|
+
"""
|
|
448
|
+
if not self._connected:
|
|
449
|
+
raise ConnectionError("Not connected to Redis")
|
|
450
|
+
|
|
451
|
+
keys = [
|
|
452
|
+
f"{self._prefix}queue:{name}",
|
|
453
|
+
f"{self._prefix}queue_meta:{name}",
|
|
454
|
+
f"{self._prefix}queue_ttl:{name}"
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
self._client.delete(*keys)
|
|
459
|
+
except redis.RedisError as e:
|
|
460
|
+
raise QueueError(f"Failed to delete queue: {e}")
|
|
461
|
+
|
|
462
|
+
def get_queue_info(self, name: str) -> QueueInfo:
|
|
463
|
+
"""
|
|
464
|
+
Get queue statistics.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
name: Queue name
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
QueueInfo with statistics
|
|
471
|
+
"""
|
|
472
|
+
if not self._connected:
|
|
473
|
+
raise ConnectionError("Not connected to Redis")
|
|
474
|
+
|
|
475
|
+
key = f"{self._prefix}queue:{name}"
|
|
476
|
+
meta_key = f"{self._prefix}queue_meta:{name}"
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
message_count = self._client.llen(key)
|
|
480
|
+
|
|
481
|
+
# Get metadata
|
|
482
|
+
meta = self._client.hgetall(meta_key)
|
|
483
|
+
durable = json.loads(meta.get('durable', 'true'))
|
|
484
|
+
auto_delete = json.loads(meta.get('auto_delete', 'false'))
|
|
485
|
+
|
|
486
|
+
# Count active consumers for this queue
|
|
487
|
+
consumer_count = sum(
|
|
488
|
+
1 for s in self._subscriptions.values()
|
|
489
|
+
if s.is_queue and s.pattern == name and s.active
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return QueueInfo(
|
|
493
|
+
name=name,
|
|
494
|
+
message_count=message_count,
|
|
495
|
+
consumer_count=consumer_count,
|
|
496
|
+
durable=durable,
|
|
497
|
+
auto_delete=auto_delete
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
except redis.RedisError as e:
|
|
501
|
+
raise QueueError(f"Failed to get queue info: {e}")
|
|
502
|
+
|
|
503
|
+
def _pubsub_worker(self, subscription: RedisSubscription, pattern: str) -> None:
|
|
504
|
+
"""Background worker for Redis Pub/Sub subscriptions."""
|
|
505
|
+
try:
|
|
506
|
+
# Create a new pubsub instance for this subscription
|
|
507
|
+
pubsub = self._client.pubsub()
|
|
508
|
+
|
|
509
|
+
if '*' in pattern or '?' in pattern:
|
|
510
|
+
pubsub.psubscribe(pattern)
|
|
511
|
+
else:
|
|
512
|
+
pubsub.subscribe(pattern)
|
|
513
|
+
|
|
514
|
+
for message in pubsub.listen():
|
|
515
|
+
if not subscription.active or self._shutdown.is_set():
|
|
516
|
+
break
|
|
517
|
+
|
|
518
|
+
if message['type'] in ('message', 'pmessage'):
|
|
519
|
+
try:
|
|
520
|
+
data = message['data']
|
|
521
|
+
if isinstance(data, str):
|
|
522
|
+
msg = Message.from_json(data)
|
|
523
|
+
subscription.handler(msg)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
print(f"[Redis] Pub/Sub handler error: {e}")
|
|
526
|
+
|
|
527
|
+
pubsub.close()
|
|
528
|
+
|
|
529
|
+
except Exception as e:
|
|
530
|
+
print(f"[Redis] Pub/Sub worker error: {e}")
|
|
531
|
+
|
|
532
|
+
def _queue_worker(self, subscription: RedisSubscription) -> None:
|
|
533
|
+
"""Background worker for Redis queue consumption."""
|
|
534
|
+
key = f"{self._prefix}queue:{subscription.pattern}"
|
|
535
|
+
|
|
536
|
+
while subscription.active and not self._shutdown.is_set():
|
|
537
|
+
try:
|
|
538
|
+
# BRPOP with 1 second timeout for graceful shutdown
|
|
539
|
+
result = self._client.brpop(key, timeout=1)
|
|
540
|
+
|
|
541
|
+
if result is None:
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
_, msg_json = result
|
|
545
|
+
msg = Message.from_json(msg_json)
|
|
546
|
+
|
|
547
|
+
# Track pending ack
|
|
548
|
+
with self._lock:
|
|
549
|
+
self._pending_acks[msg.id] = msg
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
subscription.handler(msg)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
print(f"[Redis] Queue handler error: {e}")
|
|
555
|
+
# Requeue on error
|
|
556
|
+
self.nack(msg, requeue=True)
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
if subscription.active:
|
|
560
|
+
print(f"[Redis] Queue worker error: {e}")
|
|
561
|
+
time.sleep(1)
|
|
562
|
+
|
|
563
|
+
def reply(self, original_message: Message, response: Message) -> None:
|
|
564
|
+
"""
|
|
565
|
+
Send a reply to a request message.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
original_message: The request message
|
|
569
|
+
response: The response message
|
|
570
|
+
"""
|
|
571
|
+
if not original_message.reply_to:
|
|
572
|
+
raise PublishError("Original message has no reply_to queue")
|
|
573
|
+
|
|
574
|
+
response.correlation_id = original_message.correlation_id
|
|
575
|
+
response.queue = original_message.reply_to
|
|
576
|
+
|
|
577
|
+
reply_key = f"{self._prefix}queue:{original_message.reply_to}"
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
self._client.lpush(reply_key, response.to_json())
|
|
581
|
+
except redis.RedisError as e:
|
|
582
|
+
raise PublishError(f"Failed to send reply: {e}")
|