quantum-framework 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
if element.get('mask') is not None:
|
|
57
|
+
# A2: accepted and never applied.
|
|
58
|
+
from quantum.core.parser import QuantumParseError
|
|
59
|
+
raise QuantumParseError(
|
|
60
|
+
f'<q:set name="{set_node.name}"> mask= is not supported: it was accepted and '
|
|
61
|
+
f'never did anything. To validate a format use pattern=.')
|
|
62
|
+
set_node.range = self.get_attr(element, 'range')
|
|
63
|
+
set_node.enum = self.get_attr(element, 'enum')
|
|
64
|
+
set_node.unique = self.get_attr(element, 'unique')
|
|
65
|
+
set_node.min = self.get_attr(element, 'min')
|
|
66
|
+
set_node.max = self.get_attr(element, 'max')
|
|
67
|
+
set_node.minlength = self.get_attr(element, 'minlength')
|
|
68
|
+
set_node.maxlength = self.get_attr(element, 'maxlength')
|
|
69
|
+
|
|
70
|
+
# Behavior
|
|
71
|
+
set_node.scope = self.get_attr(element, 'scope', 'local')
|
|
72
|
+
set_node.operation = self.get_attr(element, 'operation', 'assign')
|
|
73
|
+
|
|
74
|
+
# Step for increment/decrement
|
|
75
|
+
set_node.step = self.get_int_attr(element, 'step', 1)
|
|
76
|
+
|
|
77
|
+
# Collection operations
|
|
78
|
+
set_node.index = self.get_attr(element, 'index')
|
|
79
|
+
set_node.key = self.get_attr(element, 'key')
|
|
80
|
+
set_node.source = self.get_attr(element, 'source')
|
|
81
|
+
|
|
82
|
+
# SET-2: browser persistence never ran in a page.
|
|
83
|
+
persist = [a for a in ('persist', 'persistKey', 'persistTtl', 'persistEncrypt')
|
|
84
|
+
if element.get(a) is not None]
|
|
85
|
+
if persist:
|
|
86
|
+
from quantum.core.parser import QuantumParseError
|
|
87
|
+
raise QuantumParseError(
|
|
88
|
+
f'<q:set name="{set_node.name}"> {persist[0]}= was removed in Quantum 0.16: it '
|
|
89
|
+
f'kept the variable in the browser, and a page\'s variables live on the server, '
|
|
90
|
+
f'for one request. Keep what must last in session.{set_node.name} (per user) '
|
|
91
|
+
f'or in the database.')
|
|
92
|
+
|
|
93
|
+
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,229 @@
|
|
|
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
|
+
# DATA-4 / INV-2: a failure stops the component unless the program says
|
|
71
|
+
# it will look at <name>_result.
|
|
72
|
+
data_node.on_error = self.get_attr(element, 'onerror', 'fail')
|
|
73
|
+
if data_node.on_error not in ('fail', 'continue'):
|
|
74
|
+
from quantum.core.parser import QuantumParseError
|
|
75
|
+
raise QuantumParseError(
|
|
76
|
+
f"<q:data name=\"{data_node.name}\"> onerror must be \"fail\" or "
|
|
77
|
+
f"\"continue\", not \"{data_node.on_error}\"")
|
|
78
|
+
|
|
79
|
+
# Parse child elements
|
|
80
|
+
for child in element:
|
|
81
|
+
child_type = self.get_element_name(child)
|
|
82
|
+
|
|
83
|
+
if child_type == 'column':
|
|
84
|
+
column = self._parse_column(child)
|
|
85
|
+
data_node.add_column(column)
|
|
86
|
+
elif child_type == 'field':
|
|
87
|
+
field = self._parse_field(child)
|
|
88
|
+
data_node.add_field(field)
|
|
89
|
+
elif child_type == 'transform':
|
|
90
|
+
transform = self._parse_transform(child)
|
|
91
|
+
data_node.add_transform(transform)
|
|
92
|
+
elif child_type == 'header':
|
|
93
|
+
header = self._parse_header(child)
|
|
94
|
+
data_node.add_header(header)
|
|
95
|
+
|
|
96
|
+
return data_node
|
|
97
|
+
|
|
98
|
+
def _parse_column(self, element: ET.Element) -> ColumnNode:
|
|
99
|
+
"""Parse q:column for CSV."""
|
|
100
|
+
name = self.get_attr(element, 'name')
|
|
101
|
+
col_type = self.get_attr(element, 'type', 'string')
|
|
102
|
+
|
|
103
|
+
if not name:
|
|
104
|
+
raise ParserError("Column requires 'name' attribute")
|
|
105
|
+
|
|
106
|
+
column = ColumnNode(name, col_type)
|
|
107
|
+
column.required = self.get_bool_attr(element, 'required', False)
|
|
108
|
+
column.default = self.get_attr(element, 'default')
|
|
109
|
+
column.validate_rule = self.get_attr(element, 'validate')
|
|
110
|
+
column.pattern = self.get_attr(element, 'pattern')
|
|
111
|
+
|
|
112
|
+
# Numeric validation
|
|
113
|
+
min_val = self.get_attr(element, 'min')
|
|
114
|
+
if min_val:
|
|
115
|
+
try:
|
|
116
|
+
column.min = float(min_val)
|
|
117
|
+
except ValueError:
|
|
118
|
+
column.min = min_val
|
|
119
|
+
|
|
120
|
+
max_val = self.get_attr(element, 'max')
|
|
121
|
+
if max_val:
|
|
122
|
+
try:
|
|
123
|
+
column.max = float(max_val)
|
|
124
|
+
except ValueError:
|
|
125
|
+
column.max = max_val
|
|
126
|
+
|
|
127
|
+
column.minlength = self.get_int_attr(element, 'minlength', 0) or None
|
|
128
|
+
column.maxlength = self.get_int_attr(element, 'maxlength', 0) or None
|
|
129
|
+
column.range = self.get_attr(element, 'range')
|
|
130
|
+
column.enum = self.get_attr(element, 'enum')
|
|
131
|
+
|
|
132
|
+
return column
|
|
133
|
+
|
|
134
|
+
def _parse_field(self, element: ET.Element) -> FieldNode:
|
|
135
|
+
"""Parse q:field for JSON/XML."""
|
|
136
|
+
name = self.get_attr(element, 'name')
|
|
137
|
+
# `xpath` is the attribute every example and the reference use; only
|
|
138
|
+
# `path` was read, so `xpath="@id"` became the name "id" and `type` was
|
|
139
|
+
# dropped — attributes came back None and numbers came back as text.
|
|
140
|
+
path = self.get_attr(element, 'xpath') or self.get_attr(element, 'path')
|
|
141
|
+
|
|
142
|
+
if not name:
|
|
143
|
+
raise ParserError("Field requires 'name' attribute")
|
|
144
|
+
|
|
145
|
+
return FieldNode(name, path or name, self.get_attr(element, 'type') or 'string')
|
|
146
|
+
|
|
147
|
+
def _parse_transform(self, element: ET.Element) -> TransformNode:
|
|
148
|
+
"""Parse q:transform — a container of operations.
|
|
149
|
+
|
|
150
|
+
This read operation=/field=/value= off the tag and called
|
|
151
|
+
TransformNode(operation, field, value). TransformNode takes NO
|
|
152
|
+
arguments: it holds a list of child operations, which is also what
|
|
153
|
+
the runtime iterates and what all four q:transform blocks in
|
|
154
|
+
examples/ are written as:
|
|
155
|
+
|
|
156
|
+
<q:transform>
|
|
157
|
+
<q:filter condition="{active} == True" />
|
|
158
|
+
</q:transform>
|
|
159
|
+
|
|
160
|
+
So the parser rejected the only form anybody writes — "Transform
|
|
161
|
+
requires 'operation' attribute" — and would have raised TypeError if
|
|
162
|
+
it had ever got past that. Four example tests failed here.
|
|
163
|
+
"""
|
|
164
|
+
transform_node = TransformNode()
|
|
165
|
+
|
|
166
|
+
for child in element:
|
|
167
|
+
tag = self._local_name(child.tag)
|
|
168
|
+
|
|
169
|
+
if tag == 'filter':
|
|
170
|
+
condition = self.get_attr(child, 'condition')
|
|
171
|
+
if not condition:
|
|
172
|
+
raise ParserError("Filter requires 'condition' attribute")
|
|
173
|
+
transform_node.add_operation(FilterNode(condition))
|
|
174
|
+
|
|
175
|
+
elif tag == 'sort':
|
|
176
|
+
by = self.get_attr(child, 'by') or self.get_attr(child, 'field')
|
|
177
|
+
if not by:
|
|
178
|
+
raise ParserError("Sort requires 'by' attribute")
|
|
179
|
+
order = (self.get_attr(child, 'order') or 'asc').lower()
|
|
180
|
+
if order not in ('asc', 'desc'):
|
|
181
|
+
raise ParserError(
|
|
182
|
+
f"Invalid sort order: {order}. Must be 'asc' or 'desc'")
|
|
183
|
+
transform_node.add_operation(SortNode(by, order))
|
|
184
|
+
|
|
185
|
+
elif tag == 'limit':
|
|
186
|
+
raw = self.get_attr(child, 'value') or self.get_attr(child, 'count')
|
|
187
|
+
if raw is None or raw == '':
|
|
188
|
+
raise ParserError("Limit requires 'value' attribute")
|
|
189
|
+
try:
|
|
190
|
+
limit = int(str(raw).strip())
|
|
191
|
+
except ValueError:
|
|
192
|
+
raise ParserError(f"Limit value must be a number, got {raw!r}")
|
|
193
|
+
transform_node.add_operation(LimitNode(limit))
|
|
194
|
+
|
|
195
|
+
elif tag == 'compute':
|
|
196
|
+
field = self.get_attr(child, 'field') or self.get_attr(child, 'name')
|
|
197
|
+
expression = self.get_attr(child, 'expression')
|
|
198
|
+
if not field:
|
|
199
|
+
raise ParserError("Compute requires 'field' attribute")
|
|
200
|
+
if not expression:
|
|
201
|
+
raise ParserError("Compute requires 'expression' attribute")
|
|
202
|
+
transform_node.add_operation(ComputeNode(
|
|
203
|
+
field, expression, self.get_attr(child, 'type') or 'string'))
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
raise ParserError(
|
|
207
|
+
f"Unknown transform operation 'q:{tag}'. Supported: "
|
|
208
|
+
f"filter, sort, limit, compute")
|
|
209
|
+
|
|
210
|
+
return transform_node
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _local_name(tag: str) -> str:
|
|
214
|
+
"""'{ns}filter' or 'q:filter' -> 'filter'."""
|
|
215
|
+
if '}' in tag:
|
|
216
|
+
tag = tag.split('}', 1)[1]
|
|
217
|
+
if ':' in tag:
|
|
218
|
+
tag = tag.split(':', 1)[1]
|
|
219
|
+
return tag
|
|
220
|
+
|
|
221
|
+
def _parse_header(self, element: ET.Element) -> HeaderNode:
|
|
222
|
+
"""Parse q:header for HTTP headers."""
|
|
223
|
+
name = self.get_attr(element, 'name')
|
|
224
|
+
value = self.get_attr(element, 'value')
|
|
225
|
+
|
|
226
|
+
if not name:
|
|
227
|
+
raise ParserError("Header requires 'name' attribute")
|
|
228
|
+
|
|
229
|
+
return HeaderNode(name, value or '')
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
if self.get_attr(element, 'endpoint') is not None:
|
|
50
|
+
raise ParserError(
|
|
51
|
+
f'<q:invoke name="{name}"> endpoint= is not supported: it was accepted and never '
|
|
52
|
+
f'did anything, and was removed in Quantum 0.12. Use url= or service=.')
|
|
53
|
+
invoke_node.service = self.get_attr(element, 'service')
|
|
54
|
+
|
|
55
|
+
# HTTP attributes
|
|
56
|
+
invoke_node.method = self.get_attr(element, 'method', 'GET').upper()
|
|
57
|
+
invoke_node.content_type = self.get_attr(element, 'contentType', 'application/json')
|
|
58
|
+
|
|
59
|
+
# Authentication
|
|
60
|
+
invoke_node.auth_type = self.get_attr(element, 'authType')
|
|
61
|
+
invoke_node.auth_token = self.get_attr(element, 'authToken')
|
|
62
|
+
invoke_node.auth_header = self.get_attr(element, 'authHeader')
|
|
63
|
+
invoke_node.auth_username = self.get_attr(element, 'authUsername')
|
|
64
|
+
invoke_node.auth_password = self.get_attr(element, 'authPassword')
|
|
65
|
+
|
|
66
|
+
# Timeouts and retries
|
|
67
|
+
invoke_node.timeout = self.get_int_attr(element, 'timeout', 0) or None
|
|
68
|
+
invoke_node.retry = self.get_int_attr(element, 'retry', 0) or None
|
|
69
|
+
invoke_node.retry_delay = self.get_int_attr(element, 'retryDelay', 0) or None
|
|
70
|
+
|
|
71
|
+
# Response handling
|
|
72
|
+
invoke_node.response_format = self.get_attr(element, 'responseFormat', 'auto')
|
|
73
|
+
if element.get('transform') is not None:
|
|
74
|
+
# A2: accepted and never applied.
|
|
75
|
+
from quantum.core.parser import QuantumParseError
|
|
76
|
+
raise QuantumParseError(
|
|
77
|
+
f'<q:invoke name="{invoke_node.name}"> transform= is not supported: it was '
|
|
78
|
+
f'accepted and never did anything. Transform the result with q:set.')
|
|
79
|
+
invoke_node.cache = self.get_bool_attr(element, 'cache', False)
|
|
80
|
+
invoke_node.ttl = self.get_int_attr(element, 'ttl', 0) or None
|
|
81
|
+
invoke_node.result = self.get_attr(element, 'result')
|
|
82
|
+
|
|
83
|
+
# DATA-4 / INV-2: a failure stops the component unless the program says
|
|
84
|
+
# it will look at <name>_result.
|
|
85
|
+
invoke_node.on_error = self.get_attr(element, 'onerror', 'fail')
|
|
86
|
+
if invoke_node.on_error not in ('fail', 'continue'):
|
|
87
|
+
from quantum.core.parser import QuantumParseError
|
|
88
|
+
raise QuantumParseError(
|
|
89
|
+
f"<q:invoke name=\"{invoke_node.name}\"> onerror must be \"fail\" or "
|
|
90
|
+
f"\"continue\", not \"{invoke_node.on_error}\"")
|
|
91
|
+
|
|
92
|
+
# Parse child elements
|
|
93
|
+
for child in element:
|
|
94
|
+
child_type = self.get_element_name(child)
|
|
95
|
+
|
|
96
|
+
if child_type == 'header':
|
|
97
|
+
header = self._parse_header(child)
|
|
98
|
+
invoke_node.add_header(header)
|
|
99
|
+
elif child_type == 'param':
|
|
100
|
+
param = self._parse_param(child)
|
|
101
|
+
invoke_node.add_param(param)
|
|
102
|
+
elif child_type == 'body':
|
|
103
|
+
invoke_node.body = child.text or ""
|
|
104
|
+
|
|
105
|
+
return invoke_node
|
|
106
|
+
|
|
107
|
+
def _parse_header(self, element: ET.Element) -> InvokeHeaderNode:
|
|
108
|
+
"""Parse q:header within q:invoke."""
|
|
109
|
+
name = self.get_attr(element, 'name')
|
|
110
|
+
value = self.get_attr(element, 'value')
|
|
111
|
+
|
|
112
|
+
if not name:
|
|
113
|
+
raise ParserError("Invoke header requires 'name' attribute")
|
|
114
|
+
if value is None:
|
|
115
|
+
raise ParserError(f"Invoke header '{name}' requires 'value' attribute")
|
|
116
|
+
|
|
117
|
+
return InvokeHeaderNode(name, value)
|
|
118
|
+
|
|
119
|
+
def _parse_param(self, element: ET.Element) -> QuantumParam:
|
|
120
|
+
"""Parse q:param within q:invoke."""
|
|
121
|
+
name = self.get_attr(element, 'name')
|
|
122
|
+
value = self.get_attr(element, 'value')
|
|
123
|
+
param_type = self.get_attr(element, 'type', 'string')
|
|
124
|
+
|
|
125
|
+
if not name:
|
|
126
|
+
raise ParserError("Param requires 'name' attribute")
|
|
127
|
+
|
|
128
|
+
param = QuantumParam(name, param_type)
|
|
129
|
+
param.value = value
|
|
130
|
+
param.required = self.get_bool_attr(element, 'required', False)
|
|
131
|
+
param.default = self.get_attr(element, 'default')
|
|
132
|
+
|
|
133
|
+
return param
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
_NEVER_IMPLEMENTED = ('cache', 'ttl', 'reactive', 'interval', 'timeout', 'maxrows', 'batch')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QueryParser(BaseTagParser):
|
|
18
|
+
"""
|
|
19
|
+
Parser for q:query statements.
|
|
20
|
+
|
|
21
|
+
Supports:
|
|
22
|
+
- SQL queries with parameter binding
|
|
23
|
+
- Query of Queries (source attribute)
|
|
24
|
+
- Caching and pagination
|
|
25
|
+
- RAG/knowledge base queries
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def tag_names(self) -> List[str]:
|
|
30
|
+
return ['query']
|
|
31
|
+
|
|
32
|
+
def parse(self, element: ET.Element) -> QueryNode:
|
|
33
|
+
"""
|
|
34
|
+
Parse q:query statement.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
element: XML element for q:query
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
QueryNode AST node
|
|
41
|
+
"""
|
|
42
|
+
name = self.get_attr(element, 'name')
|
|
43
|
+
datasource = self.get_attr(element, 'datasource')
|
|
44
|
+
source = self.get_attr(element, 'source')
|
|
45
|
+
|
|
46
|
+
# A write query (UPDATE/INSERT/DELETE) has no result set to bind, so
|
|
47
|
+
# name is optional — requiring it rejected the ordinary CFML-style
|
|
48
|
+
# idiom used across the job examples. Reads still want a name; without
|
|
49
|
+
# one there is simply nothing to reference the rows by.
|
|
50
|
+
if not name:
|
|
51
|
+
name = ''
|
|
52
|
+
if not datasource and not source:
|
|
53
|
+
raise ParserError("Query requires either 'datasource' or 'source' attribute")
|
|
54
|
+
|
|
55
|
+
# DB-5: attributes that were read into the AST and never used. The
|
|
56
|
+
# guide sold cache="true" ttl="3600" and reactive="true" as features.
|
|
57
|
+
for attr in _NEVER_IMPLEMENTED:
|
|
58
|
+
if element.get(attr) is not None:
|
|
59
|
+
raise ParserError(
|
|
60
|
+
f'<q:query name="{name}"> {attr}= is not supported: it was accepted '
|
|
61
|
+
f'and never did anything, and was removed in Quantum 0.11')
|
|
62
|
+
|
|
63
|
+
# Extract SQL content
|
|
64
|
+
sql_parts = []
|
|
65
|
+
if element.text:
|
|
66
|
+
sql_parts.append(element.text.strip())
|
|
67
|
+
|
|
68
|
+
for child in element:
|
|
69
|
+
if child.tail:
|
|
70
|
+
sql_parts.append(child.tail.strip())
|
|
71
|
+
|
|
72
|
+
sql = '\n'.join(part for part in sql_parts if part)
|
|
73
|
+
|
|
74
|
+
# Create query node
|
|
75
|
+
query_node = QueryNode(name, datasource or '', sql)
|
|
76
|
+
|
|
77
|
+
# Parse attributes
|
|
78
|
+
query_node.source = source
|
|
79
|
+
query_node.paginate = self.get_bool_attr(element, 'paginate', False)
|
|
80
|
+
# UI-13: ordered by the URL's ?sort=/&dir= (a <ui:table sort="true"> writes them)
|
|
81
|
+
query_node.sortable = self.get_bool_attr(element, 'sortable', False)
|
|
82
|
+
# DB-9: page= is an expression, resolved when the query runs; it was
|
|
83
|
+
# read as an int at parse time, so page="{query.page}" was always
|
|
84
|
+
# page 1. With paginate="true" and no page=, the page is the URL's
|
|
85
|
+
# `page` parameter — the one <ui:pager> writes.
|
|
86
|
+
pagina = element.get('page')
|
|
87
|
+
query_node.page = None
|
|
88
|
+
query_node.page_expr = None
|
|
89
|
+
if pagina is not None:
|
|
90
|
+
if '{' in pagina:
|
|
91
|
+
query_node.page_expr = pagina
|
|
92
|
+
elif pagina.strip().isdigit():
|
|
93
|
+
query_node.page = int(pagina)
|
|
94
|
+
else:
|
|
95
|
+
from quantum.core.parser import QuantumParseError
|
|
96
|
+
raise QuantumParseError(
|
|
97
|
+
f'<q:query name="{name}" page="{pagina}">: a page number or an expression, '
|
|
98
|
+
f'e.g. page="{{query.pagina}}"')
|
|
99
|
+
elif query_node.paginate:
|
|
100
|
+
query_node.page_expr = '{query.page}'
|
|
101
|
+
# Accept both spellings: the codebase convention is camelCase, but
|
|
102
|
+
# snake_case reads naturally in XML and the examples used it —
|
|
103
|
+
# the mismatch made page_size None and crashed pagination with
|
|
104
|
+
# 'unsupported operand type(s) for +: int and NoneType'.
|
|
105
|
+
query_node.page_size = (
|
|
106
|
+
self.get_int_attr(element, 'pageSize', 0)
|
|
107
|
+
or self.get_int_attr(element, 'page_size', 0)
|
|
108
|
+
or None
|
|
109
|
+
)
|
|
110
|
+
query_node.result = self.get_attr(element, 'result')
|
|
111
|
+
|
|
112
|
+
# RAG/Knowledge attributes
|
|
113
|
+
query_node.mode = self.get_attr(element, 'mode')
|
|
114
|
+
query_node.rag_model = self.get_attr(element, 'model')
|
|
115
|
+
|
|
116
|
+
# Parse q:param children
|
|
117
|
+
for child in element:
|
|
118
|
+
child_type = self.get_element_name(child)
|
|
119
|
+
if child_type == 'param':
|
|
120
|
+
param_node = self._parse_query_param(child)
|
|
121
|
+
query_node.add_param(param_node)
|
|
122
|
+
|
|
123
|
+
# Validate: reject direct {var} interpolation (SQL injection risk)
|
|
124
|
+
if sql and re.search(r'\{[a-zA-Z_]\w*\}', sql):
|
|
125
|
+
raise ParserError(
|
|
126
|
+
f"Query '{name}': direct {{var}} interpolation in SQL is not allowed. "
|
|
127
|
+
"Use :param_name with <q:param> for safe parameter binding"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Validate: all :param references must have matching <q:param>
|
|
131
|
+
if sql:
|
|
132
|
+
sql_params = self._named_params(sql)
|
|
133
|
+
declared_params = {p.name for p in query_node.params}
|
|
134
|
+
missing = sql_params - declared_params
|
|
135
|
+
if missing:
|
|
136
|
+
raise ParserError(
|
|
137
|
+
f"Query '{name}': SQL parameter(s) {', '.join(':' + p for p in sorted(missing))} "
|
|
138
|
+
"declared in SQL but no matching <q:param> element provided"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return query_node
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _named_params(sql: str) -> set:
|
|
145
|
+
"""Os `:nome` que sao REALMENTE parametros de bind.
|
|
146
|
+
|
|
147
|
+
Isto era `re.findall(r':([a-zA-Z_]\\w*)', sql)` sobre o SQL cru, e
|
|
148
|
+
varria tambem o que esta dentro de aspas e de comentarios:
|
|
149
|
+
|
|
150
|
+
TO_CHAR(c.created_at, 'YYYY-MM-DD HH24:MI') -> achava `:MI`
|
|
151
|
+
SELECT x::text FROM t -> achava `:text`
|
|
152
|
+
|
|
153
|
+
As duas formas sao SQL correto e comum — a primeira aparece em
|
|
154
|
+
projects/blog/components/post.q, que por causa disto nao parseava:
|
|
155
|
+
o arquivo era recusado com "SQL parameter(s) :MI declared in SQL but
|
|
156
|
+
no matching <q:param>", e nao havia parametro nenhum para declarar.
|
|
157
|
+
|
|
158
|
+
A varredura pula literais de texto (com '' e "" escapados), o cast
|
|
159
|
+
`::` do PostgreSQL, comentarios de linha `--` e de bloco.
|
|
160
|
+
"""
|
|
161
|
+
encontrados = set()
|
|
162
|
+
i, n = 0, len(sql)
|
|
163
|
+
while i < n:
|
|
164
|
+
c = sql[i]
|
|
165
|
+
if c in ("'", '"'):
|
|
166
|
+
aspas = c
|
|
167
|
+
i += 1
|
|
168
|
+
while i < n:
|
|
169
|
+
if sql[i] == aspas:
|
|
170
|
+
if i + 1 < n and sql[i + 1] == aspas:
|
|
171
|
+
i += 2 # '' escapado dentro do literal
|
|
172
|
+
continue
|
|
173
|
+
i += 1
|
|
174
|
+
break
|
|
175
|
+
i += 1
|
|
176
|
+
continue
|
|
177
|
+
if sql.startswith('--', i):
|
|
178
|
+
fim = sql.find('\n', i)
|
|
179
|
+
i = n if fim == -1 else fim + 1
|
|
180
|
+
continue
|
|
181
|
+
if sql.startswith('/*', i):
|
|
182
|
+
fim = sql.find('*/', i)
|
|
183
|
+
i = n if fim == -1 else fim + 2
|
|
184
|
+
continue
|
|
185
|
+
if c == ':':
|
|
186
|
+
if sql.startswith('::', i):
|
|
187
|
+
i += 2 # cast do PostgreSQL, nao parametro
|
|
188
|
+
continue
|
|
189
|
+
m = re.match(r':([a-zA-Z_]\w*)', sql[i:])
|
|
190
|
+
if m:
|
|
191
|
+
encontrados.add(m.group(1))
|
|
192
|
+
i += m.end()
|
|
193
|
+
continue
|
|
194
|
+
i += 1
|
|
195
|
+
return encontrados
|
|
196
|
+
|
|
197
|
+
def _parse_query_param(self, element: ET.Element) -> QueryParamNode:
|
|
198
|
+
"""Parse q:param within q:query."""
|
|
199
|
+
name = self.get_attr(element, 'name')
|
|
200
|
+
value = self.get_attr(element, 'value')
|
|
201
|
+
param_type = self.get_attr(element, 'type', 'string')
|
|
202
|
+
|
|
203
|
+
if not name:
|
|
204
|
+
raise ParserError("Query parameter requires 'name' attribute")
|
|
205
|
+
if value is None:
|
|
206
|
+
raise ParserError(f"Query parameter '{name}' requires 'value' attribute")
|
|
207
|
+
|
|
208
|
+
param_node = QueryParamNode(name, value, param_type)
|
|
209
|
+
param_node.null = self.get_bool_attr(element, 'null', False)
|
|
210
|
+
param_node.max_length = self.get_int_attr(element, 'maxLength', 0) or None
|
|
211
|
+
param_node.scale = self.get_int_attr(element, 'scale', 0) or None
|
|
212
|
+
|
|
213
|
+
return param_node
|