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,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Job Executor - Execute q:job statements
|
|
3
|
+
|
|
4
|
+
Handles job queue operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Dict, Type
|
|
8
|
+
from quantum.runtime.executors.base import BaseExecutor, ExecutorError
|
|
9
|
+
from quantum.core.ast_nodes import JobNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JobExecutor(BaseExecutor):
|
|
13
|
+
"""
|
|
14
|
+
Executor for q:job statements.
|
|
15
|
+
|
|
16
|
+
Supports:
|
|
17
|
+
- Job definition
|
|
18
|
+
- Job dispatch (enqueue)
|
|
19
|
+
- Batch job dispatch
|
|
20
|
+
- Priority and delay
|
|
21
|
+
- Retry with backoff
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def handles(self) -> List[Type]:
|
|
26
|
+
return [JobNode]
|
|
27
|
+
|
|
28
|
+
def execute(self, node: JobNode, exec_context) -> Any:
|
|
29
|
+
"""
|
|
30
|
+
Execute job management.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
node: JobNode with job configuration
|
|
34
|
+
exec_context: Execution context
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Job operation result dict
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
context = exec_context.get_all_variables()
|
|
41
|
+
|
|
42
|
+
# Execute based on action
|
|
43
|
+
if node.action == 'define':
|
|
44
|
+
result = self._execute_define(node, context, exec_context)
|
|
45
|
+
elif node.action == 'dispatch':
|
|
46
|
+
result = self._execute_dispatch(node, context, exec_context)
|
|
47
|
+
elif node.action == 'batch':
|
|
48
|
+
result = self._execute_batch(node, context, exec_context)
|
|
49
|
+
else:
|
|
50
|
+
raise ExecutorError(f"Unknown job action: {node.action}")
|
|
51
|
+
|
|
52
|
+
# Store result with both naming conventions for compatibility
|
|
53
|
+
exec_context.set_variable(f"dispatched_job_{node.name}", result, scope="component")
|
|
54
|
+
exec_context.set_variable(f"job_{node.name}", result, scope="component")
|
|
55
|
+
|
|
56
|
+
# Also store in runtime context if available
|
|
57
|
+
if hasattr(self._runtime, 'context'):
|
|
58
|
+
self._runtime.context[f"dispatched_job_{node.name}"] = result
|
|
59
|
+
self._runtime.context[f"job_{node.name}"] = result
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise ExecutorError(f"Job execution error: {e}")
|
|
65
|
+
|
|
66
|
+
def _execute_define(self, node: JobNode, context: Dict[str, Any], exec_context) -> Dict:
|
|
67
|
+
"""Define a job (register handler)."""
|
|
68
|
+
name = node.name
|
|
69
|
+
queue = node.queue
|
|
70
|
+
|
|
71
|
+
# Get job_executor from runtime (same as legacy code)
|
|
72
|
+
job_executor = self._runtime.job_executor
|
|
73
|
+
|
|
74
|
+
# Create job handler that executes the job body
|
|
75
|
+
def job_handler(params: Dict[str, Any]):
|
|
76
|
+
from quantum.runtime.execution_context import ExecutionContext as EC
|
|
77
|
+
child_context = EC()
|
|
78
|
+
for param_name, param_value in params.items():
|
|
79
|
+
child_context.set_variable(param_name, param_value, scope="component")
|
|
80
|
+
|
|
81
|
+
# Execute job body statements
|
|
82
|
+
for statement in node.body:
|
|
83
|
+
self._runtime._execute_statement(statement, child_context)
|
|
84
|
+
|
|
85
|
+
# Register handler with job queue
|
|
86
|
+
job_executor.job_queue.register_handler(name, job_handler)
|
|
87
|
+
|
|
88
|
+
# Start worker for the queue if not already running
|
|
89
|
+
job_executor.job_queue.start_worker(queue)
|
|
90
|
+
|
|
91
|
+
result = {
|
|
92
|
+
'name': name,
|
|
93
|
+
'action': 'define',
|
|
94
|
+
'queue': queue,
|
|
95
|
+
'success': True
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
def _execute_dispatch(self, node: JobNode, context: Dict[str, Any], exec_context) -> Dict:
|
|
101
|
+
"""Dispatch a job to the queue."""
|
|
102
|
+
name = node.name
|
|
103
|
+
|
|
104
|
+
# Get job_executor from runtime (same as legacy code)
|
|
105
|
+
job_executor = self._runtime.job_executor
|
|
106
|
+
|
|
107
|
+
# Build params from job params
|
|
108
|
+
params = {}
|
|
109
|
+
for param in node.params:
|
|
110
|
+
param_value = getattr(param, 'default', None)
|
|
111
|
+
if param.name in context:
|
|
112
|
+
param_value = context[param.name]
|
|
113
|
+
# If param has value attribute, resolve it
|
|
114
|
+
if hasattr(param, 'value') and param.value:
|
|
115
|
+
param_value = self.apply_databinding(param.value, context)
|
|
116
|
+
params[param.name] = param_value
|
|
117
|
+
|
|
118
|
+
# Dispatch job to queue
|
|
119
|
+
job_id = job_executor.job_queue.dispatch(
|
|
120
|
+
name=name,
|
|
121
|
+
queue=node.queue,
|
|
122
|
+
params=params,
|
|
123
|
+
delay=node.delay,
|
|
124
|
+
priority=node.priority,
|
|
125
|
+
attempts=node.attempts,
|
|
126
|
+
backoff=node.backoff
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = {
|
|
130
|
+
'job_id': job_id,
|
|
131
|
+
'name': name,
|
|
132
|
+
'action': 'dispatch',
|
|
133
|
+
'queue': node.queue,
|
|
134
|
+
'status': 'queued',
|
|
135
|
+
'success': True
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
def _execute_batch(self, node: JobNode, context: Dict[str, Any], exec_context) -> Dict:
|
|
141
|
+
"""Dispatch multiple jobs as a batch."""
|
|
142
|
+
name = node.name
|
|
143
|
+
|
|
144
|
+
# Get job_executor from runtime (same as legacy code)
|
|
145
|
+
job_executor = self._runtime.job_executor
|
|
146
|
+
|
|
147
|
+
# Get tasks from node
|
|
148
|
+
tasks = node.tasks if node.tasks else []
|
|
149
|
+
|
|
150
|
+
# Resolve tasks
|
|
151
|
+
resolved_tasks = []
|
|
152
|
+
for task in tasks:
|
|
153
|
+
resolved_task = {}
|
|
154
|
+
for key, value in task.items():
|
|
155
|
+
if isinstance(value, str):
|
|
156
|
+
resolved_task[key] = self.apply_databinding(value, context)
|
|
157
|
+
else:
|
|
158
|
+
resolved_task[key] = value
|
|
159
|
+
resolved_tasks.append(resolved_task)
|
|
160
|
+
|
|
161
|
+
# Dispatch batch
|
|
162
|
+
job_ids = []
|
|
163
|
+
for task in resolved_tasks:
|
|
164
|
+
job_id = job_executor.job_queue.dispatch(
|
|
165
|
+
name=name,
|
|
166
|
+
queue=node.queue,
|
|
167
|
+
params=task,
|
|
168
|
+
priority=node.priority
|
|
169
|
+
)
|
|
170
|
+
job_ids.append(job_id)
|
|
171
|
+
|
|
172
|
+
result = {
|
|
173
|
+
'job_ids': job_ids,
|
|
174
|
+
'name': name,
|
|
175
|
+
'action': 'batch',
|
|
176
|
+
'queue': node.queue,
|
|
177
|
+
'count': len(job_ids),
|
|
178
|
+
'success': True
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def _param_to_dict(self, param) -> Dict:
|
|
184
|
+
"""Convert param node to dict."""
|
|
185
|
+
return {
|
|
186
|
+
'name': param.name,
|
|
187
|
+
'type': getattr(param, 'type', 'string'),
|
|
188
|
+
'required': getattr(param, 'required', False),
|
|
189
|
+
'default': getattr(param, 'default', None)
|
|
190
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schedule Executor - Execute q:schedule statements
|
|
3
|
+
|
|
4
|
+
Handles scheduled task execution (cron-like).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, List, Dict, Type
|
|
9
|
+
from quantum.runtime.executors.base import BaseExecutor, ExecutorError
|
|
10
|
+
from quantum.core.ast_nodes import ScheduleNode
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def schedule_info_to_dict(info) -> Dict[str, Any]:
|
|
16
|
+
"""Convert a ScheduleInfo dataclass to a template-friendly dict."""
|
|
17
|
+
def _iso(value):
|
|
18
|
+
return value.isoformat() if hasattr(value, 'isoformat') else value
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
'name': info.name,
|
|
22
|
+
'triggerType': info.trigger_type,
|
|
23
|
+
'triggerInfo': info.trigger_info,
|
|
24
|
+
'nextRun': _iso(info.next_run),
|
|
25
|
+
'enabled': info.enabled,
|
|
26
|
+
'runCount': info.run_count,
|
|
27
|
+
'lastRun': _iso(info.last_run),
|
|
28
|
+
'lastError': info.last_error,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ScheduleExecutor(BaseExecutor):
|
|
33
|
+
"""
|
|
34
|
+
Executor for q:schedule statements.
|
|
35
|
+
|
|
36
|
+
Supports:
|
|
37
|
+
- Interval-based scheduling (30s, 5m, 1h, 1d)
|
|
38
|
+
- Cron expression scheduling
|
|
39
|
+
- One-time scheduled execution
|
|
40
|
+
- Timezone support
|
|
41
|
+
- Retry configuration
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def handles(self) -> List[Type]:
|
|
46
|
+
return [ScheduleNode]
|
|
47
|
+
|
|
48
|
+
def execute(self, node: ScheduleNode, exec_context) -> Any:
|
|
49
|
+
"""
|
|
50
|
+
Execute schedule management.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
node: ScheduleNode with schedule configuration
|
|
54
|
+
exec_context: Execution context
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Schedule operation result dict
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
context = exec_context.get_all_variables()
|
|
61
|
+
|
|
62
|
+
# Execute based on action
|
|
63
|
+
if node.action == 'run':
|
|
64
|
+
result = self._execute_run(node, context, exec_context)
|
|
65
|
+
elif node.action == 'pause':
|
|
66
|
+
result = self._execute_pause(node.name)
|
|
67
|
+
elif node.action == 'resume':
|
|
68
|
+
result = self._execute_resume(node.name)
|
|
69
|
+
elif node.action == 'delete':
|
|
70
|
+
result = self._execute_delete(node.name)
|
|
71
|
+
else:
|
|
72
|
+
raise ExecutorError(f"Unknown schedule action: {node.action}")
|
|
73
|
+
|
|
74
|
+
# Store result
|
|
75
|
+
exec_context.set_variable(f"{node.name}_schedule", result, scope="component")
|
|
76
|
+
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
except ExecutorError:
|
|
80
|
+
raise
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise ExecutorError(f"Schedule execution error: {e}")
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def _schedule_service(self):
|
|
86
|
+
"""The real scheduling service.
|
|
87
|
+
|
|
88
|
+
There is no services.scheduler — schedules live on the JobExecutor
|
|
89
|
+
facade as services.job_executor.schedule (a ScheduleService).
|
|
90
|
+
"""
|
|
91
|
+
return self.services.job_executor.schedule
|
|
92
|
+
|
|
93
|
+
def _execute_run(self, node: ScheduleNode, context: Dict[str, Any], exec_context) -> Dict:
|
|
94
|
+
"""Create/update a scheduled task."""
|
|
95
|
+
interval = self.apply_databinding(node.interval, context) if node.interval else None
|
|
96
|
+
cron = self.apply_databinding(node.cron, context) if node.cron else None
|
|
97
|
+
at = self.apply_databinding(node.at, context) if node.at else None
|
|
98
|
+
|
|
99
|
+
body = node.body
|
|
100
|
+
|
|
101
|
+
def run_scheduled_body():
|
|
102
|
+
"""Runs on the scheduler thread — must not raise into APScheduler."""
|
|
103
|
+
try:
|
|
104
|
+
return self.run_body_in_child_context(body)
|
|
105
|
+
except Exception as exc: # noqa: BLE001 - reported via ScheduleInfo.last_error
|
|
106
|
+
logger.error(f"Scheduled task '{node.name}' failed: {exc}")
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
info = self._schedule_service.add_schedule(
|
|
110
|
+
name=node.name,
|
|
111
|
+
callback=run_scheduled_body,
|
|
112
|
+
interval=interval,
|
|
113
|
+
cron=cron,
|
|
114
|
+
at=at,
|
|
115
|
+
timezone=node.timezone or 'UTC',
|
|
116
|
+
enabled=node.enabled,
|
|
117
|
+
overlap=node.overlap,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return schedule_info_to_dict(info)
|
|
121
|
+
|
|
122
|
+
def _execute_pause(self, name: str) -> Dict:
|
|
123
|
+
"""Pause a scheduled task."""
|
|
124
|
+
return {'name': name, 'action': 'pause', 'success': self._schedule_service.pause_schedule(name)}
|
|
125
|
+
|
|
126
|
+
def _execute_resume(self, name: str) -> Dict:
|
|
127
|
+
"""Resume a paused scheduled task."""
|
|
128
|
+
return {'name': name, 'action': 'resume', 'success': self._schedule_service.resume_schedule(name)}
|
|
129
|
+
|
|
130
|
+
def _execute_delete(self, name: str) -> Dict:
|
|
131
|
+
"""Delete a scheduled task."""
|
|
132
|
+
return {'name': name, 'action': 'delete', 'success': self._schedule_service.remove_schedule(name)}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thread Executor - Execute q:thread statements
|
|
3
|
+
|
|
4
|
+
Handles async thread execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, List, Dict, Type
|
|
9
|
+
from quantum.runtime.executors.base import BaseExecutor, ExecutorError
|
|
10
|
+
from quantum.core.ast_nodes import ThreadNode
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def thread_info_to_dict(info) -> Dict[str, Any]:
|
|
16
|
+
"""Convert a ThreadInfo dataclass to a template-friendly dict."""
|
|
17
|
+
def _iso(value):
|
|
18
|
+
return value.isoformat() if hasattr(value, 'isoformat') else value
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
'name': info.name,
|
|
22
|
+
'priority': info.priority,
|
|
23
|
+
'startedAt': _iso(info.started_at),
|
|
24
|
+
'status': info.status,
|
|
25
|
+
'timeout': info.timeout,
|
|
26
|
+
'result': info.result,
|
|
27
|
+
'error': info.error,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ThreadExecutor(BaseExecutor):
|
|
32
|
+
"""
|
|
33
|
+
Executor for q:thread statements.
|
|
34
|
+
|
|
35
|
+
Supports:
|
|
36
|
+
- Async thread execution
|
|
37
|
+
- Thread join (wait for completion)
|
|
38
|
+
- Thread termination
|
|
39
|
+
- Priority levels
|
|
40
|
+
- Timeout and callbacks
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def handles(self) -> List[Type]:
|
|
45
|
+
return [ThreadNode]
|
|
46
|
+
|
|
47
|
+
def execute(self, node: ThreadNode, exec_context) -> Any:
|
|
48
|
+
"""
|
|
49
|
+
Execute thread management.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
node: ThreadNode with thread configuration
|
|
53
|
+
exec_context: Execution context
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Thread operation result dict
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
context = exec_context.get_all_variables()
|
|
60
|
+
|
|
61
|
+
# Execute based on action
|
|
62
|
+
if node.action == 'run':
|
|
63
|
+
result = self._execute_run(node, context, exec_context)
|
|
64
|
+
elif node.action == 'join':
|
|
65
|
+
result = self._execute_join(node.name)
|
|
66
|
+
elif node.action == 'terminate':
|
|
67
|
+
result = self._execute_terminate(node.name)
|
|
68
|
+
else:
|
|
69
|
+
raise ExecutorError(f"Unknown thread action: {node.action}")
|
|
70
|
+
|
|
71
|
+
# Store result
|
|
72
|
+
exec_context.set_variable(f"{node.name}_thread", result, scope="component")
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
except ExecutorError:
|
|
77
|
+
raise
|
|
78
|
+
except Exception as e:
|
|
79
|
+
raise ExecutorError(f"Thread execution error: {e}")
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def _thread_service(self):
|
|
83
|
+
"""The real threading service.
|
|
84
|
+
|
|
85
|
+
There is no services.threading — threads live on the JobExecutor
|
|
86
|
+
facade as services.job_executor.thread (a ThreadService).
|
|
87
|
+
"""
|
|
88
|
+
return self.services.job_executor.thread
|
|
89
|
+
|
|
90
|
+
def _execute_run(self, node: ThreadNode, context: Dict[str, Any], exec_context) -> Dict:
|
|
91
|
+
"""Start a new thread."""
|
|
92
|
+
body = node.body
|
|
93
|
+
# Snapshot the caller's variables: the thread runs after this
|
|
94
|
+
# component's own execution has moved on.
|
|
95
|
+
snapshot = dict(context)
|
|
96
|
+
|
|
97
|
+
def run_thread_body():
|
|
98
|
+
try:
|
|
99
|
+
return self.run_body_in_child_context(body, snapshot)
|
|
100
|
+
except Exception as exc: # noqa: BLE001 - surfaced via ThreadInfo.error
|
|
101
|
+
logger.error(f"Thread '{node.name}' failed: {exc}")
|
|
102
|
+
raise
|
|
103
|
+
|
|
104
|
+
info = self._thread_service.run_thread(
|
|
105
|
+
name=node.name,
|
|
106
|
+
callback=run_thread_body,
|
|
107
|
+
priority=node.priority or 'normal',
|
|
108
|
+
timeout=node.timeout,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return thread_info_to_dict(info)
|
|
112
|
+
|
|
113
|
+
def _execute_join(self, name: str) -> Dict:
|
|
114
|
+
"""Wait for thread to complete."""
|
|
115
|
+
result = self._thread_service.join_thread(name)
|
|
116
|
+
info = self._thread_service.get_thread(name)
|
|
117
|
+
joined = thread_info_to_dict(info) if info else {'name': name}
|
|
118
|
+
joined['result'] = result
|
|
119
|
+
return joined
|
|
120
|
+
|
|
121
|
+
def _execute_terminate(self, name: str) -> Dict:
|
|
122
|
+
"""Terminate a running thread."""
|
|
123
|
+
return {
|
|
124
|
+
'name': name,
|
|
125
|
+
'action': 'terminate',
|
|
126
|
+
'success': self._thread_service.terminate_thread(name),
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Messaging Executors
|
|
3
|
+
|
|
4
|
+
Executors for messaging operations: websocket, message, queue, subscribe.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .websocket_executor import WebSocketExecutor, WebSocketSendExecutor, WebSocketCloseExecutor
|
|
8
|
+
from .message_executor import MessageExecutor, SubscribeExecutor
|
|
9
|
+
from .queue_executor import QueueExecutor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'WebSocketExecutor', 'WebSocketSendExecutor', 'WebSocketCloseExecutor',
|
|
13
|
+
'MessageExecutor', 'SubscribeExecutor', 'QueueExecutor'
|
|
14
|
+
]
|
|
15
|
+
from quantum.runtime.executors.messaging.message_ack_executor import (
|
|
16
|
+
MessageAckExecutor, MessageNackExecutor,
|
|
17
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Executors for q:messageAck and q:messageNack.
|
|
3
|
+
|
|
4
|
+
These nodes parsed and produced an AST node with NO executor registered, so
|
|
5
|
+
inside a `<q:subscribe ack="manual">` handler they did nothing. With manual
|
|
6
|
+
acknowledgement the broker waits for an ack that could never arrive, so the
|
|
7
|
+
message was redelivered forever — the audit's most-reported critical, and the
|
|
8
|
+
worst kind of silent failure: an infinite loop of side effects.
|
|
9
|
+
|
|
10
|
+
MessageQueueService already had ack() and nack(); nothing called them.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any, List, Type
|
|
14
|
+
|
|
15
|
+
from quantum.core.ast_nodes import MessageAckNode, MessageNackNode
|
|
16
|
+
from quantum.runtime.executors.base import BaseExecutor, ExecutorError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MessageAckExecutor(BaseExecutor):
|
|
20
|
+
"""Executor for q:messageAck — acknowledge the message being handled."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def handles(self) -> List[Type]:
|
|
24
|
+
return [MessageAckNode]
|
|
25
|
+
|
|
26
|
+
def execute(self, node: MessageAckNode, exec_context) -> Any:
|
|
27
|
+
try:
|
|
28
|
+
self.services.message_queue.ack()
|
|
29
|
+
return {'acknowledged': True}
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise ExecutorError(f"q:messageAck failed: {e}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MessageNackExecutor(BaseExecutor):
|
|
35
|
+
"""Executor for q:messageNack — reject the message being handled."""
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def handles(self) -> List[Type]:
|
|
39
|
+
return [MessageNackNode]
|
|
40
|
+
|
|
41
|
+
def execute(self, node: MessageNackNode, exec_context) -> Any:
|
|
42
|
+
# requeue defaults to True: rejecting without requeueing silently
|
|
43
|
+
# drops the message, which should be the explicit choice.
|
|
44
|
+
requeue = getattr(node, 'requeue', True)
|
|
45
|
+
if isinstance(requeue, str):
|
|
46
|
+
requeue = requeue.strip().lower() not in ('false', '0', 'no', 'off')
|
|
47
|
+
try:
|
|
48
|
+
self.services.message_queue.nack(requeue=bool(requeue))
|
|
49
|
+
return {'acknowledged': False, 'requeued': bool(requeue)}
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise ExecutorError(f"q:messageNack failed: {e}")
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message Executor - Execute q:message statements
|
|
3
|
+
|
|
4
|
+
Handles message queue publish/send/request operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Dict, Type
|
|
8
|
+
from quantum.runtime.executors.base import BaseExecutor, ExecutorError
|
|
9
|
+
from quantum.core.ast_nodes import MessageNode, SubscribeNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageExecutor(BaseExecutor):
|
|
13
|
+
"""
|
|
14
|
+
Executor for q:message statements.
|
|
15
|
+
|
|
16
|
+
Supports:
|
|
17
|
+
- Pub/Sub (publish to topic)
|
|
18
|
+
- Direct queue send
|
|
19
|
+
- Request/reply pattern
|
|
20
|
+
- Message headers
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def handles(self) -> List[Type]:
|
|
25
|
+
return [MessageNode]
|
|
26
|
+
|
|
27
|
+
def execute(self, node: MessageNode, exec_context) -> Any:
|
|
28
|
+
"""
|
|
29
|
+
Execute message publish/send.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
node: MessageNode with message configuration
|
|
33
|
+
exec_context: Execution context
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Message operation result dict
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
context = exec_context.get_all_variables()
|
|
40
|
+
|
|
41
|
+
# Resolve body
|
|
42
|
+
body = None
|
|
43
|
+
if node.body:
|
|
44
|
+
body = self.apply_databinding(node.body, context)
|
|
45
|
+
|
|
46
|
+
# Resolve headers
|
|
47
|
+
headers = {}
|
|
48
|
+
for header in node.headers:
|
|
49
|
+
header_name = header.name
|
|
50
|
+
header_value = self.apply_databinding(header.value, context)
|
|
51
|
+
headers[header_name] = header_value
|
|
52
|
+
|
|
53
|
+
# Resolve target
|
|
54
|
+
topic = None
|
|
55
|
+
queue = None
|
|
56
|
+
if node.topic:
|
|
57
|
+
topic = self.apply_databinding(node.topic, context)
|
|
58
|
+
if node.queue:
|
|
59
|
+
queue = self.apply_databinding(node.queue, context)
|
|
60
|
+
|
|
61
|
+
# Execute based on message type
|
|
62
|
+
if node.type == 'publish':
|
|
63
|
+
result = self.services.message_queue.publish(
|
|
64
|
+
topic=topic,
|
|
65
|
+
body=body,
|
|
66
|
+
headers=headers
|
|
67
|
+
)
|
|
68
|
+
elif node.type == 'send':
|
|
69
|
+
result = self.services.message_queue.send(
|
|
70
|
+
queue=queue,
|
|
71
|
+
body=body,
|
|
72
|
+
headers=headers
|
|
73
|
+
)
|
|
74
|
+
elif node.type == 'request':
|
|
75
|
+
timeout = int(node.timeout) if node.timeout else 30000
|
|
76
|
+
result = self.services.message_queue.request(
|
|
77
|
+
queue=queue,
|
|
78
|
+
body=body,
|
|
79
|
+
headers=headers,
|
|
80
|
+
timeout=timeout
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
raise ExecutorError(f"Unknown message type: {node.type}")
|
|
84
|
+
|
|
85
|
+
# Store result
|
|
86
|
+
if node.name:
|
|
87
|
+
exec_context.set_variable(node.name, result, scope="component")
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
if node.name:
|
|
93
|
+
exec_context.set_variable(node.name, {
|
|
94
|
+
'success': False,
|
|
95
|
+
'error': str(e)
|
|
96
|
+
}, scope="component")
|
|
97
|
+
raise ExecutorError(f"Message operation error: {e}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SubscribeExecutor(BaseExecutor):
|
|
101
|
+
"""
|
|
102
|
+
Executor for q:subscribe statements.
|
|
103
|
+
|
|
104
|
+
Subscribes to topics or queues and handles incoming messages.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def handles(self) -> List[Type]:
|
|
109
|
+
return [SubscribeNode]
|
|
110
|
+
|
|
111
|
+
def execute(self, node: SubscribeNode, exec_context) -> Any:
|
|
112
|
+
"""
|
|
113
|
+
Execute subscription setup.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
node: SubscribeNode with subscription configuration
|
|
117
|
+
exec_context: Execution context
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Subscription info dict
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
context = exec_context.get_all_variables()
|
|
124
|
+
|
|
125
|
+
# Resolve topics/queue
|
|
126
|
+
topics = []
|
|
127
|
+
if node.topic:
|
|
128
|
+
topics = [self.apply_databinding(node.topic, context)]
|
|
129
|
+
elif node.topics:
|
|
130
|
+
topics_str = self.apply_databinding(node.topics, context)
|
|
131
|
+
topics = [t.strip() for t in topics_str.split(',')]
|
|
132
|
+
|
|
133
|
+
queue = None
|
|
134
|
+
if node.queue:
|
|
135
|
+
queue = self.apply_databinding(node.queue, context)
|
|
136
|
+
|
|
137
|
+
# MessageQueueService.subscribe() takes a single `handler`
|
|
138
|
+
# callable, not a handlers config dict, and `topics` as a
|
|
139
|
+
# comma-separated string.
|
|
140
|
+
on_message_body = node.on_message
|
|
141
|
+
on_error_body = node.on_error
|
|
142
|
+
|
|
143
|
+
def handle_message(message):
|
|
144
|
+
payload = message if isinstance(message, dict) else {'message': message}
|
|
145
|
+
try:
|
|
146
|
+
return self.run_body_in_child_context(on_message_body, payload)
|
|
147
|
+
except Exception as exc: # noqa: BLE001 - routed to on_error body
|
|
148
|
+
if on_error_body:
|
|
149
|
+
return self.run_body_in_child_context(
|
|
150
|
+
on_error_body, {**payload, 'error': str(exc)}
|
|
151
|
+
)
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
# Create subscription
|
|
155
|
+
result = self.services.message_queue.subscribe(
|
|
156
|
+
name=node.name,
|
|
157
|
+
topics=','.join(topics) if topics else None,
|
|
158
|
+
queue=queue,
|
|
159
|
+
handler=handle_message,
|
|
160
|
+
ack=node.ack,
|
|
161
|
+
prefetch=node.prefetch,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Store subscription reference
|
|
165
|
+
exec_context.set_variable(node.name, result, scope="component")
|
|
166
|
+
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
exec_context.set_variable(node.name, {
|
|
171
|
+
'active': False,
|
|
172
|
+
'error': str(e)
|
|
173
|
+
}, scope="component")
|
|
174
|
+
raise ExecutorError(f"Subscription error: {e}")
|