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,2611 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum AST Nodes - Classes that represent elements of the Quantum language
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import List, Dict, Any, Optional, Union
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QuantumNode(ABC):
|
|
11
|
+
"""Base class for all Quantum AST nodes"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
15
|
+
"""Convert the node to dictionary (debug/serialization)"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def validate(self) -> List[str]:
|
|
20
|
+
"""Validate the node and return list of errors"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class QuantumParam:
|
|
26
|
+
"""Represents a <q:param>"""
|
|
27
|
+
name: str
|
|
28
|
+
type: str = "string"
|
|
29
|
+
required: bool = False
|
|
30
|
+
default: Optional[str] = None
|
|
31
|
+
validation: Optional[str] = None
|
|
32
|
+
description: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
# REST-specific (optional)
|
|
35
|
+
source: str = "auto" # auto, path, query, body, header, cookie
|
|
36
|
+
|
|
37
|
+
# Validation (same as SetNode)
|
|
38
|
+
validate_rule: Optional[str] = None
|
|
39
|
+
pattern: Optional[str] = None
|
|
40
|
+
min: Optional[Any] = None
|
|
41
|
+
max: Optional[Any] = None
|
|
42
|
+
minlength: Optional[int] = None
|
|
43
|
+
maxlength: Optional[int] = None
|
|
44
|
+
range: Optional[str] = None
|
|
45
|
+
enum: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
# File upload (when type="binary")
|
|
48
|
+
maxsize: Optional[str] = None
|
|
49
|
+
accept: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
def validate(self) -> List[str]:
|
|
52
|
+
errors = []
|
|
53
|
+
if not self.name:
|
|
54
|
+
errors.append("Param name is required")
|
|
55
|
+
return errors
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class QuantumReturn:
|
|
60
|
+
"""Represents a <q:return>"""
|
|
61
|
+
name: Optional[str] = None
|
|
62
|
+
type: str = "string"
|
|
63
|
+
value: Optional[str] = None
|
|
64
|
+
description: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
def validate(self) -> List[str]:
|
|
67
|
+
errors = []
|
|
68
|
+
if not self.value:
|
|
69
|
+
errors.append("Return value is required")
|
|
70
|
+
return errors
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class QuantumRoute:
|
|
75
|
+
"""Represents a <q:route>"""
|
|
76
|
+
path: str
|
|
77
|
+
method: str = "GET"
|
|
78
|
+
returns: List[QuantumReturn] = None
|
|
79
|
+
|
|
80
|
+
def __post_init__(self):
|
|
81
|
+
if self.returns is None:
|
|
82
|
+
self.returns = []
|
|
83
|
+
|
|
84
|
+
def validate(self) -> List[str]:
|
|
85
|
+
errors = []
|
|
86
|
+
if not self.path:
|
|
87
|
+
errors.append("Route path is required")
|
|
88
|
+
if self.method not in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
|
|
89
|
+
errors.append(f"Invalid HTTP method: {self.method}")
|
|
90
|
+
return errors
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# MIGRATED: IfNode moved to feature-based structure
|
|
94
|
+
# Import from new location (Option C migration)
|
|
95
|
+
from .features.conditionals.src.ast_node import IfNode
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ComponentNode(QuantumNode):
|
|
99
|
+
"""Represents a <q:component>"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, name: str, component_type: str = "pure"):
|
|
102
|
+
self.name = name
|
|
103
|
+
self.component_type = component_type # pure, microservice, event-driven, worker, websocket, graphql, grpc, serverless
|
|
104
|
+
|
|
105
|
+
# Component-level config
|
|
106
|
+
self.port = None
|
|
107
|
+
self.base_path = None
|
|
108
|
+
self.health_endpoint = None
|
|
109
|
+
self.metrics_provider = None
|
|
110
|
+
self.trace_provider = None
|
|
111
|
+
|
|
112
|
+
# Phase G: Authentication & Security
|
|
113
|
+
self.require_auth = False # Require authentication to access component
|
|
114
|
+
self.require_role = None # Required role(s) to access (comma-separated: "admin,editor")
|
|
115
|
+
self.require_permission = None # Required permission(s)
|
|
116
|
+
|
|
117
|
+
# HTML rendering & interactivity (Phase 1 & future Phase 3)
|
|
118
|
+
self.interactive = False # Enable client-side hydration (future)
|
|
119
|
+
self.has_html = False # Auto-detected if component contains HTML nodes
|
|
120
|
+
|
|
121
|
+
# Component elements
|
|
122
|
+
self.params: List[QuantumParam] = []
|
|
123
|
+
self.returns: List[QuantumReturn] = []
|
|
124
|
+
self.functions: List['FunctionNode'] = []
|
|
125
|
+
self.event_handlers: List['OnEventNode'] = []
|
|
126
|
+
self.script_blocks: List[str] = []
|
|
127
|
+
self.statements: List[QuantumNode] = [] # For control flow statements (includes HTML nodes)
|
|
128
|
+
|
|
129
|
+
# Resources (refs to Admin-configured resources)
|
|
130
|
+
self.resources: Dict[str, str] = {} # type -> ref_name (e.g., "database" -> "postgres-main")
|
|
131
|
+
|
|
132
|
+
def add_param(self, param: QuantumParam):
|
|
133
|
+
self.params.append(param)
|
|
134
|
+
|
|
135
|
+
def add_return(self, return_node: QuantumReturn):
|
|
136
|
+
self.returns.append(return_node)
|
|
137
|
+
|
|
138
|
+
def add_function(self, function: 'FunctionNode'):
|
|
139
|
+
function.container = self
|
|
140
|
+
self.functions.append(function)
|
|
141
|
+
|
|
142
|
+
def add_event_handler(self, handler: 'OnEventNode'):
|
|
143
|
+
self.event_handlers.append(handler)
|
|
144
|
+
|
|
145
|
+
def add_script(self, script: str):
|
|
146
|
+
self.script_blocks.append(script)
|
|
147
|
+
|
|
148
|
+
def add_statement(self, statement: QuantumNode):
|
|
149
|
+
"""Add control flow statement (if, loop, set, etc)"""
|
|
150
|
+
self.statements.append(statement)
|
|
151
|
+
|
|
152
|
+
def add_resource(self, resource_type: str, ref_name: str):
|
|
153
|
+
"""Add resource reference (from Admin panel)"""
|
|
154
|
+
self.resources[resource_type] = ref_name
|
|
155
|
+
|
|
156
|
+
def get_rest_endpoints(self) -> List['FunctionNode']:
|
|
157
|
+
"""Get all functions that are REST endpoints"""
|
|
158
|
+
return [f for f in self.functions if f.is_rest_enabled()]
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
161
|
+
return {
|
|
162
|
+
"type": "component",
|
|
163
|
+
"name": self.name,
|
|
164
|
+
"component_type": self.component_type,
|
|
165
|
+
"port": self.port,
|
|
166
|
+
"base_path": self.base_path,
|
|
167
|
+
"interactive": self.interactive,
|
|
168
|
+
"has_html": self.has_html,
|
|
169
|
+
"params": [p.__dict__ for p in self.params],
|
|
170
|
+
"returns": [r.__dict__ for r in self.returns],
|
|
171
|
+
"functions": [f.to_dict() for f in self.functions],
|
|
172
|
+
"event_handlers": [eh.to_dict() for eh in self.event_handlers],
|
|
173
|
+
"script_blocks": len(self.script_blocks),
|
|
174
|
+
"statements": len(self.statements),
|
|
175
|
+
"resources": self.resources
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def validate(self) -> List[str]:
|
|
179
|
+
errors = []
|
|
180
|
+
if not self.name:
|
|
181
|
+
errors.append("Component name is required")
|
|
182
|
+
|
|
183
|
+
# Validate component type
|
|
184
|
+
valid_types = ['pure', 'microservice', 'event-driven', 'worker', 'websocket', 'graphql', 'grpc', 'serverless']
|
|
185
|
+
if self.component_type not in valid_types:
|
|
186
|
+
errors.append(f"Invalid component type: {self.component_type}. Must be one of {valid_types}")
|
|
187
|
+
|
|
188
|
+
for param in self.params:
|
|
189
|
+
errors.extend(param.validate())
|
|
190
|
+
|
|
191
|
+
for ret in self.returns:
|
|
192
|
+
errors.extend(ret.validate())
|
|
193
|
+
|
|
194
|
+
for func in self.functions:
|
|
195
|
+
errors.extend(func.validate())
|
|
196
|
+
|
|
197
|
+
for handler in self.event_handlers:
|
|
198
|
+
errors.extend(handler.validate())
|
|
199
|
+
|
|
200
|
+
for statement in self.statements:
|
|
201
|
+
errors.extend(statement.validate())
|
|
202
|
+
|
|
203
|
+
return errors
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ApplicationNode(QuantumNode):
|
|
207
|
+
"""Represents a <q:application>"""
|
|
208
|
+
|
|
209
|
+
def __init__(self, app_id: str, app_type: str):
|
|
210
|
+
self.app_id = app_id
|
|
211
|
+
self.app_type = app_type
|
|
212
|
+
self.engine: Optional[str] = None # "2d" for game engine
|
|
213
|
+
self.routes: List[QuantumRoute] = []
|
|
214
|
+
self.components: List[ComponentNode] = []
|
|
215
|
+
|
|
216
|
+
# q:set e q:function escritos direto no <q:application>.
|
|
217
|
+
#
|
|
218
|
+
# Nao havia onde guarda-los, e os parsers por engine so olhavam os
|
|
219
|
+
# filhos do PROPRIO namespace (qt:, qg:, ui:), entao um
|
|
220
|
+
#
|
|
221
|
+
# <q:application type="terminal">
|
|
222
|
+
# <q:function name="salvar"> ... </q:function>
|
|
223
|
+
#
|
|
224
|
+
# era descartado sem uma palavra. O gerador do terminal so
|
|
225
|
+
# encontrava funcoes declaradas DENTRO de um <qt:screen>, e o
|
|
226
|
+
# on-click que apontava para a funcao global caia no ramo "acao
|
|
227
|
+
# desconhecida", virando um `self.notify("Action: salvar")` — o
|
|
228
|
+
# botao anunciava a acao em vez de executa-la.
|
|
229
|
+
self.functions: List[QuantumNode] = [] # FunctionNode list
|
|
230
|
+
self.state_vars: List[QuantumNode] = [] # SetNode list
|
|
231
|
+
|
|
232
|
+
# Game Engine 2D: game-specific children
|
|
233
|
+
self.scenes: List[QuantumNode] = [] # SceneNode list
|
|
234
|
+
self.behaviors: List[QuantumNode] = [] # BehaviorNode list
|
|
235
|
+
self.prefabs: List[QuantumNode] = [] # PrefabNode list
|
|
236
|
+
self.enemies: List[QuantumNode] = [] # EnemyNode list
|
|
237
|
+
self.persistent: List[QuantumNode] = [] # PersistentNode children (SetNodes)
|
|
238
|
+
|
|
239
|
+
# Terminal Engine: terminal-specific children
|
|
240
|
+
self.screens: List[QuantumNode] = [] # ScreenNode list
|
|
241
|
+
self.keybindings: List[QuantumNode] = [] # KeybindingNode list
|
|
242
|
+
self.services: List[QuantumNode] = [] # ServiceNode list
|
|
243
|
+
self.terminal_css: str = "" # Accumulated TCSS
|
|
244
|
+
|
|
245
|
+
# Testing Engine: testing-specific children
|
|
246
|
+
self.test_suites: List[QuantumNode] = [] # TestSuiteNode list
|
|
247
|
+
self.test_config: Optional[QuantumNode] = None # BrowserConfigNode
|
|
248
|
+
self.test_fixtures: List[QuantumNode] = [] # FixtureNode_Testing list
|
|
249
|
+
self.test_mocks: List[QuantumNode] = [] # MockNode_Testing list
|
|
250
|
+
self.test_auth_states: List[QuantumNode] = [] # AuthNode list
|
|
251
|
+
|
|
252
|
+
# UI Engine: multi-target UI children
|
|
253
|
+
self.ui_windows: List[QuantumNode] = [] # UIWindowNode list
|
|
254
|
+
self.ui_children: List[QuantumNode] = [] # Top-level UI nodes (no window)
|
|
255
|
+
self.ui_target: str = 'html' # html, textual, desktop
|
|
256
|
+
self.ui_theme: Optional[QuantumNode] = None # UIThemeNode (from ui:theme or theme attr)
|
|
257
|
+
self.ui_theme_preset: Optional[str] = None # Theme preset from theme attribute
|
|
258
|
+
|
|
259
|
+
def add_route(self, route: QuantumRoute):
|
|
260
|
+
self.routes.append(route)
|
|
261
|
+
|
|
262
|
+
def add_component(self, component: ComponentNode):
|
|
263
|
+
self.components.append(component)
|
|
264
|
+
|
|
265
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
266
|
+
result = {
|
|
267
|
+
"type": "application",
|
|
268
|
+
"app_id": self.app_id,
|
|
269
|
+
"app_type": self.app_type,
|
|
270
|
+
"engine": self.engine,
|
|
271
|
+
"routes": [r.__dict__ for r in self.routes],
|
|
272
|
+
"components": [c.to_dict() for c in self.components],
|
|
273
|
+
}
|
|
274
|
+
if self.scenes:
|
|
275
|
+
result["scenes"] = [s.to_dict() for s in self.scenes]
|
|
276
|
+
if self.behaviors:
|
|
277
|
+
result["behaviors"] = [b.to_dict() for b in self.behaviors]
|
|
278
|
+
if self.prefabs:
|
|
279
|
+
result["prefabs"] = [p.to_dict() for p in self.prefabs]
|
|
280
|
+
if self.screens:
|
|
281
|
+
result["screens"] = [s.to_dict() for s in self.screens]
|
|
282
|
+
if self.keybindings:
|
|
283
|
+
result["keybindings"] = [k.to_dict() for k in self.keybindings]
|
|
284
|
+
if self.services:
|
|
285
|
+
result["services"] = [s.to_dict() for s in self.services]
|
|
286
|
+
if self.terminal_css:
|
|
287
|
+
result["terminal_css"] = self.terminal_css
|
|
288
|
+
if self.test_suites:
|
|
289
|
+
result["test_suites"] = [s.to_dict() for s in self.test_suites]
|
|
290
|
+
if self.test_config:
|
|
291
|
+
result["test_config"] = self.test_config.to_dict()
|
|
292
|
+
if self.test_fixtures:
|
|
293
|
+
result["test_fixtures"] = [f.to_dict() for f in self.test_fixtures]
|
|
294
|
+
if self.test_mocks:
|
|
295
|
+
result["test_mocks"] = [m.to_dict() for m in self.test_mocks]
|
|
296
|
+
if self.test_auth_states:
|
|
297
|
+
result["test_auth_states"] = [a.to_dict() for a in self.test_auth_states]
|
|
298
|
+
if self.ui_windows:
|
|
299
|
+
result["ui_windows"] = [w.to_dict() for w in self.ui_windows]
|
|
300
|
+
if self.ui_children:
|
|
301
|
+
result["ui_children"] = [c.to_dict() for c in self.ui_children]
|
|
302
|
+
if self.ui_target != 'html':
|
|
303
|
+
result["ui_target"] = self.ui_target
|
|
304
|
+
if self.ui_theme:
|
|
305
|
+
result["ui_theme"] = self.ui_theme.to_dict()
|
|
306
|
+
if self.ui_theme_preset:
|
|
307
|
+
result["ui_theme_preset"] = self.ui_theme_preset
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
def validate(self) -> List[str]:
|
|
311
|
+
errors = []
|
|
312
|
+
if not self.app_id:
|
|
313
|
+
errors.append("Application ID is required")
|
|
314
|
+
if not self.app_type:
|
|
315
|
+
errors.append("Application type is required")
|
|
316
|
+
|
|
317
|
+
for route in self.routes:
|
|
318
|
+
errors.extend(route.validate())
|
|
319
|
+
|
|
320
|
+
for component in self.components:
|
|
321
|
+
errors.extend(component.validate())
|
|
322
|
+
|
|
323
|
+
# Validate game children
|
|
324
|
+
for scene in self.scenes:
|
|
325
|
+
if hasattr(scene, 'validate'):
|
|
326
|
+
errors.extend(scene.validate())
|
|
327
|
+
for behavior in self.behaviors:
|
|
328
|
+
if hasattr(behavior, 'validate'):
|
|
329
|
+
errors.extend(behavior.validate())
|
|
330
|
+
for prefab in self.prefabs:
|
|
331
|
+
if hasattr(prefab, 'validate'):
|
|
332
|
+
errors.extend(prefab.validate())
|
|
333
|
+
|
|
334
|
+
# Validate terminal children
|
|
335
|
+
for screen in self.screens:
|
|
336
|
+
if hasattr(screen, 'validate'):
|
|
337
|
+
errors.extend(screen.validate())
|
|
338
|
+
for keybinding in self.keybindings:
|
|
339
|
+
if hasattr(keybinding, 'validate'):
|
|
340
|
+
errors.extend(keybinding.validate())
|
|
341
|
+
for service in self.services:
|
|
342
|
+
if hasattr(service, 'validate'):
|
|
343
|
+
errors.extend(service.validate())
|
|
344
|
+
|
|
345
|
+
# Validate testing children
|
|
346
|
+
for suite in self.test_suites:
|
|
347
|
+
if hasattr(suite, 'validate'):
|
|
348
|
+
errors.extend(suite.validate())
|
|
349
|
+
if self.test_config and hasattr(self.test_config, 'validate'):
|
|
350
|
+
errors.extend(self.test_config.validate())
|
|
351
|
+
for fixture in self.test_fixtures:
|
|
352
|
+
if hasattr(fixture, 'validate'):
|
|
353
|
+
errors.extend(fixture.validate())
|
|
354
|
+
for mock in self.test_mocks:
|
|
355
|
+
if hasattr(mock, 'validate'):
|
|
356
|
+
errors.extend(mock.validate())
|
|
357
|
+
for auth in self.test_auth_states:
|
|
358
|
+
if hasattr(auth, 'validate'):
|
|
359
|
+
errors.extend(auth.validate())
|
|
360
|
+
|
|
361
|
+
# Validate UI children
|
|
362
|
+
for window in self.ui_windows:
|
|
363
|
+
if hasattr(window, 'validate'):
|
|
364
|
+
errors.extend(window.validate())
|
|
365
|
+
for ui_child in self.ui_children:
|
|
366
|
+
if hasattr(ui_child, 'validate'):
|
|
367
|
+
errors.extend(ui_child.validate())
|
|
368
|
+
|
|
369
|
+
return errors
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class JobNode(QuantumNode):
|
|
373
|
+
"""
|
|
374
|
+
Represents a <q:job> - Job queue for batch processing.
|
|
375
|
+
|
|
376
|
+
Examples:
|
|
377
|
+
<q:job name="sendEmail" queue="emails" priority="5">
|
|
378
|
+
<q:param name="to" type="string" />
|
|
379
|
+
<q:param name="subject" type="string" />
|
|
380
|
+
<q:mail to="{to}" subject="{subject}">...</q:mail>
|
|
381
|
+
</q:job>
|
|
382
|
+
|
|
383
|
+
<q:job name="processOrder" action="dispatch" delay="5m" />
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
def __init__(self, name: str, queue: str = 'default', action: str = 'define'):
|
|
387
|
+
self.name = name
|
|
388
|
+
self.queue = queue
|
|
389
|
+
self.action = action # define, dispatch, batch
|
|
390
|
+
self.delay: Optional[str] = None # e.g., '30s', '5m', '1h'
|
|
391
|
+
self.priority: int = 0
|
|
392
|
+
self.attempts: int = 3
|
|
393
|
+
self.backoff: str = '30s'
|
|
394
|
+
self.timeout: Optional[str] = None
|
|
395
|
+
self.params: List['QuantumParam'] = []
|
|
396
|
+
self.body: List['QuantumNode'] = []
|
|
397
|
+
|
|
398
|
+
# Legacy compatibility
|
|
399
|
+
self.job_id: Optional[str] = name
|
|
400
|
+
self.schedule: Optional[str] = None
|
|
401
|
+
self.tasks: List[Dict[str, Any]] = []
|
|
402
|
+
|
|
403
|
+
def add_param(self, param: 'QuantumParam'):
|
|
404
|
+
"""Add parameter to job"""
|
|
405
|
+
self.params.append(param)
|
|
406
|
+
|
|
407
|
+
def add_statement(self, statement: 'QuantumNode'):
|
|
408
|
+
"""Add statement to job body"""
|
|
409
|
+
self.body.append(statement)
|
|
410
|
+
|
|
411
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
412
|
+
return {
|
|
413
|
+
"type": "job",
|
|
414
|
+
"name": self.name,
|
|
415
|
+
"queue": self.queue,
|
|
416
|
+
"action": self.action,
|
|
417
|
+
"delay": self.delay,
|
|
418
|
+
"priority": self.priority,
|
|
419
|
+
"attempts": self.attempts,
|
|
420
|
+
"backoff": self.backoff,
|
|
421
|
+
"timeout": self.timeout,
|
|
422
|
+
"params": [p.__dict__ for p in self.params],
|
|
423
|
+
"body_statements": len(self.body),
|
|
424
|
+
# Legacy
|
|
425
|
+
"job_id": self.job_id,
|
|
426
|
+
"schedule": self.schedule,
|
|
427
|
+
"tasks": self.tasks
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
def validate(self) -> List[str]:
|
|
431
|
+
errors = []
|
|
432
|
+
if not self.name:
|
|
433
|
+
errors.append("Job name is required")
|
|
434
|
+
if self.action not in ['define', 'dispatch', 'batch']:
|
|
435
|
+
errors.append(f"Invalid job action: {self.action}. Must be define, dispatch, or batch")
|
|
436
|
+
if self.priority < 0 or self.priority > 10:
|
|
437
|
+
errors.append(f"Job priority must be between 0 and 10")
|
|
438
|
+
for param in self.params:
|
|
439
|
+
errors.extend(param.validate())
|
|
440
|
+
for statement in self.body:
|
|
441
|
+
if hasattr(statement, 'validate'):
|
|
442
|
+
errors.extend(statement.validate())
|
|
443
|
+
return errors
|
|
444
|
+
|
|
445
|
+
def __repr__(self):
|
|
446
|
+
return f'<JobNode name={self.name} queue={self.queue} action={self.action}>'
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@dataclass
|
|
450
|
+
class ScheduleNode(QuantumNode):
|
|
451
|
+
"""
|
|
452
|
+
Represents a <q:schedule> - Scheduled task execution (like cfschedule).
|
|
453
|
+
|
|
454
|
+
Examples:
|
|
455
|
+
<q:schedule name="dailyReport" interval="1d" timezone="America/Sao_Paulo">
|
|
456
|
+
<q:query datasource="db" name="stats">SELECT * FROM daily_stats</q:query>
|
|
457
|
+
<q:mail to="admin@example.com" subject="Daily Report">...</q:mail>
|
|
458
|
+
</q:schedule>
|
|
459
|
+
|
|
460
|
+
<q:schedule name="cleanup" cron="0 2 * * *" retry="3">
|
|
461
|
+
<q:query datasource="db">DELETE FROM temp_data WHERE created_at < NOW() - INTERVAL 7 DAY</q:query>
|
|
462
|
+
</q:schedule>
|
|
463
|
+
|
|
464
|
+
<q:schedule name="oneTimeTask" at="2024-12-25T00:00:00" enabled="true" />
|
|
465
|
+
"""
|
|
466
|
+
name: str
|
|
467
|
+
action: str = 'run' # run, pause, resume, delete
|
|
468
|
+
interval: Optional[str] = None # 30s, 5m, 1h, 1d
|
|
469
|
+
cron: Optional[str] = None # cron expression
|
|
470
|
+
at: Optional[str] = None # specific datetime (ISO 8601)
|
|
471
|
+
timezone: str = 'UTC'
|
|
472
|
+
retry: int = 3
|
|
473
|
+
timeout: Optional[str] = None
|
|
474
|
+
overlap: bool = False # Allow overlapping executions
|
|
475
|
+
enabled: bool = True
|
|
476
|
+
body: List['QuantumNode'] = field(default_factory=list)
|
|
477
|
+
|
|
478
|
+
def add_statement(self, statement: 'QuantumNode'):
|
|
479
|
+
"""Add statement to schedule body"""
|
|
480
|
+
self.body.append(statement)
|
|
481
|
+
|
|
482
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
483
|
+
return {
|
|
484
|
+
"type": "schedule",
|
|
485
|
+
"name": self.name,
|
|
486
|
+
"action": self.action,
|
|
487
|
+
"interval": self.interval,
|
|
488
|
+
"cron": self.cron,
|
|
489
|
+
"at": self.at,
|
|
490
|
+
"timezone": self.timezone,
|
|
491
|
+
"retry": self.retry,
|
|
492
|
+
"timeout": self.timeout,
|
|
493
|
+
"overlap": self.overlap,
|
|
494
|
+
"enabled": self.enabled,
|
|
495
|
+
"body_statements": len(self.body)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
def validate(self) -> List[str]:
|
|
499
|
+
errors = []
|
|
500
|
+
if not self.name:
|
|
501
|
+
errors.append("Schedule name is required")
|
|
502
|
+
if self.action not in ['run', 'pause', 'resume', 'delete']:
|
|
503
|
+
errors.append(f"Invalid schedule action: {self.action}. Must be run, pause, resume, or delete")
|
|
504
|
+
# Must have at least one trigger type for 'run' action
|
|
505
|
+
if self.action == 'run' and not (self.interval or self.cron or self.at):
|
|
506
|
+
errors.append("Schedule requires 'interval', 'cron', or 'at' attribute for run action")
|
|
507
|
+
if self.retry < 0:
|
|
508
|
+
errors.append("Schedule retry must be non-negative")
|
|
509
|
+
for statement in self.body:
|
|
510
|
+
if hasattr(statement, 'validate'):
|
|
511
|
+
errors.extend(statement.validate())
|
|
512
|
+
return errors
|
|
513
|
+
|
|
514
|
+
def __repr__(self):
|
|
515
|
+
trigger = self.interval or self.cron or self.at or 'none'
|
|
516
|
+
return f'<ScheduleNode name={self.name} trigger={trigger}>'
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@dataclass
|
|
520
|
+
class ThreadNode(QuantumNode):
|
|
521
|
+
"""
|
|
522
|
+
Represents a <q:thread> - Async thread execution (like cfthread).
|
|
523
|
+
|
|
524
|
+
Examples:
|
|
525
|
+
<q:thread name="sendEmails" priority="high">
|
|
526
|
+
<q:loop query="pendingEmails">
|
|
527
|
+
<q:mail to="{email}" subject="Notification">...</q:mail>
|
|
528
|
+
</q:loop>
|
|
529
|
+
</q:thread>
|
|
530
|
+
|
|
531
|
+
<q:thread name="processImages" timeout="5m" onComplete="handleComplete">
|
|
532
|
+
<q:invoke url="/api/process" method="POST" />
|
|
533
|
+
</q:thread>
|
|
534
|
+
|
|
535
|
+
<q:thread name="worker1" action="join" /> <!-- Wait for thread -->
|
|
536
|
+
<q:thread name="worker1" action="terminate" /> <!-- Kill thread -->
|
|
537
|
+
"""
|
|
538
|
+
name: str
|
|
539
|
+
action: str = 'run' # run, join, terminate
|
|
540
|
+
priority: str = 'normal' # low, normal, high
|
|
541
|
+
timeout: Optional[str] = None # e.g., '30s', '5m'
|
|
542
|
+
on_complete: Optional[str] = None # Callback function name
|
|
543
|
+
on_error: Optional[str] = None # Error handler function name
|
|
544
|
+
body: List['QuantumNode'] = field(default_factory=list)
|
|
545
|
+
|
|
546
|
+
def add_statement(self, statement: 'QuantumNode'):
|
|
547
|
+
"""Add statement to thread body"""
|
|
548
|
+
self.body.append(statement)
|
|
549
|
+
|
|
550
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
551
|
+
return {
|
|
552
|
+
"type": "thread",
|
|
553
|
+
"name": self.name,
|
|
554
|
+
"action": self.action,
|
|
555
|
+
"priority": self.priority,
|
|
556
|
+
"timeout": self.timeout,
|
|
557
|
+
"on_complete": self.on_complete,
|
|
558
|
+
"on_error": self.on_error,
|
|
559
|
+
"body_statements": len(self.body)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
def validate(self) -> List[str]:
|
|
563
|
+
errors = []
|
|
564
|
+
if not self.name:
|
|
565
|
+
errors.append("Thread name is required")
|
|
566
|
+
if self.action not in ['run', 'join', 'terminate']:
|
|
567
|
+
errors.append(f"Invalid thread action: {self.action}. Must be run, join, or terminate")
|
|
568
|
+
if self.priority not in ['low', 'normal', 'high']:
|
|
569
|
+
errors.append(f"Invalid thread priority: {self.priority}. Must be low, normal, or high")
|
|
570
|
+
# Body required only for 'run' action
|
|
571
|
+
if self.action == 'run' and not self.body:
|
|
572
|
+
errors.append("Thread with action='run' requires body statements")
|
|
573
|
+
for statement in self.body:
|
|
574
|
+
if hasattr(statement, 'validate'):
|
|
575
|
+
errors.extend(statement.validate())
|
|
576
|
+
return errors
|
|
577
|
+
|
|
578
|
+
def __repr__(self):
|
|
579
|
+
return f'<ThreadNode name={self.name} action={self.action} priority={self.priority}>'
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# MIGRATED: LoopNode moved to feature-based structure
|
|
583
|
+
# Import from new location (Option C migration)
|
|
584
|
+
from .features.loops.src.ast_node import LoopNode
|
|
585
|
+
|
|
586
|
+
# MIGRATED: FunctionNode and RestConfig moved to feature-based structure
|
|
587
|
+
# Import from new location (Option C migration)
|
|
588
|
+
from .features.functions.src.ast_node import FunctionNode, RestConfig
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class DispatchEventNode(QuantumNode):
|
|
594
|
+
"""Represents a <q:dispatchEvent> - Publish event"""
|
|
595
|
+
|
|
596
|
+
def __init__(self, event: str):
|
|
597
|
+
self.event = event
|
|
598
|
+
self.data = None
|
|
599
|
+
self.queue = None
|
|
600
|
+
self.exchange = None
|
|
601
|
+
self.routing_key = None
|
|
602
|
+
self.priority = "normal" # low, normal, high
|
|
603
|
+
self.delay = None # e.g., "5s"
|
|
604
|
+
self.ttl = None # e.g., "60s"
|
|
605
|
+
self.metadata = None
|
|
606
|
+
|
|
607
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
608
|
+
return {
|
|
609
|
+
"type": "dispatchEvent",
|
|
610
|
+
"event": self.event,
|
|
611
|
+
"data": self.data,
|
|
612
|
+
"queue": self.queue,
|
|
613
|
+
"exchange": self.exchange,
|
|
614
|
+
"routing_key": self.routing_key,
|
|
615
|
+
"priority": self.priority,
|
|
616
|
+
"delay": self.delay,
|
|
617
|
+
"ttl": self.ttl,
|
|
618
|
+
"metadata": self.metadata
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
def validate(self) -> List[str]:
|
|
622
|
+
errors = []
|
|
623
|
+
if not self.event:
|
|
624
|
+
errors.append("Event name is required for dispatchEvent")
|
|
625
|
+
if self.priority not in ['low', 'normal', 'high']:
|
|
626
|
+
errors.append(f"Invalid priority: {self.priority}. Must be low, normal, or high")
|
|
627
|
+
return errors
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class OnEventNode(QuantumNode):
|
|
631
|
+
"""Represents a <q:onEvent> - Subscribe to event"""
|
|
632
|
+
|
|
633
|
+
def __init__(self, event: str):
|
|
634
|
+
self.event = event # Event pattern (e.g., "user.*" or "user.created")
|
|
635
|
+
self.queue = None
|
|
636
|
+
self.max_retries = 0
|
|
637
|
+
self.retry_delay = None # e.g., "30s"
|
|
638
|
+
self.dead_letter = None # Dead letter queue
|
|
639
|
+
self.filter = None # Filter expression
|
|
640
|
+
self.concurrent = 1 # Parallel processing
|
|
641
|
+
self.prefetch = 1 # Prefetch count
|
|
642
|
+
self.timeout = None # Handler timeout
|
|
643
|
+
self.body: List[QuantumNode] = []
|
|
644
|
+
|
|
645
|
+
def add_statement(self, statement: QuantumNode):
|
|
646
|
+
"""Add statement to event handler body"""
|
|
647
|
+
self.body.append(statement)
|
|
648
|
+
|
|
649
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
650
|
+
return {
|
|
651
|
+
"type": "onEvent",
|
|
652
|
+
"event": self.event,
|
|
653
|
+
"queue": self.queue,
|
|
654
|
+
"max_retries": self.max_retries,
|
|
655
|
+
"retry_delay": self.retry_delay,
|
|
656
|
+
"dead_letter": self.dead_letter,
|
|
657
|
+
"filter": self.filter,
|
|
658
|
+
"concurrent": self.concurrent,
|
|
659
|
+
"prefetch": self.prefetch,
|
|
660
|
+
"timeout": self.timeout,
|
|
661
|
+
"body_statements": len(self.body)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
def validate(self) -> List[str]:
|
|
665
|
+
errors = []
|
|
666
|
+
if not self.event:
|
|
667
|
+
errors.append("Event pattern is required for onEvent")
|
|
668
|
+
|
|
669
|
+
# Validate body
|
|
670
|
+
for statement in self.body:
|
|
671
|
+
errors.extend(statement.validate())
|
|
672
|
+
|
|
673
|
+
return errors
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
# MIGRATED: SetNode and PersistNode moved to feature-based structure
|
|
677
|
+
# Import from new location (Option C migration)
|
|
678
|
+
from .features.state_management.src.ast_node import SetNode, PersistNode
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class QueryNode(QuantumNode):
|
|
682
|
+
"""Represents a <q:query> - Database query component"""
|
|
683
|
+
|
|
684
|
+
def __init__(self, name: str, datasource: str, sql: str, params: List['QueryParamNode'] = None):
|
|
685
|
+
self.name = name
|
|
686
|
+
self.datasource = datasource
|
|
687
|
+
self.sql = sql
|
|
688
|
+
self.params = params or []
|
|
689
|
+
|
|
690
|
+
# Optional attributes
|
|
691
|
+
self.source = None # For Query-of-Queries
|
|
692
|
+
self.mode = None # None for normal, "rag" for RAG pipeline
|
|
693
|
+
self.rag_model = None # LLM model override for RAG queries
|
|
694
|
+
self.cache = False
|
|
695
|
+
self.ttl = None
|
|
696
|
+
self.reactive = False
|
|
697
|
+
self.interval = None
|
|
698
|
+
self.paginate = False # Enable automatic pagination
|
|
699
|
+
self.page = None
|
|
700
|
+
self.page_size = 20
|
|
701
|
+
self.timeout = None
|
|
702
|
+
self.maxrows = None
|
|
703
|
+
self.result = None # Variable name for metadata
|
|
704
|
+
|
|
705
|
+
def add_param(self, param: 'QueryParamNode'):
|
|
706
|
+
"""Add parameter to query"""
|
|
707
|
+
self.params.append(param)
|
|
708
|
+
|
|
709
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
710
|
+
return {
|
|
711
|
+
"type": "query",
|
|
712
|
+
"name": self.name,
|
|
713
|
+
"datasource": self.datasource,
|
|
714
|
+
"sql": self.sql[:50] + "..." if len(self.sql) > 50 else self.sql,
|
|
715
|
+
"params": [p.to_dict() for p in self.params],
|
|
716
|
+
"cache": self.cache,
|
|
717
|
+
"reactive": self.reactive
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
def validate(self) -> List[str]:
|
|
721
|
+
errors = []
|
|
722
|
+
# name is optional: a write query (UPDATE/INSERT/DELETE) has no result
|
|
723
|
+
# set to bind. Reads without a name simply cannot be referenced.
|
|
724
|
+
if not self.datasource and not self.source:
|
|
725
|
+
errors.append("Query requires either 'datasource' or 'source' attribute")
|
|
726
|
+
if not self.sql:
|
|
727
|
+
errors.append("Query SQL is required")
|
|
728
|
+
|
|
729
|
+
# Validate params
|
|
730
|
+
for param in self.params:
|
|
731
|
+
errors.extend(param.validate())
|
|
732
|
+
|
|
733
|
+
return errors
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class QueryParamNode(QuantumNode):
|
|
737
|
+
"""Represents a <q:param> within q:query"""
|
|
738
|
+
|
|
739
|
+
def __init__(self, name: str, value: Any, param_type: str):
|
|
740
|
+
self.name = name
|
|
741
|
+
self.value = value
|
|
742
|
+
self.param_type = param_type # string, integer, decimal, boolean, datetime, date, time, array, json
|
|
743
|
+
|
|
744
|
+
# Optional attributes
|
|
745
|
+
self.null = False
|
|
746
|
+
self.max_length = None
|
|
747
|
+
self.scale = None
|
|
748
|
+
|
|
749
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
750
|
+
return {
|
|
751
|
+
"type": "queryParam",
|
|
752
|
+
"name": self.name,
|
|
753
|
+
"value": str(self.value)[:20] + "..." if self.value and len(str(self.value)) > 20 else self.value,
|
|
754
|
+
"param_type": self.param_type,
|
|
755
|
+
"null": self.null
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
def validate(self) -> List[str]:
|
|
759
|
+
errors = []
|
|
760
|
+
if not self.name:
|
|
761
|
+
errors.append("Query parameter name is required")
|
|
762
|
+
if self.value is None and not self.null:
|
|
763
|
+
errors.append(f"Query parameter '{self.name}' cannot be null")
|
|
764
|
+
if self.param_type not in ['string', 'integer', 'decimal', 'boolean', 'datetime', 'date', 'time', 'array', 'json']:
|
|
765
|
+
errors.append(f"Invalid parameter type: {self.param_type}")
|
|
766
|
+
|
|
767
|
+
return errors
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
# MIGRATED: InvokeNode and InvokeHeaderNode moved to feature-based structure
|
|
771
|
+
# Import from new location (Option C migration)
|
|
772
|
+
from .features.invocation.src.ast_node import InvokeNode, InvokeHeaderNode
|
|
773
|
+
|
|
774
|
+
# MIGRATED: DataNode and related nodes moved to feature-based structure
|
|
775
|
+
# Import from new location (Option C migration)
|
|
776
|
+
from .features.data_import.src.ast_node import (
|
|
777
|
+
DataNode, ColumnNode, FieldNode, TransformNode,
|
|
778
|
+
FilterNode, SortNode, LimitNode, ComputeNode, HeaderNode
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# ============================================
|
|
783
|
+
# HTML RENDERING NODES (Phase 1)
|
|
784
|
+
# ============================================
|
|
785
|
+
|
|
786
|
+
# HTML void elements that are self-closing
|
|
787
|
+
HTML_VOID_ELEMENTS = {
|
|
788
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
789
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class HTMLNode(QuantumNode):
|
|
794
|
+
"""
|
|
795
|
+
Represents an HTML element that should be rendered to output.
|
|
796
|
+
|
|
797
|
+
Phase 1: Server-side rendering only
|
|
798
|
+
Phase 3: Can be marked for client-side hydration
|
|
799
|
+
|
|
800
|
+
Examples:
|
|
801
|
+
<div class="container">...</div>
|
|
802
|
+
<img src="/logo.png" />
|
|
803
|
+
<a href="/about">About</a>
|
|
804
|
+
<button q:click="handleClick">Click me</button> (future)
|
|
805
|
+
"""
|
|
806
|
+
|
|
807
|
+
def __init__(
|
|
808
|
+
self,
|
|
809
|
+
tag: str,
|
|
810
|
+
attributes: Optional[Dict[str, str]] = None,
|
|
811
|
+
children: Optional[List[QuantumNode]] = None,
|
|
812
|
+
self_closing: bool = False
|
|
813
|
+
):
|
|
814
|
+
self.tag = tag
|
|
815
|
+
self.attributes = attributes or {}
|
|
816
|
+
self.children = children or []
|
|
817
|
+
self.self_closing = self_closing or tag in HTML_VOID_ELEMENTS
|
|
818
|
+
|
|
819
|
+
# Future: client-side event handlers (Phase 3)
|
|
820
|
+
self.has_events = self._detect_event_handlers()
|
|
821
|
+
|
|
822
|
+
def _detect_event_handlers(self) -> bool:
|
|
823
|
+
"""Detect if element has client-side event handlers (q:click, q:change, etc)"""
|
|
824
|
+
event_attributes = ['q:click', 'q:change', 'q:input', 'q:submit', 'q:keyup', 'q:keydown']
|
|
825
|
+
return any(attr in self.attributes for attr in event_attributes)
|
|
826
|
+
|
|
827
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
828
|
+
return {
|
|
829
|
+
"type": "html",
|
|
830
|
+
"tag": self.tag,
|
|
831
|
+
"attributes": self.attributes,
|
|
832
|
+
"children_count": len(self.children),
|
|
833
|
+
"self_closing": self.self_closing,
|
|
834
|
+
"has_events": self.has_events
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
def validate(self) -> List[str]:
|
|
838
|
+
errors = []
|
|
839
|
+
if not self.tag:
|
|
840
|
+
errors.append("HTML tag name is required")
|
|
841
|
+
|
|
842
|
+
# Validate children
|
|
843
|
+
for child in self.children:
|
|
844
|
+
if hasattr(child, 'validate'):
|
|
845
|
+
errors.extend(child.validate())
|
|
846
|
+
|
|
847
|
+
return errors
|
|
848
|
+
|
|
849
|
+
def __repr__(self):
|
|
850
|
+
return f'<HTMLNode tag={self.tag} attrs={len(self.attributes)} children={len(self.children)}>'
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
class TextNode(QuantumNode):
|
|
854
|
+
"""
|
|
855
|
+
Represents raw text content, possibly with databinding expressions.
|
|
856
|
+
|
|
857
|
+
Examples:
|
|
858
|
+
"Hello World"
|
|
859
|
+
"User: {user.name}"
|
|
860
|
+
"Total: ${price * quantity}"
|
|
861
|
+
"Items: {items.length}"
|
|
862
|
+
"""
|
|
863
|
+
|
|
864
|
+
def __init__(self, content: str):
|
|
865
|
+
self.content = content
|
|
866
|
+
self.has_databinding = '{' in content and '}' in content
|
|
867
|
+
|
|
868
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
869
|
+
preview = self.content[:50] + '...' if len(self.content) > 50 else self.content
|
|
870
|
+
return {
|
|
871
|
+
"type": "text",
|
|
872
|
+
"content": preview,
|
|
873
|
+
"has_databinding": self.has_databinding,
|
|
874
|
+
"length": len(self.content)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
def validate(self) -> List[str]:
|
|
878
|
+
# Text nodes are always valid
|
|
879
|
+
return []
|
|
880
|
+
|
|
881
|
+
def __repr__(self):
|
|
882
|
+
preview = self.content[:30] + '...' if len(self.content) > 30 else self.content
|
|
883
|
+
return f'<TextNode "{preview}">'
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
class DocTypeNode(QuantumNode):
|
|
887
|
+
"""
|
|
888
|
+
Represents HTML DOCTYPE declaration.
|
|
889
|
+
|
|
890
|
+
Example:
|
|
891
|
+
<!DOCTYPE html>
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
def __init__(self, value: str = "html"):
|
|
895
|
+
self.value = value
|
|
896
|
+
|
|
897
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
898
|
+
return {
|
|
899
|
+
"type": "doctype",
|
|
900
|
+
"value": self.value
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
def validate(self) -> List[str]:
|
|
904
|
+
return []
|
|
905
|
+
|
|
906
|
+
def __repr__(self):
|
|
907
|
+
return f'<DocTypeNode {self.value}>'
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
class CommentNode(QuantumNode):
|
|
911
|
+
"""
|
|
912
|
+
Represents HTML/XML comments.
|
|
913
|
+
|
|
914
|
+
Example:
|
|
915
|
+
<!-- This is a comment -->
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
def __init__(self, content: str):
|
|
919
|
+
self.content = content
|
|
920
|
+
|
|
921
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
922
|
+
return {
|
|
923
|
+
"type": "comment",
|
|
924
|
+
"content": self.content[:50] + '...' if len(self.content) > 50 else self.content
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
def validate(self) -> List[str]:
|
|
928
|
+
return []
|
|
929
|
+
|
|
930
|
+
def __repr__(self):
|
|
931
|
+
return f'<CommentNode>'
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
# ============================================
|
|
935
|
+
# COMPONENT COMPOSITION NODES (Phase 2)
|
|
936
|
+
# ============================================
|
|
937
|
+
|
|
938
|
+
class ImportNode(QuantumNode):
|
|
939
|
+
"""
|
|
940
|
+
Represents component import declaration.
|
|
941
|
+
|
|
942
|
+
Phase 2: Component Composition
|
|
943
|
+
|
|
944
|
+
Examples:
|
|
945
|
+
<q:import component="Header" />
|
|
946
|
+
<q:import component="Button" from="./components/ui" />
|
|
947
|
+
<q:import component="AdminLayout" as="Layout" />
|
|
948
|
+
|
|
949
|
+
Game Engine Imports (SMW Componentization):
|
|
950
|
+
<q:import behavior="PlayerBehavior" from="./behaviors" />
|
|
951
|
+
<q:import prefab="KoopaGreen" from="./prefabs" />
|
|
952
|
+
<q:import tilemap="yi1" from="./levels" />
|
|
953
|
+
"""
|
|
954
|
+
|
|
955
|
+
def __init__(
|
|
956
|
+
self,
|
|
957
|
+
component: Optional[str] = None,
|
|
958
|
+
from_path: Optional[str] = None,
|
|
959
|
+
alias: Optional[str] = None,
|
|
960
|
+
behavior: Optional[str] = None,
|
|
961
|
+
prefab: Optional[str] = None,
|
|
962
|
+
tilemap: Optional[str] = None
|
|
963
|
+
):
|
|
964
|
+
self.component = component
|
|
965
|
+
self.from_path = from_path
|
|
966
|
+
self.alias = alias or component or behavior or prefab or tilemap
|
|
967
|
+
|
|
968
|
+
# Game engine imports
|
|
969
|
+
self.behavior = behavior
|
|
970
|
+
self.prefab = prefab
|
|
971
|
+
self.tilemap = tilemap
|
|
972
|
+
|
|
973
|
+
@property
|
|
974
|
+
def import_type(self) -> str:
|
|
975
|
+
"""Return the type of import: component, behavior, prefab, or tilemap."""
|
|
976
|
+
if self.behavior:
|
|
977
|
+
return "behavior"
|
|
978
|
+
elif self.prefab:
|
|
979
|
+
return "prefab"
|
|
980
|
+
elif self.tilemap:
|
|
981
|
+
return "tilemap"
|
|
982
|
+
return "component"
|
|
983
|
+
|
|
984
|
+
@property
|
|
985
|
+
def name(self) -> str:
|
|
986
|
+
"""Return the imported item's name regardless of type."""
|
|
987
|
+
return self.component or self.behavior or self.prefab or self.tilemap or ""
|
|
988
|
+
|
|
989
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
990
|
+
return {
|
|
991
|
+
"type": "import",
|
|
992
|
+
"import_type": self.import_type,
|
|
993
|
+
"component": self.component,
|
|
994
|
+
"behavior": self.behavior,
|
|
995
|
+
"prefab": self.prefab,
|
|
996
|
+
"tilemap": self.tilemap,
|
|
997
|
+
"from": self.from_path,
|
|
998
|
+
"alias": self.alias
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
def validate(self) -> List[str]:
|
|
1002
|
+
errors = []
|
|
1003
|
+
if not self.component and not self.behavior and not self.prefab and not self.tilemap:
|
|
1004
|
+
errors.append("One of component, behavior, prefab, or tilemap is required for q:import")
|
|
1005
|
+
return errors
|
|
1006
|
+
|
|
1007
|
+
def __repr__(self):
|
|
1008
|
+
return f'<ImportNode {self.import_type}={self.name}>'
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
class SlotNode(QuantumNode):
|
|
1012
|
+
"""
|
|
1013
|
+
Represents content projection slot.
|
|
1014
|
+
|
|
1015
|
+
Phase 2: Component Composition
|
|
1016
|
+
Allows parent components to inject content into child components.
|
|
1017
|
+
|
|
1018
|
+
Examples:
|
|
1019
|
+
<q:slot /> <!-- Default slot -->
|
|
1020
|
+
<q:slot name="header" /> <!-- Named slot -->
|
|
1021
|
+
<q:slot name="footer">Default content here</q:slot>
|
|
1022
|
+
"""
|
|
1023
|
+
|
|
1024
|
+
def __init__(
|
|
1025
|
+
self,
|
|
1026
|
+
name: str = "default",
|
|
1027
|
+
default_content: Optional[List[QuantumNode]] = None
|
|
1028
|
+
):
|
|
1029
|
+
self.name = name
|
|
1030
|
+
self.default_content = default_content or []
|
|
1031
|
+
|
|
1032
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1033
|
+
return {
|
|
1034
|
+
"type": "slot",
|
|
1035
|
+
"name": self.name,
|
|
1036
|
+
"has_default": len(self.default_content) > 0
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
def validate(self) -> List[str]:
|
|
1040
|
+
errors = []
|
|
1041
|
+
# Validate default content
|
|
1042
|
+
for node in self.default_content:
|
|
1043
|
+
if hasattr(node, 'validate'):
|
|
1044
|
+
errors.extend(node.validate())
|
|
1045
|
+
return errors
|
|
1046
|
+
|
|
1047
|
+
def __repr__(self):
|
|
1048
|
+
return f'<SlotNode name={self.name}>'
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
class ComponentCallNode(QuantumNode):
|
|
1052
|
+
"""
|
|
1053
|
+
Represents a call to another component (used internally during composition).
|
|
1054
|
+
|
|
1055
|
+
Phase 2: Component Composition
|
|
1056
|
+
|
|
1057
|
+
Examples:
|
|
1058
|
+
<Header title="Products" />
|
|
1059
|
+
<Button label="Save" color="green" />
|
|
1060
|
+
<Card title="Info">
|
|
1061
|
+
<p>Card content here</p>
|
|
1062
|
+
</Card>
|
|
1063
|
+
"""
|
|
1064
|
+
|
|
1065
|
+
def __init__(
|
|
1066
|
+
self,
|
|
1067
|
+
component_name: str,
|
|
1068
|
+
props: Optional[Dict[str, str]] = None,
|
|
1069
|
+
children: Optional[List[QuantumNode]] = None
|
|
1070
|
+
):
|
|
1071
|
+
self.component_name = component_name
|
|
1072
|
+
self.props = props or {}
|
|
1073
|
+
self.children = children or []
|
|
1074
|
+
|
|
1075
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1076
|
+
return {
|
|
1077
|
+
"type": "component_call",
|
|
1078
|
+
"component": self.component_name,
|
|
1079
|
+
"props": self.props,
|
|
1080
|
+
"children_count": len(self.children)
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
def validate(self) -> List[str]:
|
|
1084
|
+
errors = []
|
|
1085
|
+
if not self.component_name:
|
|
1086
|
+
errors.append("Component name is required")
|
|
1087
|
+
|
|
1088
|
+
# Validate children
|
|
1089
|
+
for child in self.children:
|
|
1090
|
+
if hasattr(child, 'validate'):
|
|
1091
|
+
errors.extend(child.validate())
|
|
1092
|
+
|
|
1093
|
+
return errors
|
|
1094
|
+
|
|
1095
|
+
def __repr__(self):
|
|
1096
|
+
return f'<ComponentCallNode component={self.component_name} props={len(self.props)}>'
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
# ============================================
|
|
1100
|
+
# FORMS & ACTIONS NODES (Phase A)
|
|
1101
|
+
# ============================================
|
|
1102
|
+
|
|
1103
|
+
class ActionNode(QuantumNode):
|
|
1104
|
+
"""
|
|
1105
|
+
Represents a server-side action handler for forms.
|
|
1106
|
+
|
|
1107
|
+
Phase A: Forms & Actions
|
|
1108
|
+
Actions handle POST/PUT/DELETE requests with automatic form data binding,
|
|
1109
|
+
validation, and redirect support.
|
|
1110
|
+
|
|
1111
|
+
Examples:
|
|
1112
|
+
<q:action name="createUser" method="POST">
|
|
1113
|
+
<q:param name="email" type="email" required="true" />
|
|
1114
|
+
<q:param name="password" type="string" minlength="8" />
|
|
1115
|
+
<q:query datasource="db">
|
|
1116
|
+
INSERT INTO users (email, password) VALUES (:email, :password)
|
|
1117
|
+
</q:query>
|
|
1118
|
+
<q:redirect url="/users" />
|
|
1119
|
+
</q:action>
|
|
1120
|
+
|
|
1121
|
+
<q:action name="deleteProduct" method="DELETE">
|
|
1122
|
+
<q:param name="id" type="integer" required="true" />
|
|
1123
|
+
<q:query datasource="db">
|
|
1124
|
+
DELETE FROM products WHERE id = :id
|
|
1125
|
+
</q:query>
|
|
1126
|
+
<q:redirect url="/products" flash="Product deleted successfully" />
|
|
1127
|
+
</q:action>
|
|
1128
|
+
"""
|
|
1129
|
+
|
|
1130
|
+
def __init__(
|
|
1131
|
+
self,
|
|
1132
|
+
name: str,
|
|
1133
|
+
method: str = "POST"
|
|
1134
|
+
):
|
|
1135
|
+
self.name = name
|
|
1136
|
+
self.method = method.upper()
|
|
1137
|
+
self.params: List[QuantumParam] = []
|
|
1138
|
+
self.body: List[QuantumNode] = []
|
|
1139
|
+
|
|
1140
|
+
# Action configuration
|
|
1141
|
+
self.validate_csrf = True # Auto CSRF protection
|
|
1142
|
+
self.rate_limit = None # e.g., "10/minute"
|
|
1143
|
+
self.require_auth = False # Require authentication
|
|
1144
|
+
|
|
1145
|
+
def add_param(self, param: QuantumParam):
|
|
1146
|
+
"""Add parameter to action"""
|
|
1147
|
+
self.params.append(param)
|
|
1148
|
+
|
|
1149
|
+
def add_statement(self, statement: QuantumNode):
|
|
1150
|
+
"""Add statement to action body"""
|
|
1151
|
+
self.body.append(statement)
|
|
1152
|
+
|
|
1153
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1154
|
+
return {
|
|
1155
|
+
"type": "action",
|
|
1156
|
+
"name": self.name,
|
|
1157
|
+
"method": self.method,
|
|
1158
|
+
"params": [p.__dict__ for p in self.params],
|
|
1159
|
+
"body_statements": len(self.body),
|
|
1160
|
+
"validate_csrf": self.validate_csrf,
|
|
1161
|
+
"rate_limit": self.rate_limit,
|
|
1162
|
+
"require_auth": self.require_auth
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
def validate(self) -> List[str]:
|
|
1166
|
+
errors = []
|
|
1167
|
+
if not self.name:
|
|
1168
|
+
errors.append("Action name is required")
|
|
1169
|
+
|
|
1170
|
+
if self.method not in ['POST', 'PUT', 'DELETE', 'PATCH']:
|
|
1171
|
+
errors.append(f"Invalid action method: {self.method}. Must be POST, PUT, DELETE, or PATCH")
|
|
1172
|
+
|
|
1173
|
+
# Validate params
|
|
1174
|
+
for param in self.params:
|
|
1175
|
+
errors.extend(param.validate())
|
|
1176
|
+
|
|
1177
|
+
# Validate body
|
|
1178
|
+
for statement in self.body:
|
|
1179
|
+
if hasattr(statement, 'validate'):
|
|
1180
|
+
errors.extend(statement.validate())
|
|
1181
|
+
|
|
1182
|
+
return errors
|
|
1183
|
+
|
|
1184
|
+
def __repr__(self):
|
|
1185
|
+
return f'<ActionNode name={self.name} method={self.method}>'
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
class RedirectNode(QuantumNode):
|
|
1189
|
+
"""
|
|
1190
|
+
Represents a redirect response.
|
|
1191
|
+
|
|
1192
|
+
Phase A: Forms & Actions
|
|
1193
|
+
Used within actions to redirect after processing.
|
|
1194
|
+
|
|
1195
|
+
Examples:
|
|
1196
|
+
<q:redirect url="/thank-you" />
|
|
1197
|
+
<q:redirect url="/users/{userId}" />
|
|
1198
|
+
<q:redirect url="/products" flash="Product created successfully" />
|
|
1199
|
+
<q:redirect url="/error" status="500" flash="An error occurred" />
|
|
1200
|
+
"""
|
|
1201
|
+
|
|
1202
|
+
def __init__(
|
|
1203
|
+
self,
|
|
1204
|
+
url: str,
|
|
1205
|
+
flash: Optional[str] = None,
|
|
1206
|
+
status: int = 302
|
|
1207
|
+
):
|
|
1208
|
+
self.url = url
|
|
1209
|
+
self.flash = flash # Flash message to show on next page
|
|
1210
|
+
self.status = status # HTTP status code (302 default)
|
|
1211
|
+
|
|
1212
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1213
|
+
return {
|
|
1214
|
+
"type": "redirect",
|
|
1215
|
+
"url": self.url,
|
|
1216
|
+
"flash": self.flash,
|
|
1217
|
+
"status": self.status
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
def validate(self) -> List[str]:
|
|
1221
|
+
errors = []
|
|
1222
|
+
if not self.url:
|
|
1223
|
+
errors.append("Redirect URL is required")
|
|
1224
|
+
|
|
1225
|
+
# Validate status code
|
|
1226
|
+
if self.status not in [301, 302, 303, 307, 308]:
|
|
1227
|
+
errors.append(f"Invalid redirect status: {self.status}. Must be 301, 302, 303, 307, or 308")
|
|
1228
|
+
|
|
1229
|
+
return errors
|
|
1230
|
+
|
|
1231
|
+
def __repr__(self):
|
|
1232
|
+
return f'<RedirectNode url={self.url}>'
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
class FlashNode(QuantumNode):
|
|
1236
|
+
"""
|
|
1237
|
+
Represents a flash message (temporary message shown once).
|
|
1238
|
+
|
|
1239
|
+
Phase A: Forms & Actions
|
|
1240
|
+
Flash messages persist across redirects and are shown once.
|
|
1241
|
+
|
|
1242
|
+
Examples:
|
|
1243
|
+
<q:flash type="success" message="User created successfully" />
|
|
1244
|
+
<q:flash type="error" message="{errorMessage}" />
|
|
1245
|
+
<q:flash type="warning">Please verify your email</q:flash>
|
|
1246
|
+
"""
|
|
1247
|
+
|
|
1248
|
+
def __init__(
|
|
1249
|
+
self,
|
|
1250
|
+
message: str,
|
|
1251
|
+
flash_type: str = "info"
|
|
1252
|
+
):
|
|
1253
|
+
self.message = message
|
|
1254
|
+
self.flash_type = flash_type # info, success, warning, error
|
|
1255
|
+
|
|
1256
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1257
|
+
return {
|
|
1258
|
+
"type": "flash",
|
|
1259
|
+
"message": self.message,
|
|
1260
|
+
"flash_type": self.flash_type
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
def validate(self) -> List[str]:
|
|
1264
|
+
errors = []
|
|
1265
|
+
if not self.message:
|
|
1266
|
+
errors.append("Flash message is required")
|
|
1267
|
+
|
|
1268
|
+
if self.flash_type not in ['info', 'success', 'warning', 'error']:
|
|
1269
|
+
errors.append(f"Invalid flash type: {self.flash_type}. Must be info, success, warning, or error")
|
|
1270
|
+
|
|
1271
|
+
return errors
|
|
1272
|
+
|
|
1273
|
+
def __repr__(self):
|
|
1274
|
+
return f'<FlashNode type={self.flash_type}>'
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
class FileNode(QuantumNode):
|
|
1278
|
+
"""
|
|
1279
|
+
Represents file upload handling (q:file).
|
|
1280
|
+
|
|
1281
|
+
Phase H: File Uploads
|
|
1282
|
+
Handles file uploads with validation and storage.
|
|
1283
|
+
|
|
1284
|
+
Examples:
|
|
1285
|
+
<q:file action="upload" file="{avatar}" destination="./uploads/" />
|
|
1286
|
+
<q:file action="upload" file="{document}" destination="./files/" nameConflict="makeUnique" />
|
|
1287
|
+
"""
|
|
1288
|
+
|
|
1289
|
+
def __init__(
|
|
1290
|
+
self,
|
|
1291
|
+
action: str,
|
|
1292
|
+
file: str,
|
|
1293
|
+
destination: str = "./uploads/",
|
|
1294
|
+
name_conflict: str = "error", # error, overwrite, skip, makeUnique
|
|
1295
|
+
result: str = None
|
|
1296
|
+
):
|
|
1297
|
+
self.action = action # upload, delete, move, copy
|
|
1298
|
+
self.file = file # Variable name containing file data
|
|
1299
|
+
self.destination = destination # Where to save file
|
|
1300
|
+
self.name_conflict = name_conflict # What to do if file exists
|
|
1301
|
+
self.result = result # Optional variable name to store upload result
|
|
1302
|
+
|
|
1303
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1304
|
+
return {
|
|
1305
|
+
"type": "file",
|
|
1306
|
+
"action": self.action,
|
|
1307
|
+
"file": self.file,
|
|
1308
|
+
"destination": self.destination,
|
|
1309
|
+
"name_conflict": self.name_conflict,
|
|
1310
|
+
"result": self.result
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
def validate(self) -> List[str]:
|
|
1314
|
+
errors = []
|
|
1315
|
+
|
|
1316
|
+
if self.action not in ['upload', 'delete', 'move', 'copy']:
|
|
1317
|
+
errors.append(f"Invalid file action: {self.action}")
|
|
1318
|
+
|
|
1319
|
+
if not self.file:
|
|
1320
|
+
errors.append("File variable is required")
|
|
1321
|
+
|
|
1322
|
+
if self.action == 'upload' and not self.destination:
|
|
1323
|
+
errors.append("Destination is required for upload action")
|
|
1324
|
+
|
|
1325
|
+
if self.name_conflict not in ['error', 'overwrite', 'skip', 'makeUnique']:
|
|
1326
|
+
errors.append(f"Invalid nameConflict: {self.name_conflict}")
|
|
1327
|
+
|
|
1328
|
+
return errors
|
|
1329
|
+
|
|
1330
|
+
def __repr__(self):
|
|
1331
|
+
return f'<FileNode action={self.action} file={self.file}>'
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
class MailNode(QuantumNode):
|
|
1335
|
+
"""
|
|
1336
|
+
Represents email sending (q:mail).
|
|
1337
|
+
|
|
1338
|
+
Phase I: Email Sending
|
|
1339
|
+
ColdFusion cfmail-inspired email functionality.
|
|
1340
|
+
|
|
1341
|
+
Examples:
|
|
1342
|
+
<q:mail to="{email}" from="noreply@app.com" subject="Welcome!">
|
|
1343
|
+
<h1>Welcome {name}!</h1>
|
|
1344
|
+
</q:mail>
|
|
1345
|
+
"""
|
|
1346
|
+
|
|
1347
|
+
def __init__(
|
|
1348
|
+
self,
|
|
1349
|
+
to: str,
|
|
1350
|
+
subject: str,
|
|
1351
|
+
from_addr: str = None,
|
|
1352
|
+
cc: str = None,
|
|
1353
|
+
bcc: str = None,
|
|
1354
|
+
reply_to: str = None,
|
|
1355
|
+
type: str = "html", # html or text
|
|
1356
|
+
charset: str = "UTF-8"
|
|
1357
|
+
):
|
|
1358
|
+
self.to = to # Recipient email(s)
|
|
1359
|
+
self.subject = subject # Email subject
|
|
1360
|
+
self.from_addr = from_addr # Sender email
|
|
1361
|
+
self.cc = cc # CC recipients
|
|
1362
|
+
self.bcc = bcc # BCC recipients
|
|
1363
|
+
self.reply_to = reply_to # Reply-To address
|
|
1364
|
+
self.type = type # html or text
|
|
1365
|
+
self.charset = charset # Character encoding
|
|
1366
|
+
self.body = "" # Email body (set from tag content)
|
|
1367
|
+
self.attachments = [] # List of file attachments
|
|
1368
|
+
|
|
1369
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1370
|
+
return {
|
|
1371
|
+
"type": "mail",
|
|
1372
|
+
"to": self.to,
|
|
1373
|
+
"subject": self.subject,
|
|
1374
|
+
"from": self.from_addr,
|
|
1375
|
+
"cc": self.cc,
|
|
1376
|
+
"bcc": self.bcc,
|
|
1377
|
+
"reply_to": self.reply_to,
|
|
1378
|
+
"mail_type": self.type,
|
|
1379
|
+
"charset": self.charset,
|
|
1380
|
+
"body": self.body,
|
|
1381
|
+
"attachments": self.attachments
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
def validate(self) -> List[str]:
|
|
1385
|
+
errors = []
|
|
1386
|
+
|
|
1387
|
+
if not self.to:
|
|
1388
|
+
errors.append("Email recipient (to) is required")
|
|
1389
|
+
|
|
1390
|
+
if not self.subject:
|
|
1391
|
+
errors.append("Email subject is required")
|
|
1392
|
+
|
|
1393
|
+
if self.type not in ['html', 'text']:
|
|
1394
|
+
errors.append(f"Invalid email type: {self.type}. Must be 'html' or 'text'")
|
|
1395
|
+
|
|
1396
|
+
return errors
|
|
1397
|
+
|
|
1398
|
+
def __repr__(self):
|
|
1399
|
+
return f'<MailNode to={self.to} subject={self.subject}>'
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
# MIGRATED: KnowledgeNode moved to feature-based structure
|
|
1403
|
+
# Import from new location (Option C migration)
|
|
1404
|
+
from .features.knowledge_base.src import KnowledgeNode, KnowledgeSourceNode
|
|
1405
|
+
|
|
1406
|
+
# GAME ENGINE 2D: Import game AST nodes
|
|
1407
|
+
from .features.game_engine_2d.src.ast_nodes import (
|
|
1408
|
+
SceneNode, SpriteNode, PhysicsNode, ColliderNode, AnimationNode,
|
|
1409
|
+
CameraNode, InputNode, SoundNode, ParticleNode, TimerNode,
|
|
1410
|
+
SpawnNode, HudNode, TweenNode, TilemapNode, TilemapLayerNode,
|
|
1411
|
+
BehaviorNode, UseNode, PrefabNode, InstanceNode, GroupNode,
|
|
1412
|
+
StateMachineNode, StateNode, TransitionNode,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
class LLMMessageNode(QuantumNode):
|
|
1417
|
+
"""
|
|
1418
|
+
Represents <q:message> inside <q:llm> for chat mode.
|
|
1419
|
+
|
|
1420
|
+
Examples:
|
|
1421
|
+
<q:message role="system">You are a helpful assistant</q:message>
|
|
1422
|
+
<q:message role="user">{userQuestion}</q:message>
|
|
1423
|
+
"""
|
|
1424
|
+
|
|
1425
|
+
def __init__(self, role: str, content: str):
|
|
1426
|
+
self.role = role # "system", "user", "assistant"
|
|
1427
|
+
self.content = content
|
|
1428
|
+
|
|
1429
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1430
|
+
return {
|
|
1431
|
+
"type": "llm_message",
|
|
1432
|
+
"role": self.role,
|
|
1433
|
+
"content": self.content[:50] + '...' if len(self.content) > 50 else self.content
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
def validate(self) -> List[str]:
|
|
1437
|
+
errors = []
|
|
1438
|
+
if self.role not in ['system', 'user', 'assistant']:
|
|
1439
|
+
errors.append(f"Invalid message role: {self.role}. Must be system, user, or assistant")
|
|
1440
|
+
if not self.content:
|
|
1441
|
+
errors.append("Message content is required")
|
|
1442
|
+
return errors
|
|
1443
|
+
|
|
1444
|
+
def __repr__(self):
|
|
1445
|
+
return f'<LLMMessageNode role={self.role}>'
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
class LLMNode(QuantumNode):
|
|
1449
|
+
"""
|
|
1450
|
+
Represents <q:llm> - LLM invocation via Ollama/compatible API.
|
|
1451
|
+
|
|
1452
|
+
Examples:
|
|
1453
|
+
<q:llm name="greeting" model="phi3">
|
|
1454
|
+
<q:prompt>Say hello to {userName}</q:prompt>
|
|
1455
|
+
</q:llm>
|
|
1456
|
+
|
|
1457
|
+
<q:llm name="chat" model="mistral">
|
|
1458
|
+
<q:message role="system">You are a helpful assistant</q:message>
|
|
1459
|
+
<q:message role="user">{question}</q:message>
|
|
1460
|
+
</q:llm>
|
|
1461
|
+
|
|
1462
|
+
<q:llm name="data" model="llama3" responseFormat="json">
|
|
1463
|
+
<q:prompt>Extract name and age from: {text}</q:prompt>
|
|
1464
|
+
</q:llm>
|
|
1465
|
+
"""
|
|
1466
|
+
|
|
1467
|
+
def __init__(self, name: str):
|
|
1468
|
+
self.name = name # Result variable name
|
|
1469
|
+
self.model = None # e.g. "phi3", "mistral", "llama3"
|
|
1470
|
+
self.endpoint = None # Override base URL (optional)
|
|
1471
|
+
self.prompt = None # Prompt text (with databinding)
|
|
1472
|
+
self.system = None # System prompt (optional)
|
|
1473
|
+
self.messages: List[LLMMessageNode] = [] # Chat mode messages
|
|
1474
|
+
self.temperature = None # 0.0-2.0
|
|
1475
|
+
self.max_tokens = None # Token limit
|
|
1476
|
+
self.response_format = None # "text" or "json"
|
|
1477
|
+
self.cache = False
|
|
1478
|
+
self.ttl = None # Cache TTL in seconds (None = no expiry)
|
|
1479
|
+
self.provider = None # ollama, openai, anthropic, auto
|
|
1480
|
+
self.api_key = None # For cloud providers
|
|
1481
|
+
self.timeout = 30
|
|
1482
|
+
|
|
1483
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1484
|
+
return {
|
|
1485
|
+
"type": "llm",
|
|
1486
|
+
"name": self.name,
|
|
1487
|
+
"model": self.model,
|
|
1488
|
+
"endpoint": self.endpoint,
|
|
1489
|
+
"prompt": self.prompt[:50] + '...' if self.prompt and len(self.prompt) > 50 else self.prompt,
|
|
1490
|
+
"system": self.system,
|
|
1491
|
+
"messages": [m.to_dict() for m in self.messages],
|
|
1492
|
+
"temperature": self.temperature,
|
|
1493
|
+
"max_tokens": self.max_tokens,
|
|
1494
|
+
"response_format": self.response_format,
|
|
1495
|
+
"cache": self.cache,
|
|
1496
|
+
"timeout": self.timeout
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
def validate(self) -> List[str]:
|
|
1500
|
+
errors = []
|
|
1501
|
+
if not self.name:
|
|
1502
|
+
errors.append("LLM name (result variable) is required")
|
|
1503
|
+
if not self.prompt and not self.messages:
|
|
1504
|
+
errors.append("LLM requires either a <q:prompt> or <q:message> children")
|
|
1505
|
+
for msg in self.messages:
|
|
1506
|
+
errors.extend(msg.validate())
|
|
1507
|
+
return errors
|
|
1508
|
+
|
|
1509
|
+
def __repr__(self):
|
|
1510
|
+
mode = "chat" if self.messages else "completion"
|
|
1511
|
+
return f'<LLMNode name={self.name} model={self.model} mode={mode}>'
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
class TransactionNode(QuantumNode):
|
|
1515
|
+
"""
|
|
1516
|
+
Represents database transaction (q:transaction).
|
|
1517
|
+
|
|
1518
|
+
Phase D: Database Backend
|
|
1519
|
+
Ensures atomic operations - all queries succeed or all rollback.
|
|
1520
|
+
|
|
1521
|
+
Examples:
|
|
1522
|
+
<q:transaction>
|
|
1523
|
+
<q:query>UPDATE accounts SET balance = balance - 100 WHERE id = 1</q:query>
|
|
1524
|
+
<q:query>UPDATE accounts SET balance = balance + 100 WHERE id = 2</q:query>
|
|
1525
|
+
</q:transaction>
|
|
1526
|
+
"""
|
|
1527
|
+
|
|
1528
|
+
def __init__(
|
|
1529
|
+
self,
|
|
1530
|
+
isolation_level: str = "READ_COMMITTED"
|
|
1531
|
+
):
|
|
1532
|
+
self.isolation_level = isolation_level # READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
|
|
1533
|
+
self.statements: List[QuantumNode] = [] # Queries and other statements inside transaction
|
|
1534
|
+
|
|
1535
|
+
def add_statement(self, statement: QuantumNode):
|
|
1536
|
+
"""Add statement to transaction"""
|
|
1537
|
+
self.statements.append(statement)
|
|
1538
|
+
|
|
1539
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1540
|
+
return {
|
|
1541
|
+
"type": "transaction",
|
|
1542
|
+
"isolation_level": self.isolation_level,
|
|
1543
|
+
"statements": [s.to_dict() if hasattr(s, 'to_dict') else str(s) for s in self.statements]
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
def validate(self) -> List[str]:
|
|
1547
|
+
errors = []
|
|
1548
|
+
|
|
1549
|
+
if not self.statements:
|
|
1550
|
+
errors.append("Transaction must contain at least one statement")
|
|
1551
|
+
|
|
1552
|
+
valid_levels = ['READ_UNCOMMITTED', 'READ_COMMITTED', 'REPEATABLE_READ', 'SERIALIZABLE']
|
|
1553
|
+
if self.isolation_level not in valid_levels:
|
|
1554
|
+
errors.append(f"Invalid isolation level: {self.isolation_level}")
|
|
1555
|
+
|
|
1556
|
+
return errors
|
|
1557
|
+
|
|
1558
|
+
def __repr__(self):
|
|
1559
|
+
return f'<TransactionNode isolation={self.isolation_level} statements={len(self.statements)}>'
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
class InvokeHeaderNode(QuantumNode):
|
|
1563
|
+
"""
|
|
1564
|
+
Represents an HTTP header within q:invoke.
|
|
1565
|
+
|
|
1566
|
+
Example:
|
|
1567
|
+
<q:invoke url="..." method="POST">
|
|
1568
|
+
<q:header name="Authorization" value="Bearer {token}" />
|
|
1569
|
+
</q:invoke>
|
|
1570
|
+
"""
|
|
1571
|
+
|
|
1572
|
+
def __init__(self, name: str, value: str):
|
|
1573
|
+
self.name = name
|
|
1574
|
+
self.value = value
|
|
1575
|
+
|
|
1576
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1577
|
+
return {
|
|
1578
|
+
"type": "invoke_header",
|
|
1579
|
+
"name": self.name,
|
|
1580
|
+
"value": self.value
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
def validate(self) -> List[str]:
|
|
1584
|
+
errors = []
|
|
1585
|
+
if not self.name:
|
|
1586
|
+
errors.append("Invoke header requires 'name' attribute")
|
|
1587
|
+
if self.value is None:
|
|
1588
|
+
errors.append(f"Invoke header '{self.name}' requires 'value' attribute")
|
|
1589
|
+
return errors
|
|
1590
|
+
|
|
1591
|
+
def __repr__(self):
|
|
1592
|
+
return f'<InvokeHeaderNode name={self.name}>'
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
class HeaderNode(QuantumNode):
|
|
1596
|
+
"""
|
|
1597
|
+
Represents an HTTP header for data sources (q:data).
|
|
1598
|
+
|
|
1599
|
+
Example:
|
|
1600
|
+
<q:data name="api_data" source="https://api.example.com/data" type="json">
|
|
1601
|
+
<q:header name="Authorization" value="Bearer {token}" />
|
|
1602
|
+
</q:data>
|
|
1603
|
+
"""
|
|
1604
|
+
|
|
1605
|
+
def __init__(self, name: str, value: str):
|
|
1606
|
+
self.name = name
|
|
1607
|
+
self.value = value
|
|
1608
|
+
|
|
1609
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1610
|
+
return {
|
|
1611
|
+
"type": "header",
|
|
1612
|
+
"name": self.name,
|
|
1613
|
+
"value": self.value
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
def validate(self) -> List[str]:
|
|
1617
|
+
errors = []
|
|
1618
|
+
if not self.name:
|
|
1619
|
+
errors.append("Header requires 'name' attribute")
|
|
1620
|
+
if self.value is None:
|
|
1621
|
+
errors.append(f"Header '{self.name}' requires 'value' attribute")
|
|
1622
|
+
return errors
|
|
1623
|
+
|
|
1624
|
+
def __repr__(self):
|
|
1625
|
+
return f'<HeaderNode name={self.name}>'
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
class ColumnNode(QuantumNode):
|
|
1629
|
+
"""
|
|
1630
|
+
Represents a column definition for CSV data import.
|
|
1631
|
+
|
|
1632
|
+
Example:
|
|
1633
|
+
<q:data name="users" source="users.csv" type="csv">
|
|
1634
|
+
<q:column name="id" type="integer" required="true" />
|
|
1635
|
+
<q:column name="email" type="string" validate="email" />
|
|
1636
|
+
<q:column name="age" type="integer" min="0" max="150" />
|
|
1637
|
+
</q:data>
|
|
1638
|
+
"""
|
|
1639
|
+
|
|
1640
|
+
def __init__(self, name: str, col_type: str = 'string'):
|
|
1641
|
+
self.name = name
|
|
1642
|
+
self.col_type = col_type
|
|
1643
|
+
self.required: bool = False
|
|
1644
|
+
self.default: Optional[str] = None
|
|
1645
|
+
self.validate_rule: Optional[str] = None
|
|
1646
|
+
self.pattern: Optional[str] = None
|
|
1647
|
+
self.min: Optional[Union[int, float, str]] = None
|
|
1648
|
+
self.max: Optional[Union[int, float, str]] = None
|
|
1649
|
+
self.minlength: Optional[int] = None
|
|
1650
|
+
self.maxlength: Optional[int] = None
|
|
1651
|
+
self.range: Optional[str] = None
|
|
1652
|
+
self.enum: Optional[str] = None
|
|
1653
|
+
|
|
1654
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1655
|
+
return {
|
|
1656
|
+
"type": "column",
|
|
1657
|
+
"name": self.name,
|
|
1658
|
+
"col_type": self.col_type,
|
|
1659
|
+
"required": self.required,
|
|
1660
|
+
"default": self.default,
|
|
1661
|
+
"validate_rule": self.validate_rule,
|
|
1662
|
+
"pattern": self.pattern,
|
|
1663
|
+
"min": self.min,
|
|
1664
|
+
"max": self.max,
|
|
1665
|
+
"minlength": self.minlength,
|
|
1666
|
+
"maxlength": self.maxlength,
|
|
1667
|
+
"range": self.range,
|
|
1668
|
+
"enum": self.enum
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
def validate(self) -> List[str]:
|
|
1672
|
+
errors = []
|
|
1673
|
+
if not self.name:
|
|
1674
|
+
errors.append("Column requires 'name' attribute")
|
|
1675
|
+
return errors
|
|
1676
|
+
|
|
1677
|
+
def __repr__(self):
|
|
1678
|
+
return f'<ColumnNode name={self.name} type={self.col_type}>'
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
class FieldNode(QuantumNode):
|
|
1682
|
+
"""
|
|
1683
|
+
Represents a field mapping for XML data import.
|
|
1684
|
+
|
|
1685
|
+
Example:
|
|
1686
|
+
<q:data name="catalog" source="products.xml" type="xml">
|
|
1687
|
+
<q:field name="id" xpath="//product/@id" type="integer" />
|
|
1688
|
+
<q:field name="name" xpath="//product/name/text()" />
|
|
1689
|
+
</q:data>
|
|
1690
|
+
"""
|
|
1691
|
+
|
|
1692
|
+
def __init__(self, name: str, xpath: str, field_type: str = 'string'):
|
|
1693
|
+
self.name = name
|
|
1694
|
+
self.xpath = xpath
|
|
1695
|
+
self.field_type = field_type
|
|
1696
|
+
|
|
1697
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1698
|
+
return {
|
|
1699
|
+
"type": "field",
|
|
1700
|
+
"name": self.name,
|
|
1701
|
+
"xpath": self.xpath,
|
|
1702
|
+
"field_type": self.field_type
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
def validate(self) -> List[str]:
|
|
1706
|
+
errors = []
|
|
1707
|
+
if not self.name:
|
|
1708
|
+
errors.append("Field requires 'name' attribute")
|
|
1709
|
+
if not self.xpath:
|
|
1710
|
+
errors.append("Field requires 'xpath' attribute")
|
|
1711
|
+
return errors
|
|
1712
|
+
|
|
1713
|
+
def __repr__(self):
|
|
1714
|
+
return f'<FieldNode name={self.name} xpath={self.xpath}>'
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
class FilterNode(QuantumNode):
|
|
1718
|
+
"""
|
|
1719
|
+
Represents a filter operation in data transformation.
|
|
1720
|
+
|
|
1721
|
+
Example:
|
|
1722
|
+
<q:transform>
|
|
1723
|
+
<q:filter condition="age >= 18" />
|
|
1724
|
+
</q:transform>
|
|
1725
|
+
"""
|
|
1726
|
+
|
|
1727
|
+
def __init__(self, condition: str):
|
|
1728
|
+
self.condition = condition
|
|
1729
|
+
|
|
1730
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1731
|
+
return {
|
|
1732
|
+
"type": "filter",
|
|
1733
|
+
"condition": self.condition
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
def validate(self) -> List[str]:
|
|
1737
|
+
errors = []
|
|
1738
|
+
if not self.condition:
|
|
1739
|
+
errors.append("Filter requires 'condition' attribute")
|
|
1740
|
+
return errors
|
|
1741
|
+
|
|
1742
|
+
def __repr__(self):
|
|
1743
|
+
return f'<FilterNode condition={self.condition}>'
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
class SortNode(QuantumNode):
|
|
1747
|
+
"""
|
|
1748
|
+
Represents a sort operation in data transformation.
|
|
1749
|
+
|
|
1750
|
+
Example:
|
|
1751
|
+
<q:transform>
|
|
1752
|
+
<q:sort by="name" order="asc" />
|
|
1753
|
+
</q:transform>
|
|
1754
|
+
"""
|
|
1755
|
+
|
|
1756
|
+
def __init__(self, by: str, order: str = 'asc'):
|
|
1757
|
+
self.by = by
|
|
1758
|
+
self.order = order
|
|
1759
|
+
|
|
1760
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1761
|
+
return {
|
|
1762
|
+
"type": "sort",
|
|
1763
|
+
"by": self.by,
|
|
1764
|
+
"order": self.order
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
def validate(self) -> List[str]:
|
|
1768
|
+
errors = []
|
|
1769
|
+
if not self.by:
|
|
1770
|
+
errors.append("Sort requires 'by' attribute")
|
|
1771
|
+
if self.order not in ('asc', 'desc'):
|
|
1772
|
+
errors.append(f"Sort order must be 'asc' or 'desc', got: {self.order}")
|
|
1773
|
+
return errors
|
|
1774
|
+
|
|
1775
|
+
def __repr__(self):
|
|
1776
|
+
return f'<SortNode by={self.by} order={self.order}>'
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
class LimitNode(QuantumNode):
|
|
1780
|
+
"""
|
|
1781
|
+
Represents a limit operation in data transformation.
|
|
1782
|
+
|
|
1783
|
+
Example:
|
|
1784
|
+
<q:transform>
|
|
1785
|
+
<q:limit value="10" />
|
|
1786
|
+
</q:transform>
|
|
1787
|
+
"""
|
|
1788
|
+
|
|
1789
|
+
def __init__(self, value: int):
|
|
1790
|
+
self.value = value
|
|
1791
|
+
|
|
1792
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1793
|
+
return {
|
|
1794
|
+
"type": "limit",
|
|
1795
|
+
"value": self.value
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
def validate(self) -> List[str]:
|
|
1799
|
+
errors = []
|
|
1800
|
+
if not isinstance(self.value, int) or self.value < 0:
|
|
1801
|
+
errors.append("Limit value must be a non-negative integer")
|
|
1802
|
+
return errors
|
|
1803
|
+
|
|
1804
|
+
def __repr__(self):
|
|
1805
|
+
return f'<LimitNode value={self.value}>'
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
class ComputeNode(QuantumNode):
|
|
1809
|
+
"""
|
|
1810
|
+
Represents a compute operation in data transformation.
|
|
1811
|
+
|
|
1812
|
+
Example:
|
|
1813
|
+
<q:transform>
|
|
1814
|
+
<q:compute field="full_name" expression="first_name + ' ' + last_name" />
|
|
1815
|
+
</q:transform>
|
|
1816
|
+
"""
|
|
1817
|
+
|
|
1818
|
+
def __init__(self, field: str, expression: str, comp_type: str = 'string'):
|
|
1819
|
+
self.field = field
|
|
1820
|
+
self.expression = expression
|
|
1821
|
+
self.comp_type = comp_type
|
|
1822
|
+
|
|
1823
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1824
|
+
return {
|
|
1825
|
+
"type": "compute",
|
|
1826
|
+
"field": self.field,
|
|
1827
|
+
"expression": self.expression,
|
|
1828
|
+
"comp_type": self.comp_type
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
def validate(self) -> List[str]:
|
|
1832
|
+
errors = []
|
|
1833
|
+
if not self.field:
|
|
1834
|
+
errors.append("Compute requires 'field' attribute")
|
|
1835
|
+
if not self.expression:
|
|
1836
|
+
errors.append("Compute requires 'expression' attribute")
|
|
1837
|
+
return errors
|
|
1838
|
+
|
|
1839
|
+
def __repr__(self):
|
|
1840
|
+
return f'<ComputeNode field={self.field}>'
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
class TransformNode(QuantumNode):
|
|
1844
|
+
"""
|
|
1845
|
+
Represents a container for data transformation operations.
|
|
1846
|
+
|
|
1847
|
+
Example:
|
|
1848
|
+
<q:data name="filtered_users" source="users.csv" type="csv">
|
|
1849
|
+
<q:transform>
|
|
1850
|
+
<q:filter condition="age >= 18" />
|
|
1851
|
+
<q:sort by="name" order="asc" />
|
|
1852
|
+
<q:limit value="100" />
|
|
1853
|
+
</q:transform>
|
|
1854
|
+
</q:data>
|
|
1855
|
+
"""
|
|
1856
|
+
|
|
1857
|
+
def __init__(self):
|
|
1858
|
+
self.operations: List[QuantumNode] = []
|
|
1859
|
+
|
|
1860
|
+
def add_operation(self, operation: QuantumNode):
|
|
1861
|
+
"""Add a transformation operation (filter, sort, limit, compute)"""
|
|
1862
|
+
self.operations.append(operation)
|
|
1863
|
+
|
|
1864
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1865
|
+
return {
|
|
1866
|
+
"type": "transform",
|
|
1867
|
+
"operations": [op.to_dict() for op in self.operations]
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
def validate(self) -> List[str]:
|
|
1871
|
+
errors = []
|
|
1872
|
+
for op in self.operations:
|
|
1873
|
+
errors.extend(op.validate())
|
|
1874
|
+
return errors
|
|
1875
|
+
|
|
1876
|
+
def __repr__(self):
|
|
1877
|
+
return f'<TransformNode operations={len(self.operations)}>'
|
|
1878
|
+
|
|
1879
|
+
|
|
1880
|
+
class DataNode(QuantumNode):
|
|
1881
|
+
"""
|
|
1882
|
+
Represents a data import statement (q:data).
|
|
1883
|
+
|
|
1884
|
+
Supports importing data from various sources:
|
|
1885
|
+
- CSV files
|
|
1886
|
+
- XML files
|
|
1887
|
+
- JSON APIs
|
|
1888
|
+
- Excel files
|
|
1889
|
+
|
|
1890
|
+
Example:
|
|
1891
|
+
<q:data name="users" source="users.csv" type="csv" delimiter="," header="true">
|
|
1892
|
+
<q:column name="id" type="integer" />
|
|
1893
|
+
<q:column name="name" type="string" />
|
|
1894
|
+
<q:transform>
|
|
1895
|
+
<q:filter condition="id > 0" />
|
|
1896
|
+
</q:transform>
|
|
1897
|
+
</q:data>
|
|
1898
|
+
"""
|
|
1899
|
+
|
|
1900
|
+
def __init__(self, name: str, source: str, data_type: str = 'csv'):
|
|
1901
|
+
self.name = name
|
|
1902
|
+
self.source = source
|
|
1903
|
+
self.data_type = data_type
|
|
1904
|
+
|
|
1905
|
+
# Caching options
|
|
1906
|
+
self.cache: bool = True
|
|
1907
|
+
self.ttl: Optional[int] = None
|
|
1908
|
+
|
|
1909
|
+
# CSV-specific options
|
|
1910
|
+
self.delimiter: str = ','
|
|
1911
|
+
self.quote: str = '"'
|
|
1912
|
+
self.header: bool = True
|
|
1913
|
+
self.encoding: str = 'utf-8'
|
|
1914
|
+
self.skip_rows: int = 0
|
|
1915
|
+
|
|
1916
|
+
# XML-specific options
|
|
1917
|
+
self.xpath: Optional[str] = None
|
|
1918
|
+
self.namespace: Optional[str] = None
|
|
1919
|
+
|
|
1920
|
+
# Result metadata
|
|
1921
|
+
self.result: Optional[str] = None
|
|
1922
|
+
|
|
1923
|
+
# Child elements
|
|
1924
|
+
self.columns: List[ColumnNode] = []
|
|
1925
|
+
self.fields: List[FieldNode] = []
|
|
1926
|
+
self.transforms: List[TransformNode] = []
|
|
1927
|
+
self.headers: List[HeaderNode] = []
|
|
1928
|
+
|
|
1929
|
+
def add_column(self, column: 'ColumnNode'):
|
|
1930
|
+
"""Add a column definition (for CSV)"""
|
|
1931
|
+
self.columns.append(column)
|
|
1932
|
+
|
|
1933
|
+
def add_field(self, field: 'FieldNode'):
|
|
1934
|
+
"""Add a field mapping (for XML)"""
|
|
1935
|
+
self.fields.append(field)
|
|
1936
|
+
|
|
1937
|
+
def add_transform(self, transform: 'TransformNode'):
|
|
1938
|
+
"""Add a transform container"""
|
|
1939
|
+
self.transforms.append(transform)
|
|
1940
|
+
|
|
1941
|
+
def add_header(self, header: 'HeaderNode'):
|
|
1942
|
+
"""Add an HTTP header (for remote sources)"""
|
|
1943
|
+
self.headers.append(header)
|
|
1944
|
+
|
|
1945
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1946
|
+
return {
|
|
1947
|
+
"type": "data",
|
|
1948
|
+
"name": self.name,
|
|
1949
|
+
"source": self.source,
|
|
1950
|
+
"data_type": self.data_type,
|
|
1951
|
+
"cache": self.cache,
|
|
1952
|
+
"ttl": self.ttl,
|
|
1953
|
+
"delimiter": self.delimiter,
|
|
1954
|
+
"quote": self.quote,
|
|
1955
|
+
"header": self.header,
|
|
1956
|
+
"encoding": self.encoding,
|
|
1957
|
+
"skip_rows": self.skip_rows,
|
|
1958
|
+
"xpath": self.xpath,
|
|
1959
|
+
"namespace": self.namespace,
|
|
1960
|
+
"result": self.result,
|
|
1961
|
+
"columns": [c.to_dict() for c in self.columns],
|
|
1962
|
+
"fields": [f.to_dict() for f in self.fields],
|
|
1963
|
+
"transforms": [t.to_dict() for t in self.transforms],
|
|
1964
|
+
"headers": [h.to_dict() for h in self.headers]
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
def validate(self) -> List[str]:
|
|
1968
|
+
errors = []
|
|
1969
|
+
if not self.name:
|
|
1970
|
+
errors.append("Data requires 'name' attribute")
|
|
1971
|
+
if not self.source:
|
|
1972
|
+
errors.append("Data requires 'source' attribute")
|
|
1973
|
+
|
|
1974
|
+
for col in self.columns:
|
|
1975
|
+
errors.extend(col.validate())
|
|
1976
|
+
for field in self.fields:
|
|
1977
|
+
errors.extend(field.validate())
|
|
1978
|
+
for transform in self.transforms:
|
|
1979
|
+
errors.extend(transform.validate())
|
|
1980
|
+
for header in self.headers:
|
|
1981
|
+
errors.extend(header.validate())
|
|
1982
|
+
|
|
1983
|
+
return errors
|
|
1984
|
+
|
|
1985
|
+
def __repr__(self):
|
|
1986
|
+
return f'<DataNode name={self.name} source={self.source} type={self.data_type}>'
|
|
1987
|
+
|
|
1988
|
+
|
|
1989
|
+
# ============================================
|
|
1990
|
+
# MESSAGE QUEUE NODES (Phase: Message Queue System)
|
|
1991
|
+
# ============================================
|
|
1992
|
+
|
|
1993
|
+
@dataclass
|
|
1994
|
+
class MessageHeaderNode(QuantumNode):
|
|
1995
|
+
"""
|
|
1996
|
+
Represents a message header for q:message.
|
|
1997
|
+
|
|
1998
|
+
Example:
|
|
1999
|
+
<q:message topic="orders.created">
|
|
2000
|
+
<q:header name="priority" value="high" />
|
|
2001
|
+
<q:header name="correlation-id" value="{requestId}" />
|
|
2002
|
+
</q:message>
|
|
2003
|
+
"""
|
|
2004
|
+
name: str = ""
|
|
2005
|
+
value: str = ""
|
|
2006
|
+
|
|
2007
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2008
|
+
return {
|
|
2009
|
+
"type": "message_header",
|
|
2010
|
+
"name": self.name,
|
|
2011
|
+
"value": self.value
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
def validate(self) -> List[str]:
|
|
2015
|
+
errors = []
|
|
2016
|
+
if not self.name:
|
|
2017
|
+
errors.append("Message header requires 'name' attribute")
|
|
2018
|
+
return errors
|
|
2019
|
+
|
|
2020
|
+
def __repr__(self):
|
|
2021
|
+
return f'<MessageHeaderNode name={self.name}>'
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
@dataclass
|
|
2025
|
+
class MessageNode(QuantumNode):
|
|
2026
|
+
"""
|
|
2027
|
+
Represents a message publish/send operation (q:message).
|
|
2028
|
+
|
|
2029
|
+
Supports three modes:
|
|
2030
|
+
- publish: Pub/Sub to a topic (fanout)
|
|
2031
|
+
- send: Direct queue send (point-to-point)
|
|
2032
|
+
- request: Request/reply pattern with timeout
|
|
2033
|
+
|
|
2034
|
+
Examples:
|
|
2035
|
+
<!-- Publish to topic -->
|
|
2036
|
+
<q:message name="result" topic="orders.created" type="publish">
|
|
2037
|
+
<q:body>{orderData}</q:body>
|
|
2038
|
+
</q:message>
|
|
2039
|
+
|
|
2040
|
+
<!-- Send to queue -->
|
|
2041
|
+
<q:message name="taskId" queue="email-queue" type="send">
|
|
2042
|
+
<q:header name="priority" value="high" />
|
|
2043
|
+
<q:body>{"to": "{email}", "subject": "Welcome!"}</q:body>
|
|
2044
|
+
</q:message>
|
|
2045
|
+
|
|
2046
|
+
<!-- Request/reply -->
|
|
2047
|
+
<q:message name="response" queue="calculator" type="request" timeout="5000">
|
|
2048
|
+
<q:body>{"operation": "add", "a": 5, "b": 3}</q:body>
|
|
2049
|
+
</q:message>
|
|
2050
|
+
"""
|
|
2051
|
+
name: Optional[str] = None # result variable name
|
|
2052
|
+
topic: Optional[str] = None # for pub/sub
|
|
2053
|
+
queue: Optional[str] = None # for direct queue
|
|
2054
|
+
type: str = 'publish' # publish, send, request
|
|
2055
|
+
timeout: Optional[str] = None # for request type (ms)
|
|
2056
|
+
headers: List['MessageHeaderNode'] = field(default_factory=list)
|
|
2057
|
+
body: Optional[str] = None
|
|
2058
|
+
|
|
2059
|
+
def add_header(self, header: 'MessageHeaderNode'):
|
|
2060
|
+
"""Add a header to the message"""
|
|
2061
|
+
self.headers.append(header)
|
|
2062
|
+
|
|
2063
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2064
|
+
return {
|
|
2065
|
+
"type": "message",
|
|
2066
|
+
"name": self.name,
|
|
2067
|
+
"topic": self.topic,
|
|
2068
|
+
"queue": self.queue,
|
|
2069
|
+
"message_type": self.type,
|
|
2070
|
+
"timeout": self.timeout,
|
|
2071
|
+
"headers": [h.to_dict() for h in self.headers],
|
|
2072
|
+
"body": self.body[:50] + '...' if self.body and len(self.body) > 50 else self.body
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
def validate(self) -> List[str]:
|
|
2076
|
+
errors = []
|
|
2077
|
+
|
|
2078
|
+
if not self.topic and not self.queue:
|
|
2079
|
+
errors.append("Message requires either 'topic' or 'queue' attribute")
|
|
2080
|
+
|
|
2081
|
+
if self.topic and self.queue:
|
|
2082
|
+
errors.append("Message cannot have both 'topic' and 'queue' attributes")
|
|
2083
|
+
|
|
2084
|
+
if self.type not in ['publish', 'send', 'request']:
|
|
2085
|
+
errors.append(f"Invalid message type: {self.type}. Must be publish, send, or request")
|
|
2086
|
+
|
|
2087
|
+
if self.type == 'request' and not self.timeout:
|
|
2088
|
+
# Default timeout for request type
|
|
2089
|
+
pass # Will use default in runtime
|
|
2090
|
+
|
|
2091
|
+
for header in self.headers:
|
|
2092
|
+
errors.extend(header.validate())
|
|
2093
|
+
|
|
2094
|
+
return errors
|
|
2095
|
+
|
|
2096
|
+
def __repr__(self):
|
|
2097
|
+
target = self.topic if self.topic else self.queue
|
|
2098
|
+
return f'<MessageNode type={self.type} target={target}>'
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
@dataclass
|
|
2102
|
+
class SubscribeNode(QuantumNode):
|
|
2103
|
+
"""
|
|
2104
|
+
Represents a subscription to topics or queue consumption (q:subscribe).
|
|
2105
|
+
|
|
2106
|
+
Examples:
|
|
2107
|
+
<!-- Subscribe to topic -->
|
|
2108
|
+
<q:subscribe name="orderHandler" topic="orders.*" ack="auto">
|
|
2109
|
+
<q:onMessage>
|
|
2110
|
+
<q:set name="order" value="{message.body}" />
|
|
2111
|
+
<q:log message="Received order: {order.id}" />
|
|
2112
|
+
</q:onMessage>
|
|
2113
|
+
<q:onError>
|
|
2114
|
+
<q:log level="error" message="Failed to process: {error}" />
|
|
2115
|
+
</q:onError>
|
|
2116
|
+
</q:subscribe>
|
|
2117
|
+
|
|
2118
|
+
<!-- Consume from queue with manual ack -->
|
|
2119
|
+
<q:subscribe name="emailWorker" queue="email-queue" ack="manual" prefetch="10">
|
|
2120
|
+
<q:onMessage>
|
|
2121
|
+
<q:mail to="{message.body.to}" subject="{message.body.subject}" />
|
|
2122
|
+
<q:messageAck />
|
|
2123
|
+
</q:onMessage>
|
|
2124
|
+
</q:subscribe>
|
|
2125
|
+
"""
|
|
2126
|
+
name: str = ""
|
|
2127
|
+
topic: Optional[str] = None # single topic
|
|
2128
|
+
topics: Optional[str] = None # comma-separated or pattern
|
|
2129
|
+
queue: Optional[str] = None # for queue consumption
|
|
2130
|
+
ack: str = 'auto' # auto, manual
|
|
2131
|
+
prefetch: int = 1 # prefetch count
|
|
2132
|
+
on_message: List['QuantumNode'] = field(default_factory=list)
|
|
2133
|
+
on_error: List['QuantumNode'] = field(default_factory=list)
|
|
2134
|
+
|
|
2135
|
+
def add_on_message_statement(self, statement: 'QuantumNode'):
|
|
2136
|
+
"""Add statement to on_message handler"""
|
|
2137
|
+
self.on_message.append(statement)
|
|
2138
|
+
|
|
2139
|
+
def add_on_error_statement(self, statement: 'QuantumNode'):
|
|
2140
|
+
"""Add statement to on_error handler"""
|
|
2141
|
+
self.on_error.append(statement)
|
|
2142
|
+
|
|
2143
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2144
|
+
return {
|
|
2145
|
+
"type": "subscribe",
|
|
2146
|
+
"name": self.name,
|
|
2147
|
+
"topic": self.topic,
|
|
2148
|
+
"topics": self.topics,
|
|
2149
|
+
"queue": self.queue,
|
|
2150
|
+
"ack": self.ack,
|
|
2151
|
+
"prefetch": self.prefetch,
|
|
2152
|
+
"on_message_count": len(self.on_message),
|
|
2153
|
+
"on_error_count": len(self.on_error)
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
def validate(self) -> List[str]:
|
|
2157
|
+
errors = []
|
|
2158
|
+
|
|
2159
|
+
if not self.name:
|
|
2160
|
+
errors.append("Subscribe requires 'name' attribute")
|
|
2161
|
+
|
|
2162
|
+
if not self.topic and not self.topics and not self.queue:
|
|
2163
|
+
errors.append("Subscribe requires 'topic', 'topics', or 'queue' attribute")
|
|
2164
|
+
|
|
2165
|
+
if self.ack not in ['auto', 'manual']:
|
|
2166
|
+
errors.append(f"Invalid ack mode: {self.ack}. Must be 'auto' or 'manual'")
|
|
2167
|
+
|
|
2168
|
+
if self.prefetch < 1:
|
|
2169
|
+
errors.append(f"Prefetch must be at least 1 (got {self.prefetch})")
|
|
2170
|
+
|
|
2171
|
+
for statement in self.on_message:
|
|
2172
|
+
if hasattr(statement, 'validate'):
|
|
2173
|
+
errors.extend(statement.validate())
|
|
2174
|
+
|
|
2175
|
+
for statement in self.on_error:
|
|
2176
|
+
if hasattr(statement, 'validate'):
|
|
2177
|
+
errors.extend(statement.validate())
|
|
2178
|
+
|
|
2179
|
+
return errors
|
|
2180
|
+
|
|
2181
|
+
def __repr__(self):
|
|
2182
|
+
target = self.topic or self.topics or self.queue
|
|
2183
|
+
return f'<SubscribeNode name={self.name} target={target}>'
|
|
2184
|
+
|
|
2185
|
+
|
|
2186
|
+
@dataclass
|
|
2187
|
+
class QueueNode(QuantumNode):
|
|
2188
|
+
"""
|
|
2189
|
+
Represents queue/exchange declaration and management (q:queue).
|
|
2190
|
+
|
|
2191
|
+
Actions:
|
|
2192
|
+
- declare: Create queue/exchange if not exists
|
|
2193
|
+
- purge: Remove all messages from queue
|
|
2194
|
+
- delete: Delete the queue
|
|
2195
|
+
- info: Get queue information (message count, consumers, etc.)
|
|
2196
|
+
|
|
2197
|
+
Examples:
|
|
2198
|
+
<!-- Declare a durable queue -->
|
|
2199
|
+
<q:queue name="email-queue" action="declare" durable="true" />
|
|
2200
|
+
|
|
2201
|
+
<!-- Declare with dead letter queue -->
|
|
2202
|
+
<q:queue name="orders" action="declare" durable="true"
|
|
2203
|
+
deadLetterQueue="orders-dlq" ttl="86400000" />
|
|
2204
|
+
|
|
2205
|
+
<!-- Purge messages -->
|
|
2206
|
+
<q:queue name="temp-queue" action="purge" />
|
|
2207
|
+
|
|
2208
|
+
<!-- Get queue info -->
|
|
2209
|
+
<q:queue name="orders" action="info" result="queueInfo" />
|
|
2210
|
+
"""
|
|
2211
|
+
name: str = ""
|
|
2212
|
+
action: str = 'declare' # declare, purge, delete, info
|
|
2213
|
+
durable: bool = True
|
|
2214
|
+
exclusive: bool = False
|
|
2215
|
+
auto_delete: bool = False
|
|
2216
|
+
dead_letter_queue: Optional[str] = None
|
|
2217
|
+
ttl: Optional[int] = None # message TTL in ms
|
|
2218
|
+
result: Optional[str] = None # for info action
|
|
2219
|
+
|
|
2220
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2221
|
+
return {
|
|
2222
|
+
"type": "queue",
|
|
2223
|
+
"name": self.name,
|
|
2224
|
+
"action": self.action,
|
|
2225
|
+
"durable": self.durable,
|
|
2226
|
+
"exclusive": self.exclusive,
|
|
2227
|
+
"auto_delete": self.auto_delete,
|
|
2228
|
+
"dead_letter_queue": self.dead_letter_queue,
|
|
2229
|
+
"ttl": self.ttl,
|
|
2230
|
+
"result": self.result
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
def validate(self) -> List[str]:
|
|
2234
|
+
errors = []
|
|
2235
|
+
|
|
2236
|
+
if not self.name:
|
|
2237
|
+
errors.append("Queue requires 'name' attribute")
|
|
2238
|
+
|
|
2239
|
+
if self.action not in ['declare', 'purge', 'delete', 'info']:
|
|
2240
|
+
errors.append(f"Invalid queue action: {self.action}. Must be declare, purge, delete, or info")
|
|
2241
|
+
|
|
2242
|
+
if self.action == 'info' and not self.result:
|
|
2243
|
+
errors.append("Queue 'info' action requires 'result' attribute")
|
|
2244
|
+
|
|
2245
|
+
if self.ttl is not None and self.ttl < 0:
|
|
2246
|
+
errors.append(f"TTL must be non-negative (got {self.ttl})")
|
|
2247
|
+
|
|
2248
|
+
return errors
|
|
2249
|
+
|
|
2250
|
+
def __repr__(self):
|
|
2251
|
+
return f'<QueueNode name={self.name} action={self.action}>'
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
@dataclass
|
|
2255
|
+
class MessageAckNode(QuantumNode):
|
|
2256
|
+
"""
|
|
2257
|
+
Represents message acknowledgment (q:messageAck).
|
|
2258
|
+
|
|
2259
|
+
Used inside q:subscribe with ack="manual" to acknowledge
|
|
2260
|
+
successful message processing.
|
|
2261
|
+
|
|
2262
|
+
Example:
|
|
2263
|
+
<q:subscribe name="worker" queue="tasks" ack="manual">
|
|
2264
|
+
<q:onMessage>
|
|
2265
|
+
<!-- Process message -->
|
|
2266
|
+
<q:set name="processed" value="true" />
|
|
2267
|
+
<!-- Acknowledge on success -->
|
|
2268
|
+
<q:messageAck />
|
|
2269
|
+
</q:onMessage>
|
|
2270
|
+
</q:subscribe>
|
|
2271
|
+
"""
|
|
2272
|
+
|
|
2273
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2274
|
+
return {
|
|
2275
|
+
"type": "message_ack"
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
def validate(self) -> List[str]:
|
|
2279
|
+
return []
|
|
2280
|
+
|
|
2281
|
+
def __repr__(self):
|
|
2282
|
+
return '<MessageAckNode>'
|
|
2283
|
+
|
|
2284
|
+
|
|
2285
|
+
@dataclass
|
|
2286
|
+
class MessageNackNode(QuantumNode):
|
|
2287
|
+
"""
|
|
2288
|
+
Represents negative message acknowledgment (q:messageNack).
|
|
2289
|
+
|
|
2290
|
+
Used inside q:subscribe with ack="manual" to reject a message.
|
|
2291
|
+
The requeue attribute controls whether the message is requeued.
|
|
2292
|
+
|
|
2293
|
+
Examples:
|
|
2294
|
+
<!-- Reject and requeue (for retry) -->
|
|
2295
|
+
<q:messageNack requeue="true" />
|
|
2296
|
+
|
|
2297
|
+
<!-- Reject without requeue (send to DLQ if configured) -->
|
|
2298
|
+
<q:messageNack requeue="false" />
|
|
2299
|
+
"""
|
|
2300
|
+
requeue: bool = True
|
|
2301
|
+
|
|
2302
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2303
|
+
return {
|
|
2304
|
+
"type": "message_nack",
|
|
2305
|
+
"requeue": self.requeue
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
def validate(self) -> List[str]:
|
|
2309
|
+
return []
|
|
2310
|
+
|
|
2311
|
+
def __repr__(self):
|
|
2312
|
+
return f'<MessageNackNode requeue={self.requeue}>'
|
|
2313
|
+
|
|
2314
|
+
|
|
2315
|
+
# =============================================================================
|
|
2316
|
+
# Python Scripting Nodes (q:python, q:pyimport, q:class, q:decorator)
|
|
2317
|
+
# =============================================================================
|
|
2318
|
+
|
|
2319
|
+
@dataclass
|
|
2320
|
+
class PythonNode(QuantumNode):
|
|
2321
|
+
"""
|
|
2322
|
+
Represents a Python code block (q:python).
|
|
2323
|
+
|
|
2324
|
+
Allows embedding native Python code within Quantum components.
|
|
2325
|
+
The 'q' bridge object provides access to the Quantum context.
|
|
2326
|
+
|
|
2327
|
+
Attributes:
|
|
2328
|
+
code: The Python code to execute
|
|
2329
|
+
scope: Variable scope ('component', 'isolated', 'module')
|
|
2330
|
+
async_mode: Whether to execute as async code
|
|
2331
|
+
timeout: Maximum execution time (e.g., '30s')
|
|
2332
|
+
result: Variable name to store the result
|
|
2333
|
+
|
|
2334
|
+
Examples:
|
|
2335
|
+
<!-- Simple Python block -->
|
|
2336
|
+
<q:python>
|
|
2337
|
+
import pandas as pd
|
|
2338
|
+
df = pd.read_csv('data.csv')
|
|
2339
|
+
q.total = df['sales'].sum()
|
|
2340
|
+
</q:python>
|
|
2341
|
+
|
|
2342
|
+
<!-- Async execution -->
|
|
2343
|
+
<q:python async="true" timeout="30s">
|
|
2344
|
+
import aiohttp
|
|
2345
|
+
async with aiohttp.ClientSession() as session:
|
|
2346
|
+
response = await session.get(url)
|
|
2347
|
+
q.data = await response.json()
|
|
2348
|
+
</q:python>
|
|
2349
|
+
|
|
2350
|
+
<!-- Isolated scope (variables don't leak) -->
|
|
2351
|
+
<q:python scope="isolated">
|
|
2352
|
+
temp = calculate_something()
|
|
2353
|
+
q.export('result', temp)
|
|
2354
|
+
</q:python>
|
|
2355
|
+
|
|
2356
|
+
<!-- With result capture -->
|
|
2357
|
+
<q:python result="calculation">
|
|
2358
|
+
return sum([1, 2, 3, 4, 5])
|
|
2359
|
+
</q:python>
|
|
2360
|
+
"""
|
|
2361
|
+
code: str
|
|
2362
|
+
scope: str = "component" # component, isolated, module
|
|
2363
|
+
async_mode: bool = False
|
|
2364
|
+
timeout: Optional[str] = None
|
|
2365
|
+
result: Optional[str] = None
|
|
2366
|
+
|
|
2367
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2368
|
+
return {
|
|
2369
|
+
"type": "python",
|
|
2370
|
+
"code": self.code,
|
|
2371
|
+
"scope": self.scope,
|
|
2372
|
+
"async": self.async_mode,
|
|
2373
|
+
"timeout": self.timeout,
|
|
2374
|
+
"result": self.result
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
def validate(self) -> List[str]:
|
|
2378
|
+
errors = []
|
|
2379
|
+
if not self.code or not self.code.strip():
|
|
2380
|
+
errors.append("Python code block cannot be empty")
|
|
2381
|
+
if self.scope not in ('component', 'isolated', 'module'):
|
|
2382
|
+
errors.append(f"Invalid scope: {self.scope}. Must be 'component', 'isolated', or 'module'")
|
|
2383
|
+
return errors
|
|
2384
|
+
|
|
2385
|
+
def __repr__(self):
|
|
2386
|
+
code_preview = self.code[:50] + '...' if len(self.code) > 50 else self.code
|
|
2387
|
+
return f'<PythonNode scope={self.scope} code="{code_preview}">'
|
|
2388
|
+
|
|
2389
|
+
|
|
2390
|
+
@dataclass
|
|
2391
|
+
class PyImportNode(QuantumNode):
|
|
2392
|
+
"""
|
|
2393
|
+
Represents a Python module import (q:pyimport).
|
|
2394
|
+
|
|
2395
|
+
Imports Python modules into the component's namespace for use
|
|
2396
|
+
in q:python blocks and expressions.
|
|
2397
|
+
|
|
2398
|
+
Attributes:
|
|
2399
|
+
module: The module to import (e.g., 'pandas', 'sklearn.ensemble')
|
|
2400
|
+
alias: Optional alias (e.g., 'pd' for pandas)
|
|
2401
|
+
names: Specific names to import from the module
|
|
2402
|
+
|
|
2403
|
+
Examples:
|
|
2404
|
+
<!-- Import with alias -->
|
|
2405
|
+
<q:pyimport module="pandas" as="pd" />
|
|
2406
|
+
<q:pyimport module="numpy" as="np" />
|
|
2407
|
+
|
|
2408
|
+
<!-- Import specific names -->
|
|
2409
|
+
<q:pyimport module="sklearn.ensemble" names="RandomForestClassifier, GradientBoostingClassifier" />
|
|
2410
|
+
|
|
2411
|
+
<!-- Import everything from module -->
|
|
2412
|
+
<q:pyimport module="math" names="*" />
|
|
2413
|
+
|
|
2414
|
+
<!-- Simple import -->
|
|
2415
|
+
<q:pyimport module="json" />
|
|
2416
|
+
"""
|
|
2417
|
+
module: str
|
|
2418
|
+
alias: Optional[str] = None
|
|
2419
|
+
names: List[str] = field(default_factory=list)
|
|
2420
|
+
|
|
2421
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2422
|
+
return {
|
|
2423
|
+
"type": "pyimport",
|
|
2424
|
+
"module": self.module,
|
|
2425
|
+
"alias": self.alias,
|
|
2426
|
+
"names": self.names
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
def validate(self) -> List[str]:
|
|
2430
|
+
errors = []
|
|
2431
|
+
if not self.module:
|
|
2432
|
+
errors.append("Module name is required for q:pyimport")
|
|
2433
|
+
if self.alias and self.names:
|
|
2434
|
+
errors.append("Cannot use both 'as' and 'names' in the same import")
|
|
2435
|
+
return errors
|
|
2436
|
+
|
|
2437
|
+
def __repr__(self):
|
|
2438
|
+
if self.alias:
|
|
2439
|
+
return f'<PyImportNode module={self.module} as={self.alias}>'
|
|
2440
|
+
elif self.names:
|
|
2441
|
+
return f'<PyImportNode from={self.module} names={self.names}>'
|
|
2442
|
+
return f'<PyImportNode module={self.module}>'
|
|
2443
|
+
|
|
2444
|
+
|
|
2445
|
+
@dataclass
|
|
2446
|
+
class PyClassNode(QuantumNode):
|
|
2447
|
+
"""
|
|
2448
|
+
Represents an inline Python class definition (q:class).
|
|
2449
|
+
|
|
2450
|
+
Allows defining Python classes within Quantum components.
|
|
2451
|
+
|
|
2452
|
+
Attributes:
|
|
2453
|
+
name: The class name
|
|
2454
|
+
code: The class body (methods, attributes)
|
|
2455
|
+
bases: Base classes to inherit from
|
|
2456
|
+
decorators: Decorators to apply to the class
|
|
2457
|
+
|
|
2458
|
+
Examples:
|
|
2459
|
+
<q:class name="OrderProcessor">
|
|
2460
|
+
def __init__(self, db):
|
|
2461
|
+
self.db = db
|
|
2462
|
+
|
|
2463
|
+
def process(self, order_id):
|
|
2464
|
+
order = self.db.get(order_id)
|
|
2465
|
+
return self.validate(order)
|
|
2466
|
+
|
|
2467
|
+
def validate(self, order):
|
|
2468
|
+
return order.total > 0
|
|
2469
|
+
</q:class>
|
|
2470
|
+
|
|
2471
|
+
<!-- With inheritance -->
|
|
2472
|
+
<q:class name="AdminUser" bases="User, Permissions">
|
|
2473
|
+
role = 'admin'
|
|
2474
|
+
|
|
2475
|
+
def has_permission(self, perm):
|
|
2476
|
+
return True
|
|
2477
|
+
</q:class>
|
|
2478
|
+
"""
|
|
2479
|
+
name: str
|
|
2480
|
+
code: str
|
|
2481
|
+
bases: List[str] = field(default_factory=list)
|
|
2482
|
+
decorators: List[str] = field(default_factory=list)
|
|
2483
|
+
|
|
2484
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2485
|
+
return {
|
|
2486
|
+
"type": "pyclass",
|
|
2487
|
+
"name": self.name,
|
|
2488
|
+
"code": self.code,
|
|
2489
|
+
"bases": self.bases,
|
|
2490
|
+
"decorators": self.decorators
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
def validate(self) -> List[str]:
|
|
2494
|
+
errors = []
|
|
2495
|
+
if not self.name:
|
|
2496
|
+
errors.append("Class name is required for q:class")
|
|
2497
|
+
if not self.name.isidentifier():
|
|
2498
|
+
errors.append(f"Invalid class name: {self.name}")
|
|
2499
|
+
return errors
|
|
2500
|
+
|
|
2501
|
+
def __repr__(self):
|
|
2502
|
+
bases_str = f"({', '.join(self.bases)})" if self.bases else ""
|
|
2503
|
+
return f'<PyClassNode name={self.name}{bases_str}>'
|
|
2504
|
+
|
|
2505
|
+
|
|
2506
|
+
@dataclass
|
|
2507
|
+
class PyDecoratorNode(QuantumNode):
|
|
2508
|
+
"""
|
|
2509
|
+
Represents a Python decorator definition (q:decorator).
|
|
2510
|
+
|
|
2511
|
+
Allows defining reusable decorators for functions.
|
|
2512
|
+
|
|
2513
|
+
Attributes:
|
|
2514
|
+
name: The decorator name
|
|
2515
|
+
code: The decorator implementation
|
|
2516
|
+
params: Parameters the decorator accepts
|
|
2517
|
+
|
|
2518
|
+
Examples:
|
|
2519
|
+
<!-- Caching decorator -->
|
|
2520
|
+
<q:decorator name="cached" params="ttl">
|
|
2521
|
+
from functools import wraps
|
|
2522
|
+
def decorator(func):
|
|
2523
|
+
cache = {}
|
|
2524
|
+
@wraps(func)
|
|
2525
|
+
def wrapper(*args):
|
|
2526
|
+
if args in cache:
|
|
2527
|
+
return cache[args]
|
|
2528
|
+
result = func(*args)
|
|
2529
|
+
cache[args] = result
|
|
2530
|
+
return result
|
|
2531
|
+
return wrapper
|
|
2532
|
+
return decorator
|
|
2533
|
+
</q:decorator>
|
|
2534
|
+
|
|
2535
|
+
<!-- Logging decorator -->
|
|
2536
|
+
<q:decorator name="logged">
|
|
2537
|
+
import logging
|
|
2538
|
+
def decorator(func):
|
|
2539
|
+
def wrapper(*args, **kwargs):
|
|
2540
|
+
logging.info(f"Calling {func.__name__}")
|
|
2541
|
+
return func(*args, **kwargs)
|
|
2542
|
+
return wrapper
|
|
2543
|
+
return decorator
|
|
2544
|
+
</q:decorator>
|
|
2545
|
+
"""
|
|
2546
|
+
name: str
|
|
2547
|
+
code: str
|
|
2548
|
+
params: List[str] = field(default_factory=list)
|
|
2549
|
+
|
|
2550
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2551
|
+
return {
|
|
2552
|
+
"type": "pydecorator",
|
|
2553
|
+
"name": self.name,
|
|
2554
|
+
"code": self.code,
|
|
2555
|
+
"params": self.params
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
def validate(self) -> List[str]:
|
|
2559
|
+
errors = []
|
|
2560
|
+
if not self.name:
|
|
2561
|
+
errors.append("Decorator name is required")
|
|
2562
|
+
if not self.name.isidentifier():
|
|
2563
|
+
errors.append(f"Invalid decorator name: {self.name}")
|
|
2564
|
+
return errors
|
|
2565
|
+
|
|
2566
|
+
def __repr__(self):
|
|
2567
|
+
return f'<PyDecoratorNode name={self.name}>'
|
|
2568
|
+
|
|
2569
|
+
|
|
2570
|
+
@dataclass
|
|
2571
|
+
class PyExprNode(QuantumNode):
|
|
2572
|
+
"""
|
|
2573
|
+
Represents an inline Python expression (q:expr or {py: ...}).
|
|
2574
|
+
|
|
2575
|
+
Used for embedding Python expressions within templates and attributes.
|
|
2576
|
+
|
|
2577
|
+
Attributes:
|
|
2578
|
+
expr: The Python expression to evaluate
|
|
2579
|
+
format_spec: Optional format specification
|
|
2580
|
+
|
|
2581
|
+
Examples:
|
|
2582
|
+
<!-- In attributes -->
|
|
2583
|
+
<q:set name="total" value="{py: sum(items)}" />
|
|
2584
|
+
|
|
2585
|
+
<!-- With formatting -->
|
|
2586
|
+
<span>{py: price * 1.1:.2f}</span>
|
|
2587
|
+
|
|
2588
|
+
<!-- List comprehension -->
|
|
2589
|
+
<q:set name="adults" value="{py: [u for u in users if u.age >= 18]}" />
|
|
2590
|
+
|
|
2591
|
+
<!-- Ternary expression -->
|
|
2592
|
+
<span class="{py: 'active' if is_active else 'inactive'}">Status</span>
|
|
2593
|
+
"""
|
|
2594
|
+
expr: str
|
|
2595
|
+
format_spec: Optional[str] = None
|
|
2596
|
+
|
|
2597
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
2598
|
+
return {
|
|
2599
|
+
"type": "pyexpr",
|
|
2600
|
+
"expr": self.expr,
|
|
2601
|
+
"format_spec": self.format_spec
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
def validate(self) -> List[str]:
|
|
2605
|
+
errors = []
|
|
2606
|
+
if not self.expr or not self.expr.strip():
|
|
2607
|
+
errors.append("Python expression cannot be empty")
|
|
2608
|
+
return errors
|
|
2609
|
+
|
|
2610
|
+
def __repr__(self):
|
|
2611
|
+
return f'<PyExprNode expr="{self.expr}">'
|