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.
Files changed (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,43 @@
1
+ """
2
+ Redirect Executor - q:redirect outside a q:action (ACT-7)
3
+
4
+ Inside a q:action the ActionHandler ends the action itself. Anywhere else —
5
+ the page's statements, a guard, a q:function — q:redirect ends the page:
6
+ nothing after it runs and the response is the redirect. It used to be skipped
7
+ silently, so `<q:if condition="not session.authenticated"><q:redirect .../>`
8
+ rendered the page it was meant to protect.
9
+ """
10
+
11
+ from typing import Any, List, Type
12
+
13
+ from quantum.core.ast_nodes import RedirectNode
14
+ from quantum.runtime.executors.base import BaseExecutor
15
+
16
+
17
+ class PageRedirect(BaseException):
18
+ """Raised by q:redirect to end the page; the web server turns it into the response.
19
+
20
+ A BaseException, like GeneratorExit: the layers between an executor and
21
+ the server catch `Exception` to report errors, and a redirect is not one.
22
+ """
23
+
24
+ def __init__(self, url: str, status: int = 302, flash: str = None, flash_type: str = None):
25
+ super().__init__(url)
26
+ self.url = url
27
+ self.status = status
28
+ self.flash = flash
29
+ self.flash_type = flash_type or 'success'
30
+
31
+
32
+ class RedirectExecutor(BaseExecutor):
33
+ """Executor for q:redirect outside a q:action."""
34
+
35
+ @property
36
+ def handles(self) -> List[Type]:
37
+ return [RedirectNode]
38
+
39
+ def execute(self, node: RedirectNode, exec_context) -> Any:
40
+ context = exec_context.get_all_variables()
41
+ url = str(self.apply_databinding(node.url, context))
42
+ flash = str(self.apply_databinding(node.flash, context)) if node.flash else None
43
+ raise PageRedirect(url, node.status, flash, getattr(node, 'flash_type', None))
@@ -0,0 +1,464 @@
1
+ """
2
+ Set Executor - Execute q:set statements
3
+
4
+ Handles variable assignment and all operations:
5
+ - assign, increment, decrement
6
+ - add, multiply (arithmetic)
7
+ - append, prepend, remove, clear, sort, etc. (array)
8
+ - merge, setProperty, deleteProperty, clone (object)
9
+ - uppercase, lowercase, trim, format (string)
10
+ """
11
+
12
+ from typing import Any, List, Dict, Type
13
+ import json
14
+ import re
15
+ from quantum.runtime.executors.base import BaseExecutor, ExecutorError
16
+ from quantum.core.features.state_management.src.ast_node import SetNode
17
+ from quantum.core.expressions import coerce_number
18
+
19
+
20
+ class SetExecutor(BaseExecutor):
21
+ """
22
+ Executor for q:set statements.
23
+
24
+ Supports:
25
+ - Basic assignment with type conversion
26
+ - Increment/decrement operations
27
+ - Array operations (append, remove, sort, etc.)
28
+ - Object operations (merge, setProperty, etc.)
29
+ - String transformations (uppercase, trim, etc.)
30
+ - Validation rules
31
+ """
32
+
33
+ @property
34
+ def handles(self) -> List[Type]:
35
+ return [SetNode]
36
+
37
+ def execute(self, node: SetNode, exec_context) -> Any:
38
+ """
39
+ Execute q:set statement.
40
+
41
+ Args:
42
+ node: SetNode to execute
43
+ exec_context: Execution context
44
+
45
+ Returns:
46
+ None (set doesn't return a value)
47
+ """
48
+ try:
49
+ context = exec_context.get_all_variables()
50
+
51
+ # Handle different operations
52
+ if node.operation == "assign":
53
+ value = self._execute_assign(node, context)
54
+ elif node.operation == "increment":
55
+ value = self._execute_increment(node, exec_context, node.step)
56
+ elif node.operation == "decrement":
57
+ value = self._execute_decrement(node, exec_context, node.step)
58
+ elif node.operation in ["add", "multiply"]:
59
+ value = self._execute_arithmetic(node, exec_context, context)
60
+ elif node.operation in ["append", "prepend", "remove", "removeAt", "clear", "sort", "reverse", "unique"]:
61
+ value = self._execute_array_operation(node, exec_context)
62
+ elif node.operation in ["merge", "setProperty", "deleteProperty", "clone"]:
63
+ value = self._execute_object_operation(node, exec_context, context)
64
+ elif node.operation in ["uppercase", "lowercase", "trim", "format"]:
65
+ value = self._execute_transformation(node, exec_context)
66
+ else:
67
+ raise ExecutorError(f"Unsupported operation: {node.operation}")
68
+
69
+ # Validate the value
70
+ self._validate_value(node, value)
71
+
72
+ # Set the variable in the appropriate scope
73
+ self._set_variable(node, value, exec_context)
74
+
75
+ return None # q:set doesn't return a value
76
+
77
+ except Exception as e:
78
+ raise ExecutorError(f"Set execution error for '{node.name}': {e}")
79
+
80
+ def _execute_assign(self, node: SetNode, context: Dict[str, Any]) -> Any:
81
+ """Execute assign operation"""
82
+ value_expr = node.value if node.value is not None else node.default
83
+
84
+ if value_expr is None:
85
+ if not node.nullable:
86
+ raise ExecutorError(f"Variable '{node.name}' cannot be null")
87
+ return None
88
+
89
+ # Process databinding
90
+ processed_value = self.apply_databinding(value_expr, context)
91
+
92
+ # SET-1: default= is what to store when value= resolves to nothing.
93
+ # It was only read when value= was ABSENT from the tag, so
94
+ # <q:set name="clicks" value="{session.click_count}" default="0"/>
95
+ # stored '' on the first visit and the next {clicks + 1} failed.
96
+ if (processed_value is None or processed_value == '') and node.value is not None \
97
+ and node.default is not None:
98
+ processed_value = self.apply_databinding(node.default, context)
99
+
100
+ # Convert to appropriate type
101
+ return self._convert_to_type(processed_value, node.type, source=value_expr)
102
+
103
+ def _numeric_or_fail(self, value: Any, op: str, where: str) -> Any:
104
+ """Coerce a numeric-looking value, or fail loudly.
105
+
106
+ A .q attribute is a string by nature — `<q:set name="estoque"
107
+ value="7" />` stores "7" unless the author remembered type="number".
108
+ Refusing to add to that made increment/decrement unusable in the
109
+ ordinary case, so it is coerced, the same way {a + b} coerces.
110
+ """
111
+ coerced = coerce_number(value)
112
+ if isinstance(coerced, bool) or not isinstance(coerced, (int, float)):
113
+ raise ExecutorError(
114
+ f"Cannot {op} non-numeric value {value!r} ({where}). "
115
+ f"Set it with type=\"number\", or give q:set a numeric value."
116
+ )
117
+ return coerced
118
+
119
+ def _current_numeric(self, node: SetNode, exec_context, op: str):
120
+ """(value, exists) for the variable being incremented/decremented.
121
+
122
+ The previous version wrapped BOTH the lookup and the type check in one
123
+ `try: ... except Exception: return step`, so the ExecutorError it
124
+ raised for a non-numeric value was caught by its own handler one line
125
+ later and turned into the step itself. A stock of "7" decremented by 5
126
+ became -5 — the detection was correct and then discarded. Missing and
127
+ non-numeric are separated here so only the first one falls back.
128
+ """
129
+ try:
130
+ current = exec_context.get_variable(node.name)
131
+ except Exception:
132
+ return None, False
133
+ if current is None or current == '':
134
+ return None, False
135
+ return self._numeric_or_fail(current, op, f"current value of {node.name!r}"), True
136
+
137
+ def _execute_increment(self, node: SetNode, exec_context, step: int) -> Any:
138
+ """Execute increment operation"""
139
+ if node.value:
140
+ context = exec_context.get_all_variables()
141
+ base_value = self.apply_databinding(node.value, context)
142
+ return self._numeric_or_fail(base_value, 'increment', 'value=') + step
143
+
144
+ current, exists = self._current_numeric(node, exec_context, 'increment')
145
+ return (current + step) if exists else step
146
+
147
+ def _execute_decrement(self, node: SetNode, exec_context, step: int) -> Any:
148
+ """Execute decrement operation"""
149
+ if node.value:
150
+ context = exec_context.get_all_variables()
151
+ base_value = self.apply_databinding(node.value, context)
152
+ return self._numeric_or_fail(base_value, 'decrement', 'value=') - step
153
+
154
+ current, exists = self._current_numeric(node, exec_context, 'decrement')
155
+ return (current - step) if exists else -step
156
+
157
+ def _execute_arithmetic(self, node: SetNode, exec_context, context: Dict[str, Any]) -> Any:
158
+ """Execute arithmetic operations (add, multiply)"""
159
+ try:
160
+ current_value = exec_context.get_variable(node.name)
161
+ except Exception:
162
+ current_value = 0
163
+ if current_value is None or current_value == '':
164
+ current_value = 0
165
+
166
+ # Same coercion as increment/decrement: a .q attribute is a string by
167
+ # nature, so add/multiply on a variable set without type="number" used
168
+ # to fail even though the value was plainly numeric.
169
+ current_value = self._numeric_or_fail(
170
+ current_value, node.operation, f"current value of {node.name!r}"
171
+ )
172
+
173
+ operand_expr = node.value
174
+ if not operand_expr:
175
+ raise ExecutorError("Arithmetic operation requires a value")
176
+
177
+ fresh_context = exec_context.get_all_variables()
178
+ processed = self.apply_databinding(operand_expr, fresh_context)
179
+ operand = self._convert_to_type(processed, "number")
180
+
181
+ if node.operation == "add":
182
+ return current_value + operand
183
+ elif node.operation == "multiply":
184
+ return current_value * operand
185
+
186
+ return current_value
187
+
188
+ def _execute_array_operation(self, node: SetNode, exec_context) -> Any:
189
+ """Execute array operations"""
190
+ try:
191
+ current_value = exec_context.get_variable(node.name)
192
+ except Exception:
193
+ current_value = []
194
+
195
+ if not isinstance(current_value, list):
196
+ raise ExecutorError(f"Cannot perform array operation on non-array: {type(current_value)}")
197
+
198
+ result = current_value.copy()
199
+
200
+ # Resolve databinding in value
201
+ resolved_value = node.value
202
+ if resolved_value and node.operation in ("append", "prepend", "remove"):
203
+ context = exec_context.get_all_variables()
204
+ resolved = self.apply_databinding(resolved_value, context)
205
+ if resolved is not None:
206
+ resolved_value = resolved
207
+
208
+ if node.operation == "append":
209
+ if resolved_value:
210
+ result.append(resolved_value)
211
+ elif node.operation == "prepend":
212
+ if resolved_value:
213
+ result.insert(0, resolved_value)
214
+ elif node.operation == "remove":
215
+ if resolved_value and resolved_value in result:
216
+ result.remove(resolved_value)
217
+ elif node.operation == "removeAt":
218
+ if node.index is not None:
219
+ idx = int(node.index)
220
+ if 0 <= idx < len(result):
221
+ result.pop(idx)
222
+ elif node.operation == "clear":
223
+ result = []
224
+ elif node.operation == "sort":
225
+ result.sort()
226
+ elif node.operation == "reverse":
227
+ result.reverse()
228
+ elif node.operation == "unique":
229
+ result = list(dict.fromkeys(result))
230
+
231
+ return result
232
+
233
+ def _execute_object_operation(self, node: SetNode, exec_context, context: Dict[str, Any]) -> Any:
234
+ """Execute object operations"""
235
+ try:
236
+ current_value = exec_context.get_variable(node.name)
237
+ except Exception:
238
+ current_value = {}
239
+
240
+ if not isinstance(current_value, dict):
241
+ raise ExecutorError(f"Cannot perform object operation on non-object: {type(current_value)}")
242
+
243
+ result = current_value.copy()
244
+
245
+ if node.operation == "merge":
246
+ if node.value:
247
+ try:
248
+ merge_data = json.loads(node.value)
249
+ result.update(merge_data)
250
+ except json.JSONDecodeError:
251
+ raise ExecutorError(f"Invalid JSON for merge: {node.value}")
252
+ elif node.operation == "setProperty":
253
+ if node.key and node.value:
254
+ result[node.key] = node.value
255
+ elif node.operation == "deleteProperty":
256
+ if node.key and node.key in result:
257
+ del result[node.key]
258
+ elif node.operation == "clone":
259
+ if node.source:
260
+ try:
261
+ source_obj = exec_context.get_variable(node.source)
262
+ if isinstance(source_obj, dict):
263
+ result = source_obj.copy()
264
+ except Exception:
265
+ pass
266
+
267
+ return result
268
+
269
+ def _execute_transformation(self, node: SetNode, exec_context) -> Any:
270
+ """Execute string transformation operations"""
271
+ if node.value:
272
+ context = exec_context.get_all_variables()
273
+ processed_value = self.apply_databinding(node.value, context)
274
+ value_str = str(processed_value)
275
+ else:
276
+ try:
277
+ current_value = exec_context.get_variable(node.name)
278
+ value_str = str(current_value)
279
+ except Exception:
280
+ value_str = ""
281
+
282
+ if node.operation == "uppercase":
283
+ return value_str.upper()
284
+ elif node.operation == "lowercase":
285
+ return value_str.lower()
286
+ elif node.operation == "trim":
287
+ return value_str.strip()
288
+ elif node.operation == "format":
289
+ return value_str
290
+
291
+ return value_str
292
+
293
+ def _convert_to_type(self, value: Any, target_type: str, source: str = None) -> Any:
294
+ """Convert value to target type.
295
+
296
+ ERR-1: a value that does not convert is an error that shows the value
297
+ and how to write it. The messages used to be Python's own — "could not
298
+ convert string to float: '10 + 20'" for value="{a} + {b}", and
299
+ "Expecting property name enclosed in double quotes" for an array
300
+ written with single quotes — neither of which says what to change.
301
+ """
302
+ if value is None:
303
+ return None
304
+
305
+ if target_type in ("integer", "number", "decimal"):
306
+ if isinstance(value, bool):
307
+ raise ExecutorError(f"{value!r} is a boolean, not a number")
308
+ numero = coerce_number(value)
309
+ if not isinstance(numero, (int, float)):
310
+ raise ExecutorError(self._not_a_number(value, source))
311
+ if target_type == "decimal":
312
+ return float(numero)
313
+ if target_type == "integer":
314
+ # It used to be int(value): {7 / 2} became 3, silently.
315
+ if isinstance(numero, float):
316
+ if not numero.is_integer():
317
+ raise ExecutorError(
318
+ f"{numero!r} is not a whole number; use round() in the "
319
+ f"expression, or type=\"number\"")
320
+ return int(numero)
321
+ return numero
322
+ # type="number" also went through int() first: {5 / 2} stored 2.
323
+ return numero
324
+
325
+ if target_type == "boolean":
326
+ if isinstance(value, bool):
327
+ return value
328
+ if isinstance(value, str):
329
+ texto = value.strip().lower()
330
+ if texto in ('true', '1', 'yes'):
331
+ return True
332
+ if texto in ('false', '0', 'no', ''):
333
+ return False
334
+ raise ExecutorError(
335
+ f"{value!r} is not a boolean; use true or false")
336
+ return bool(value)
337
+
338
+ if target_type in ("array", "object", "json"):
339
+ vazio = {"array": [value], "object": {}, "json": value}[target_type]
340
+ if isinstance(value, (dict, list)):
341
+ if target_type == "array" and not isinstance(value, list):
342
+ raise ExecutorError(f"expected an array, got an object")
343
+ if target_type == "object" and not isinstance(value, dict):
344
+ raise ExecutorError(f"expected an object, got an array")
345
+ return value
346
+ if not isinstance(value, str):
347
+ return vazio
348
+ try:
349
+ return json.loads(value)
350
+ except json.JSONDecodeError as exc:
351
+ raise ExecutorError(self._not_json(value, target_type, exc)) from None
352
+
353
+ if target_type == "string":
354
+ return str(value)
355
+ return value
356
+
357
+ @staticmethod
358
+ def _not_a_number(value: Any, source: str) -> str:
359
+ mensagem = f"{value!r} is not a number"
360
+ # value="{a} + {b}": each pair of braces is evaluated alone and the
361
+ # text between them is kept, so the result is the text '10 + 20'.
362
+ if source and '{' in source and not re.fullmatch(r'\s*\{[^{}]*\}\s*', source):
363
+ junto = '{' + re.sub(r'\{([^{}]*)\}', r'\1', source).strip() + '}'
364
+ mensagem += (f". Only what is inside one pair of braces is calculated: "
365
+ f"write value=\"{junto}\" instead of value=\"{source}\"")
366
+ return mensagem
367
+
368
+ @staticmethod
369
+ def _not_json(value: str, target_type: str, exc: json.JSONDecodeError) -> str:
370
+ trecho = value.strip()
371
+ if len(trecho) > 60:
372
+ trecho = trecho[:57] + '...'
373
+ mensagem = (f"value is not a valid {target_type} (JSON): {trecho!r}, "
374
+ f"problem at character {exc.pos + 1}")
375
+ if "'" in value:
376
+ mensagem += ('. JSON uses double quotes for text and keys: '
377
+ 'value=\'[{"name": "Ana"}]\' — put the attribute itself in single quotes')
378
+ return mensagem
379
+
380
+ def _validate_value(self, node: SetNode, value: Any):
381
+ """Validate value against set_node rules"""
382
+ # Import validators lazily to avoid circular imports
383
+ from quantum.runtime.validators import QuantumValidators, ValidationError
384
+
385
+ # Check required
386
+ if node.required:
387
+ is_valid, error = QuantumValidators.validate_required(value)
388
+ if not is_valid:
389
+ raise ExecutorError(error)
390
+
391
+ # Check nullable
392
+ if not node.nullable and value is None:
393
+ raise ExecutorError(f"Variable '{node.name}' cannot be null")
394
+
395
+ # Skip other validations if null and nullable
396
+ if value is None and node.nullable:
397
+ return
398
+
399
+ # Check validate_rule
400
+ if node.validate_rule:
401
+ if node.validate_rule == 'cpf':
402
+ is_valid, error = QuantumValidators.validate_cpf(str(value))
403
+ elif node.validate_rule == 'cnpj':
404
+ is_valid, error = QuantumValidators.validate_cnpj(str(value))
405
+ else:
406
+ is_valid, error = QuantumValidators.validate(value, node.validate_rule)
407
+
408
+ if not is_valid:
409
+ raise ExecutorError(error)
410
+
411
+ # Check pattern
412
+ if node.pattern and not node.validate_rule:
413
+ is_valid, error = QuantumValidators.validate(value, node.pattern)
414
+ if not is_valid:
415
+ raise ExecutorError(error)
416
+
417
+ # Check range
418
+ if node.range:
419
+ is_valid, error = QuantumValidators.validate_range(value, node.range)
420
+ if not is_valid:
421
+ raise ExecutorError(error)
422
+
423
+ # Check enum
424
+ if node.enum:
425
+ is_valid, error = QuantumValidators.validate_enum(value, node.enum)
426
+ if not is_valid:
427
+ raise ExecutorError(error)
428
+
429
+ # Check min/max
430
+ if node.min or node.max:
431
+ is_valid, error = QuantumValidators.validate_min_max(
432
+ value, min_val=node.min, max_val=node.max
433
+ )
434
+ if not is_valid:
435
+ raise ExecutorError(error)
436
+
437
+ # Check minlength/maxlength
438
+ if node.type == "string" and (node.minlength or node.maxlength):
439
+ minlen = int(node.minlength) if node.minlength else None
440
+ maxlen = int(node.maxlength) if node.maxlength else None
441
+
442
+ is_valid, error = QuantumValidators.validate_length(
443
+ value, minlength=minlen, maxlength=maxlen
444
+ )
445
+ if not is_valid:
446
+ raise ExecutorError(error)
447
+
448
+ def _set_variable(self, node: SetNode, value: Any, exec_context):
449
+ """Set variable in the appropriate scope"""
450
+ is_update_operation = node.operation in [
451
+ "increment", "decrement", "add", "multiply",
452
+ "append", "prepend", "remove", "removeAt",
453
+ "clear", "sort", "reverse", "unique",
454
+ "merge", "setProperty", "deleteProperty",
455
+ "uppercase", "lowercase", "trim", "format"
456
+ ]
457
+
458
+ if is_update_operation or (exec_context.has_variable(node.name) and node.scope == "local"):
459
+ exec_context.update_variable(node.name, value)
460
+ else:
461
+ actual_scope = node.scope
462
+ if node.scope == "local" and exec_context.parent is not None:
463
+ actual_scope = "function"
464
+ exec_context.set_variable(node.name, value, scope=actual_scope)
@@ -0,0 +1,12 @@
1
+ """
2
+ Data Executors
3
+
4
+ Executors for data operations: query, invoke, data, transaction.
5
+ """
6
+
7
+ from .query_executor import QueryExecutor
8
+ from .invoke_executor import InvokeExecutor
9
+ from .data_executor import DataExecutor
10
+ from .transaction_executor import TransactionExecutor
11
+
12
+ __all__ = ['QueryExecutor', 'InvokeExecutor', 'DataExecutor', 'TransactionExecutor']
@@ -0,0 +1,156 @@
1
+ """
2
+ Data Executor - Execute q:data statements
3
+
4
+ Handles data import from CSV, JSON, XML sources with transformations.
5
+ """
6
+
7
+ from typing import Any, List, Dict, Type
8
+ from quantum.runtime.executors.base import BaseExecutor, ExecutorError
9
+ from quantum.core.ast_nodes import DataNode
10
+
11
+
12
+ class DataExecutor(BaseExecutor):
13
+ """
14
+ Executor for q:data statements.
15
+
16
+ Supports:
17
+ - CSV import with column definitions
18
+ - JSON import from files or URLs
19
+ - XML import with XPath
20
+ - Data transformations (filter, sort, limit, compute)
21
+ - Response caching with TTL
22
+ """
23
+
24
+ @property
25
+ def handles(self) -> List[Type]:
26
+ return [DataNode]
27
+
28
+ def execute(self, node: DataNode, exec_context) -> Any:
29
+ """
30
+ Execute data import.
31
+
32
+ Args:
33
+ node: DataNode with import configuration
34
+ exec_context: Execution context
35
+
36
+ Returns:
37
+ None (stores result in context)
38
+ """
39
+ try:
40
+ context = exec_context.get_all_variables()
41
+
42
+ # Resolve source
43
+ source = self.apply_databinding(node.source, context)
44
+
45
+ # Build parameters
46
+ params = self._build_params(node, context)
47
+
48
+ # Execute data import
49
+ result = self.services.data_import.import_data(
50
+ node.data_type,
51
+ source,
52
+ params,
53
+ context=exec_context
54
+ )
55
+
56
+ # Store result
57
+ self._store_result(node, result, exec_context)
58
+
59
+ except Exception as e:
60
+ raise ExecutorError(f"Data import error in '{node.name}': {e}")
61
+
62
+ # DATA-4: a failed import used to leave the variable None and carry on,
63
+ # so the page rendered empty with the reason tucked into _result.
64
+ if not result.success and getattr(node, 'on_error', 'fail') != 'continue':
65
+ motivo = (result.error or {}).get('message', 'unknown error')
66
+ raise ExecutorError(
67
+ f"q:data '{node.name}' could not read {source!r}: {motivo}. "
68
+ f"Add onerror=\"continue\" to handle it with {node.name}_result instead.")
69
+
70
+ def _build_params(self, node: DataNode, context: Dict[str, Any]) -> Dict[str, Any]:
71
+ """Build parameters for data import"""
72
+ params = {
73
+ 'cache': node.cache,
74
+ 'ttl': node.ttl,
75
+ 'delimiter': node.delimiter,
76
+ 'quote': node.quote,
77
+ 'header': node.header,
78
+ 'encoding': node.encoding,
79
+ 'skip_rows': node.skip_rows,
80
+ 'xpath': node.xpath,
81
+ 'namespace': node.namespace,
82
+ 'columns': [],
83
+ 'fields': [],
84
+ 'transforms': [],
85
+ 'headers': []
86
+ }
87
+
88
+ # Add column definitions (for CSV)
89
+ for col in node.columns:
90
+ params['columns'].append({
91
+ 'name': col.name,
92
+ 'type': col.col_type,
93
+ 'required': col.required,
94
+ 'default': col.default
95
+ })
96
+
97
+ # Add field definitions (for XML)
98
+ for field in node.fields:
99
+ params['fields'].append({
100
+ 'name': field.name,
101
+ 'xpath': field.xpath,
102
+ 'type': field.field_type
103
+ })
104
+
105
+ # Add HTTP headers
106
+ for header in node.headers:
107
+ resolved_value = self.apply_databinding(header.value, context)
108
+ params['headers'].append({
109
+ 'name': header.name,
110
+ 'value': resolved_value
111
+ })
112
+
113
+ # Add transformations
114
+ for transform in node.transforms:
115
+ for operation in transform.operations:
116
+ op_dict = {
117
+ 'type': operation.__class__.__name__.replace('Node', '').lower()
118
+ }
119
+
120
+ if hasattr(operation, 'condition'):
121
+ op_dict['condition'] = operation.condition
122
+ elif hasattr(operation, 'by'):
123
+ op_dict['by'] = operation.by
124
+ op_dict['order'] = operation.order
125
+ elif hasattr(operation, 'value'):
126
+ op_dict['value'] = operation.value
127
+ elif hasattr(operation, 'field'):
128
+ op_dict['field'] = operation.field
129
+ op_dict['expression'] = operation.expression
130
+ # Not 'type': that key names the operation ('compute').
131
+ # Writing the result type over it made every q:compute
132
+ # unrecognised, and the transformation was skipped.
133
+ op_dict['comp_type'] = operation.comp_type
134
+
135
+ params['transforms'].append(op_dict)
136
+
137
+ return params
138
+
139
+ def _store_result(self, node: DataNode, result, exec_context):
140
+ """Store data import result in context"""
141
+ exec_context.set_variable(node.name, result.data, scope="component")
142
+
143
+ result_dict = {
144
+ 'success': result.success,
145
+ 'data': result.data,
146
+ 'error': result.error,
147
+ 'recordCount': result.recordCount,
148
+ 'loadTime': result.loadTime,
149
+ 'cached': result.cached,
150
+ 'source': result.source
151
+ }
152
+
153
+ exec_context.set_variable(f"{node.name}_result", result_dict, scope="component")
154
+
155
+ if node.result:
156
+ exec_context.set_variable(node.result, result_dict, scope="component")