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,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Set Parser - Parse q:set statements
|
|
3
|
+
|
|
4
|
+
Handles variable assignment with validation and persistence.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
from xml.etree import ElementTree as ET
|
|
9
|
+
from quantum.core.parsers.base import BaseTagParser, ParserError
|
|
10
|
+
from quantum.core.features.state_management.src.ast_node import SetNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SetParser(BaseTagParser):
|
|
14
|
+
"""
|
|
15
|
+
Parser for q:set statements.
|
|
16
|
+
|
|
17
|
+
Supports:
|
|
18
|
+
- Type-safe variable assignment
|
|
19
|
+
- Validation rules
|
|
20
|
+
- Scope management
|
|
21
|
+
- Operations (assign, append, increment, etc.)
|
|
22
|
+
- State persistence
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def tag_names(self) -> List[str]:
|
|
27
|
+
return ['set']
|
|
28
|
+
|
|
29
|
+
def parse(self, element: ET.Element) -> SetNode:
|
|
30
|
+
"""
|
|
31
|
+
Parse q:set statement.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
element: XML element for q:set
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
SetNode AST node
|
|
38
|
+
"""
|
|
39
|
+
name = self.get_attr(element, 'name')
|
|
40
|
+
|
|
41
|
+
if not name:
|
|
42
|
+
raise ParserError("Set requires 'name' attribute")
|
|
43
|
+
|
|
44
|
+
set_node = SetNode(name)
|
|
45
|
+
|
|
46
|
+
# Type and value
|
|
47
|
+
set_node.type = self.get_attr(element, 'type', 'string')
|
|
48
|
+
set_node.value = self.get_attr(element, 'value')
|
|
49
|
+
set_node.default = self.get_attr(element, 'default')
|
|
50
|
+
|
|
51
|
+
# Validation
|
|
52
|
+
set_node.required = self.get_bool_attr(element, 'required', False)
|
|
53
|
+
set_node.nullable = self.get_bool_attr(element, 'nullable', True)
|
|
54
|
+
set_node.validate_rule = self.get_attr(element, 'validate')
|
|
55
|
+
set_node.pattern = self.get_attr(element, 'pattern')
|
|
56
|
+
set_node.mask = self.get_attr(element, 'mask')
|
|
57
|
+
set_node.range = self.get_attr(element, 'range')
|
|
58
|
+
set_node.enum = self.get_attr(element, 'enum')
|
|
59
|
+
set_node.unique = self.get_attr(element, 'unique')
|
|
60
|
+
set_node.min = self.get_attr(element, 'min')
|
|
61
|
+
set_node.max = self.get_attr(element, 'max')
|
|
62
|
+
set_node.minlength = self.get_attr(element, 'minlength')
|
|
63
|
+
set_node.maxlength = self.get_attr(element, 'maxlength')
|
|
64
|
+
|
|
65
|
+
# Behavior
|
|
66
|
+
set_node.scope = self.get_attr(element, 'scope', 'local')
|
|
67
|
+
set_node.operation = self.get_attr(element, 'operation', 'assign')
|
|
68
|
+
|
|
69
|
+
# Step for increment/decrement
|
|
70
|
+
set_node.step = self.get_int_attr(element, 'step', 1)
|
|
71
|
+
|
|
72
|
+
# Collection operations
|
|
73
|
+
set_node.index = self.get_attr(element, 'index')
|
|
74
|
+
set_node.key = self.get_attr(element, 'key')
|
|
75
|
+
set_node.source = self.get_attr(element, 'source')
|
|
76
|
+
|
|
77
|
+
# State persistence
|
|
78
|
+
set_node.persist = self.get_attr(element, 'persist')
|
|
79
|
+
set_node.persist_key = self.get_attr(element, 'persistKey')
|
|
80
|
+
set_node.persist_encrypt = self.get_bool_attr(element, 'persistEncrypt', False)
|
|
81
|
+
|
|
82
|
+
persist_ttl = self.get_attr(element, 'persistTtl')
|
|
83
|
+
if persist_ttl:
|
|
84
|
+
try:
|
|
85
|
+
set_node.persist_ttl = int(persist_ttl)
|
|
86
|
+
except ValueError:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return set_node
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Parsers
|
|
3
|
+
|
|
4
|
+
Parsers for data operations: query, invoke, data, transaction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .query_parser import QueryParser
|
|
8
|
+
from .invoke_parser import InvokeParser
|
|
9
|
+
from .data_parser import DataParser
|
|
10
|
+
from .transaction_parser import TransactionParser
|
|
11
|
+
|
|
12
|
+
__all__ = ['QueryParser', 'InvokeParser', 'DataParser', 'TransactionParser']
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Parser - Parse q:data statements
|
|
3
|
+
|
|
4
|
+
Handles data import from CSV, JSON, XML sources.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
from xml.etree import ElementTree as ET
|
|
9
|
+
from quantum.core.parsers.base import BaseTagParser, ParserError
|
|
10
|
+
from quantum.core.ast_nodes import (
|
|
11
|
+
DataNode, ColumnNode, FieldNode, TransformNode, HeaderNode,
|
|
12
|
+
FilterNode, SortNode, LimitNode, ComputeNode,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataParser(BaseTagParser):
|
|
17
|
+
"""
|
|
18
|
+
Parser for q:data statements.
|
|
19
|
+
|
|
20
|
+
Supports:
|
|
21
|
+
- CSV import with column definitions
|
|
22
|
+
- JSON import with field mappings
|
|
23
|
+
- XML import with XPath
|
|
24
|
+
- Data transformations
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def tag_names(self) -> List[str]:
|
|
29
|
+
return ['data']
|
|
30
|
+
|
|
31
|
+
def parse(self, element: ET.Element) -> DataNode:
|
|
32
|
+
"""
|
|
33
|
+
Parse q:data statement.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
element: XML element for q:data
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
DataNode AST node
|
|
40
|
+
"""
|
|
41
|
+
name = self.get_attr(element, 'name')
|
|
42
|
+
source = self.get_attr(element, 'source')
|
|
43
|
+
data_type = self.get_attr(element, 'type', 'csv')
|
|
44
|
+
|
|
45
|
+
if not name:
|
|
46
|
+
raise ParserError("Data requires 'name' attribute")
|
|
47
|
+
if not source:
|
|
48
|
+
raise ParserError("Data requires 'source' attribute")
|
|
49
|
+
|
|
50
|
+
data_node = DataNode(name, source, data_type)
|
|
51
|
+
|
|
52
|
+
# Caching
|
|
53
|
+
data_node.cache = self.get_bool_attr(element, 'cache', True)
|
|
54
|
+
data_node.ttl = self.get_int_attr(element, 'ttl', 0) or None
|
|
55
|
+
|
|
56
|
+
# CSV attributes
|
|
57
|
+
data_node.delimiter = self.get_attr(element, 'delimiter', ',')
|
|
58
|
+
data_node.quote = self.get_attr(element, 'quote', '"')
|
|
59
|
+
data_node.header = self.get_bool_attr(element, 'header', True)
|
|
60
|
+
data_node.encoding = self.get_attr(element, 'encoding', 'utf-8')
|
|
61
|
+
data_node.skip_rows = self.get_int_attr(element, 'skip_rows', 0)
|
|
62
|
+
|
|
63
|
+
# XML attributes
|
|
64
|
+
data_node.xpath = self.get_attr(element, 'xpath')
|
|
65
|
+
data_node.namespace = self.get_attr(element, 'namespace')
|
|
66
|
+
|
|
67
|
+
# Result
|
|
68
|
+
data_node.result = self.get_attr(element, 'result')
|
|
69
|
+
|
|
70
|
+
# Parse child elements
|
|
71
|
+
for child in element:
|
|
72
|
+
child_type = self.get_element_name(child)
|
|
73
|
+
|
|
74
|
+
if child_type == 'column':
|
|
75
|
+
column = self._parse_column(child)
|
|
76
|
+
data_node.add_column(column)
|
|
77
|
+
elif child_type == 'field':
|
|
78
|
+
field = self._parse_field(child)
|
|
79
|
+
data_node.add_field(field)
|
|
80
|
+
elif child_type == 'transform':
|
|
81
|
+
transform = self._parse_transform(child)
|
|
82
|
+
data_node.add_transform(transform)
|
|
83
|
+
elif child_type == 'header':
|
|
84
|
+
header = self._parse_header(child)
|
|
85
|
+
data_node.add_header(header)
|
|
86
|
+
|
|
87
|
+
return data_node
|
|
88
|
+
|
|
89
|
+
def _parse_column(self, element: ET.Element) -> ColumnNode:
|
|
90
|
+
"""Parse q:column for CSV."""
|
|
91
|
+
name = self.get_attr(element, 'name')
|
|
92
|
+
col_type = self.get_attr(element, 'type', 'string')
|
|
93
|
+
|
|
94
|
+
if not name:
|
|
95
|
+
raise ParserError("Column requires 'name' attribute")
|
|
96
|
+
|
|
97
|
+
column = ColumnNode(name, col_type)
|
|
98
|
+
column.required = self.get_bool_attr(element, 'required', False)
|
|
99
|
+
column.default = self.get_attr(element, 'default')
|
|
100
|
+
column.validate_rule = self.get_attr(element, 'validate')
|
|
101
|
+
column.pattern = self.get_attr(element, 'pattern')
|
|
102
|
+
|
|
103
|
+
# Numeric validation
|
|
104
|
+
min_val = self.get_attr(element, 'min')
|
|
105
|
+
if min_val:
|
|
106
|
+
try:
|
|
107
|
+
column.min = float(min_val)
|
|
108
|
+
except ValueError:
|
|
109
|
+
column.min = min_val
|
|
110
|
+
|
|
111
|
+
max_val = self.get_attr(element, 'max')
|
|
112
|
+
if max_val:
|
|
113
|
+
try:
|
|
114
|
+
column.max = float(max_val)
|
|
115
|
+
except ValueError:
|
|
116
|
+
column.max = max_val
|
|
117
|
+
|
|
118
|
+
column.minlength = self.get_int_attr(element, 'minlength', 0) or None
|
|
119
|
+
column.maxlength = self.get_int_attr(element, 'maxlength', 0) or None
|
|
120
|
+
column.range = self.get_attr(element, 'range')
|
|
121
|
+
column.enum = self.get_attr(element, 'enum')
|
|
122
|
+
|
|
123
|
+
return column
|
|
124
|
+
|
|
125
|
+
def _parse_field(self, element: ET.Element) -> FieldNode:
|
|
126
|
+
"""Parse q:field for JSON/XML."""
|
|
127
|
+
name = self.get_attr(element, 'name')
|
|
128
|
+
path = self.get_attr(element, 'path')
|
|
129
|
+
|
|
130
|
+
if not name:
|
|
131
|
+
raise ParserError("Field requires 'name' attribute")
|
|
132
|
+
|
|
133
|
+
return FieldNode(name, path or name)
|
|
134
|
+
|
|
135
|
+
def _parse_transform(self, element: ET.Element) -> TransformNode:
|
|
136
|
+
"""Parse q:transform — a container of operations.
|
|
137
|
+
|
|
138
|
+
This read operation=/field=/value= off the tag and called
|
|
139
|
+
TransformNode(operation, field, value). TransformNode takes NO
|
|
140
|
+
arguments: it holds a list of child operations, which is also what
|
|
141
|
+
the runtime iterates and what all four q:transform blocks in
|
|
142
|
+
examples/ are written as:
|
|
143
|
+
|
|
144
|
+
<q:transform>
|
|
145
|
+
<q:filter condition="{active} == True" />
|
|
146
|
+
</q:transform>
|
|
147
|
+
|
|
148
|
+
So the parser rejected the only form anybody writes — "Transform
|
|
149
|
+
requires 'operation' attribute" — and would have raised TypeError if
|
|
150
|
+
it had ever got past that. Four example tests failed here.
|
|
151
|
+
"""
|
|
152
|
+
transform_node = TransformNode()
|
|
153
|
+
|
|
154
|
+
for child in element:
|
|
155
|
+
tag = self._local_name(child.tag)
|
|
156
|
+
|
|
157
|
+
if tag == 'filter':
|
|
158
|
+
condition = self.get_attr(child, 'condition')
|
|
159
|
+
if not condition:
|
|
160
|
+
raise ParserError("Filter requires 'condition' attribute")
|
|
161
|
+
transform_node.add_operation(FilterNode(condition))
|
|
162
|
+
|
|
163
|
+
elif tag == 'sort':
|
|
164
|
+
by = self.get_attr(child, 'by') or self.get_attr(child, 'field')
|
|
165
|
+
if not by:
|
|
166
|
+
raise ParserError("Sort requires 'by' attribute")
|
|
167
|
+
order = (self.get_attr(child, 'order') or 'asc').lower()
|
|
168
|
+
if order not in ('asc', 'desc'):
|
|
169
|
+
raise ParserError(
|
|
170
|
+
f"Invalid sort order: {order}. Must be 'asc' or 'desc'")
|
|
171
|
+
transform_node.add_operation(SortNode(by, order))
|
|
172
|
+
|
|
173
|
+
elif tag == 'limit':
|
|
174
|
+
raw = self.get_attr(child, 'value') or self.get_attr(child, 'count')
|
|
175
|
+
if raw is None or raw == '':
|
|
176
|
+
raise ParserError("Limit requires 'value' attribute")
|
|
177
|
+
try:
|
|
178
|
+
limit = int(str(raw).strip())
|
|
179
|
+
except ValueError:
|
|
180
|
+
raise ParserError(f"Limit value must be a number, got {raw!r}")
|
|
181
|
+
transform_node.add_operation(LimitNode(limit))
|
|
182
|
+
|
|
183
|
+
elif tag == 'compute':
|
|
184
|
+
field = self.get_attr(child, 'field') or self.get_attr(child, 'name')
|
|
185
|
+
expression = self.get_attr(child, 'expression')
|
|
186
|
+
if not field:
|
|
187
|
+
raise ParserError("Compute requires 'field' attribute")
|
|
188
|
+
if not expression:
|
|
189
|
+
raise ParserError("Compute requires 'expression' attribute")
|
|
190
|
+
transform_node.add_operation(ComputeNode(
|
|
191
|
+
field, expression, self.get_attr(child, 'type') or 'string'))
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
raise ParserError(
|
|
195
|
+
f"Unknown transform operation 'q:{tag}'. Supported: "
|
|
196
|
+
f"filter, sort, limit, compute")
|
|
197
|
+
|
|
198
|
+
return transform_node
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _local_name(tag: str) -> str:
|
|
202
|
+
"""'{ns}filter' or 'q:filter' -> 'filter'."""
|
|
203
|
+
if '}' in tag:
|
|
204
|
+
tag = tag.split('}', 1)[1]
|
|
205
|
+
if ':' in tag:
|
|
206
|
+
tag = tag.split(':', 1)[1]
|
|
207
|
+
return tag
|
|
208
|
+
|
|
209
|
+
def _parse_header(self, element: ET.Element) -> HeaderNode:
|
|
210
|
+
"""Parse q:header for HTTP headers."""
|
|
211
|
+
name = self.get_attr(element, 'name')
|
|
212
|
+
value = self.get_attr(element, 'value')
|
|
213
|
+
|
|
214
|
+
if not name:
|
|
215
|
+
raise ParserError("Header requires 'name' attribute")
|
|
216
|
+
|
|
217
|
+
return HeaderNode(name, value or '')
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Invoke Parser - Parse q:invoke statements
|
|
3
|
+
|
|
4
|
+
Handles function, component, and HTTP invocations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
from xml.etree import ElementTree as ET
|
|
9
|
+
from quantum.core.parsers.base import BaseTagParser, ParserError
|
|
10
|
+
from quantum.core.features.invocation.src.ast_node import InvokeNode, InvokeHeaderNode
|
|
11
|
+
from quantum.core.ast_nodes import QuantumParam
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvokeParser(BaseTagParser):
|
|
15
|
+
"""
|
|
16
|
+
Parser for q:invoke statements.
|
|
17
|
+
|
|
18
|
+
Supports:
|
|
19
|
+
- Function invocation
|
|
20
|
+
- Component invocation
|
|
21
|
+
- HTTP requests (REST APIs)
|
|
22
|
+
- Service invocation
|
|
23
|
+
- Authentication (bearer, basic, custom)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def tag_names(self) -> List[str]:
|
|
28
|
+
return ['invoke']
|
|
29
|
+
|
|
30
|
+
def parse(self, element: ET.Element) -> InvokeNode:
|
|
31
|
+
"""
|
|
32
|
+
Parse q:invoke statement.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
element: XML element for q:invoke
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
InvokeNode AST node
|
|
39
|
+
"""
|
|
40
|
+
name = self.get_attr(element, 'name')
|
|
41
|
+
# Fire-and-forget invocation (no result to bind) is legitimate — the
|
|
42
|
+
# job/thread examples all use it — so name is optional.
|
|
43
|
+
invoke_node = InvokeNode(name or '')
|
|
44
|
+
|
|
45
|
+
# Parse invocation target
|
|
46
|
+
invoke_node.function = self.get_attr(element, 'function')
|
|
47
|
+
invoke_node.component = self.get_attr(element, 'component')
|
|
48
|
+
invoke_node.url = self.get_attr(element, 'url')
|
|
49
|
+
invoke_node.endpoint = self.get_attr(element, 'endpoint')
|
|
50
|
+
invoke_node.service = self.get_attr(element, 'service')
|
|
51
|
+
|
|
52
|
+
# HTTP attributes
|
|
53
|
+
invoke_node.method = self.get_attr(element, 'method', 'GET').upper()
|
|
54
|
+
invoke_node.content_type = self.get_attr(element, 'contentType', 'application/json')
|
|
55
|
+
|
|
56
|
+
# Authentication
|
|
57
|
+
invoke_node.auth_type = self.get_attr(element, 'authType')
|
|
58
|
+
invoke_node.auth_token = self.get_attr(element, 'authToken')
|
|
59
|
+
invoke_node.auth_header = self.get_attr(element, 'authHeader')
|
|
60
|
+
invoke_node.auth_username = self.get_attr(element, 'authUsername')
|
|
61
|
+
invoke_node.auth_password = self.get_attr(element, 'authPassword')
|
|
62
|
+
|
|
63
|
+
# Timeouts and retries
|
|
64
|
+
invoke_node.timeout = self.get_int_attr(element, 'timeout', 0) or None
|
|
65
|
+
invoke_node.retry = self.get_int_attr(element, 'retry', 0) or None
|
|
66
|
+
invoke_node.retry_delay = self.get_int_attr(element, 'retryDelay', 0) or None
|
|
67
|
+
|
|
68
|
+
# Response handling
|
|
69
|
+
invoke_node.response_format = self.get_attr(element, 'responseFormat', 'auto')
|
|
70
|
+
invoke_node.transform = self.get_attr(element, 'transform')
|
|
71
|
+
invoke_node.cache = self.get_bool_attr(element, 'cache', False)
|
|
72
|
+
invoke_node.ttl = self.get_int_attr(element, 'ttl', 0) or None
|
|
73
|
+
invoke_node.result = self.get_attr(element, 'result')
|
|
74
|
+
|
|
75
|
+
# Parse child elements
|
|
76
|
+
for child in element:
|
|
77
|
+
child_type = self.get_element_name(child)
|
|
78
|
+
|
|
79
|
+
if child_type == 'header':
|
|
80
|
+
header = self._parse_header(child)
|
|
81
|
+
invoke_node.add_header(header)
|
|
82
|
+
elif child_type == 'param':
|
|
83
|
+
param = self._parse_param(child)
|
|
84
|
+
invoke_node.add_param(param)
|
|
85
|
+
elif child_type == 'body':
|
|
86
|
+
invoke_node.body = child.text or ""
|
|
87
|
+
|
|
88
|
+
return invoke_node
|
|
89
|
+
|
|
90
|
+
def _parse_header(self, element: ET.Element) -> InvokeHeaderNode:
|
|
91
|
+
"""Parse q:header within q:invoke."""
|
|
92
|
+
name = self.get_attr(element, 'name')
|
|
93
|
+
value = self.get_attr(element, 'value')
|
|
94
|
+
|
|
95
|
+
if not name:
|
|
96
|
+
raise ParserError("Invoke header requires 'name' attribute")
|
|
97
|
+
if value is None:
|
|
98
|
+
raise ParserError(f"Invoke header '{name}' requires 'value' attribute")
|
|
99
|
+
|
|
100
|
+
return InvokeHeaderNode(name, value)
|
|
101
|
+
|
|
102
|
+
def _parse_param(self, element: ET.Element) -> QuantumParam:
|
|
103
|
+
"""Parse q:param within q:invoke."""
|
|
104
|
+
name = self.get_attr(element, 'name')
|
|
105
|
+
value = self.get_attr(element, 'value')
|
|
106
|
+
param_type = self.get_attr(element, 'type', 'string')
|
|
107
|
+
|
|
108
|
+
if not name:
|
|
109
|
+
raise ParserError("Param requires 'name' attribute")
|
|
110
|
+
|
|
111
|
+
param = QuantumParam(name, param_type)
|
|
112
|
+
param.value = value
|
|
113
|
+
param.required = self.get_bool_attr(element, 'required', False)
|
|
114
|
+
param.default = self.get_attr(element, 'default')
|
|
115
|
+
|
|
116
|
+
return param
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query Parser - Parse q:query statements
|
|
3
|
+
|
|
4
|
+
Handles SQL queries with parameter binding and RAG mode.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import List
|
|
9
|
+
from xml.etree import ElementTree as ET
|
|
10
|
+
from quantum.core.parsers.base import BaseTagParser, ParserError
|
|
11
|
+
from quantum.core.ast_nodes import QueryNode, QueryParamNode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QueryParser(BaseTagParser):
|
|
15
|
+
"""
|
|
16
|
+
Parser for q:query statements.
|
|
17
|
+
|
|
18
|
+
Supports:
|
|
19
|
+
- SQL queries with parameter binding
|
|
20
|
+
- Query of Queries (source attribute)
|
|
21
|
+
- Caching and pagination
|
|
22
|
+
- RAG/knowledge base queries
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def tag_names(self) -> List[str]:
|
|
27
|
+
return ['query']
|
|
28
|
+
|
|
29
|
+
def parse(self, element: ET.Element) -> QueryNode:
|
|
30
|
+
"""
|
|
31
|
+
Parse q:query statement.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
element: XML element for q:query
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
QueryNode AST node
|
|
38
|
+
"""
|
|
39
|
+
name = self.get_attr(element, 'name')
|
|
40
|
+
datasource = self.get_attr(element, 'datasource')
|
|
41
|
+
source = self.get_attr(element, 'source')
|
|
42
|
+
|
|
43
|
+
# A write query (UPDATE/INSERT/DELETE) has no result set to bind, so
|
|
44
|
+
# name is optional — requiring it rejected the ordinary CFML-style
|
|
45
|
+
# idiom used across the job examples. Reads still want a name; without
|
|
46
|
+
# one there is simply nothing to reference the rows by.
|
|
47
|
+
if not name:
|
|
48
|
+
name = ''
|
|
49
|
+
if not datasource and not source:
|
|
50
|
+
raise ParserError("Query requires either 'datasource' or 'source' attribute")
|
|
51
|
+
|
|
52
|
+
# Extract SQL content
|
|
53
|
+
sql_parts = []
|
|
54
|
+
if element.text:
|
|
55
|
+
sql_parts.append(element.text.strip())
|
|
56
|
+
|
|
57
|
+
for child in element:
|
|
58
|
+
if child.tail:
|
|
59
|
+
sql_parts.append(child.tail.strip())
|
|
60
|
+
|
|
61
|
+
sql = '\n'.join(part for part in sql_parts if part)
|
|
62
|
+
|
|
63
|
+
# Create query node
|
|
64
|
+
query_node = QueryNode(name, datasource or '', sql)
|
|
65
|
+
|
|
66
|
+
# Parse attributes
|
|
67
|
+
query_node.source = source
|
|
68
|
+
query_node.cache = self.get_bool_attr(element, 'cache', False)
|
|
69
|
+
query_node.ttl = self.get_int_attr(element, 'ttl', 0) or None
|
|
70
|
+
query_node.reactive = self.get_bool_attr(element, 'reactive', False)
|
|
71
|
+
query_node.interval = self.get_int_attr(element, 'interval', 0) or None
|
|
72
|
+
query_node.paginate = self.get_bool_attr(element, 'paginate', False)
|
|
73
|
+
query_node.page = self.get_int_attr(element, 'page', 0) or None
|
|
74
|
+
# Accept both spellings: the codebase convention is camelCase, but
|
|
75
|
+
# snake_case reads naturally in XML and the examples used it —
|
|
76
|
+
# the mismatch made page_size None and crashed pagination with
|
|
77
|
+
# 'unsupported operand type(s) for +: int and NoneType'.
|
|
78
|
+
query_node.page_size = (
|
|
79
|
+
self.get_int_attr(element, 'pageSize', 0)
|
|
80
|
+
or self.get_int_attr(element, 'page_size', 0)
|
|
81
|
+
or None
|
|
82
|
+
)
|
|
83
|
+
query_node.timeout = self.get_int_attr(element, 'timeout', 0) or None
|
|
84
|
+
query_node.maxrows = self.get_int_attr(element, 'maxrows', 0) or None
|
|
85
|
+
query_node.result = self.get_attr(element, 'result')
|
|
86
|
+
|
|
87
|
+
# RAG/Knowledge attributes
|
|
88
|
+
query_node.mode = self.get_attr(element, 'mode')
|
|
89
|
+
query_node.rag_model = self.get_attr(element, 'model')
|
|
90
|
+
|
|
91
|
+
# Parse q:param children
|
|
92
|
+
for child in element:
|
|
93
|
+
child_type = self.get_element_name(child)
|
|
94
|
+
if child_type == 'param':
|
|
95
|
+
param_node = self._parse_query_param(child)
|
|
96
|
+
query_node.add_param(param_node)
|
|
97
|
+
|
|
98
|
+
# Validate: reject direct {var} interpolation (SQL injection risk)
|
|
99
|
+
if sql and re.search(r'\{[a-zA-Z_]\w*\}', sql):
|
|
100
|
+
raise ParserError(
|
|
101
|
+
f"Query '{name}': direct {{var}} interpolation in SQL is not allowed. "
|
|
102
|
+
"Use :param_name with <q:param> for safe parameter binding"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Validate: all :param references must have matching <q:param>
|
|
106
|
+
if sql:
|
|
107
|
+
sql_params = self._named_params(sql)
|
|
108
|
+
declared_params = {p.name for p in query_node.params}
|
|
109
|
+
missing = sql_params - declared_params
|
|
110
|
+
if missing:
|
|
111
|
+
raise ParserError(
|
|
112
|
+
f"Query '{name}': SQL parameter(s) {', '.join(':' + p for p in sorted(missing))} "
|
|
113
|
+
"declared in SQL but no matching <q:param> element provided"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return query_node
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _named_params(sql: str) -> set:
|
|
120
|
+
"""Os `:nome` que sao REALMENTE parametros de bind.
|
|
121
|
+
|
|
122
|
+
Isto era `re.findall(r':([a-zA-Z_]\\w*)', sql)` sobre o SQL cru, e
|
|
123
|
+
varria tambem o que esta dentro de aspas e de comentarios:
|
|
124
|
+
|
|
125
|
+
TO_CHAR(c.created_at, 'YYYY-MM-DD HH24:MI') -> achava `:MI`
|
|
126
|
+
SELECT x::text FROM t -> achava `:text`
|
|
127
|
+
|
|
128
|
+
As duas formas sao SQL correto e comum — a primeira aparece em
|
|
129
|
+
projects/blog/components/post.q, que por causa disto nao parseava:
|
|
130
|
+
o arquivo era recusado com "SQL parameter(s) :MI declared in SQL but
|
|
131
|
+
no matching <q:param>", e nao havia parametro nenhum para declarar.
|
|
132
|
+
|
|
133
|
+
A varredura pula literais de texto (com '' e "" escapados), o cast
|
|
134
|
+
`::` do PostgreSQL, comentarios de linha `--` e de bloco.
|
|
135
|
+
"""
|
|
136
|
+
encontrados = set()
|
|
137
|
+
i, n = 0, len(sql)
|
|
138
|
+
while i < n:
|
|
139
|
+
c = sql[i]
|
|
140
|
+
if c in ("'", '"'):
|
|
141
|
+
aspas = c
|
|
142
|
+
i += 1
|
|
143
|
+
while i < n:
|
|
144
|
+
if sql[i] == aspas:
|
|
145
|
+
if i + 1 < n and sql[i + 1] == aspas:
|
|
146
|
+
i += 2 # '' escapado dentro do literal
|
|
147
|
+
continue
|
|
148
|
+
i += 1
|
|
149
|
+
break
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
if sql.startswith('--', i):
|
|
153
|
+
fim = sql.find('\n', i)
|
|
154
|
+
i = n if fim == -1 else fim + 1
|
|
155
|
+
continue
|
|
156
|
+
if sql.startswith('/*', i):
|
|
157
|
+
fim = sql.find('*/', i)
|
|
158
|
+
i = n if fim == -1 else fim + 2
|
|
159
|
+
continue
|
|
160
|
+
if c == ':':
|
|
161
|
+
if sql.startswith('::', i):
|
|
162
|
+
i += 2 # cast do PostgreSQL, nao parametro
|
|
163
|
+
continue
|
|
164
|
+
m = re.match(r':([a-zA-Z_]\w*)', sql[i:])
|
|
165
|
+
if m:
|
|
166
|
+
encontrados.add(m.group(1))
|
|
167
|
+
i += m.end()
|
|
168
|
+
continue
|
|
169
|
+
i += 1
|
|
170
|
+
return encontrados
|
|
171
|
+
|
|
172
|
+
def _parse_query_param(self, element: ET.Element) -> QueryParamNode:
|
|
173
|
+
"""Parse q:param within q:query."""
|
|
174
|
+
name = self.get_attr(element, 'name')
|
|
175
|
+
value = self.get_attr(element, 'value')
|
|
176
|
+
param_type = self.get_attr(element, 'type', 'string')
|
|
177
|
+
|
|
178
|
+
if not name:
|
|
179
|
+
raise ParserError("Query parameter requires 'name' attribute")
|
|
180
|
+
if value is None:
|
|
181
|
+
raise ParserError(f"Query parameter '{name}' requires 'value' attribute")
|
|
182
|
+
|
|
183
|
+
param_node = QueryParamNode(name, value, param_type)
|
|
184
|
+
param_node.null = self.get_bool_attr(element, 'null', False)
|
|
185
|
+
param_node.max_length = self.get_int_attr(element, 'maxLength', 0) or None
|
|
186
|
+
param_node.scale = self.get_int_attr(element, 'scale', 0) or None
|
|
187
|
+
|
|
188
|
+
return param_node
|