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,25 @@
1
+ # Runtime Quantum modules
2
+ #
3
+ # NOTE: Do NOT add eager imports here. This package has circular dependencies
4
+ # (web_server -> component -> database_service -> ... -> __init__ -> web_server)
5
+ # that cause Python to deadlock during import resolution.
6
+ #
7
+ # All consumers already import directly from submodules:
8
+ # from runtime.component import ComponentRuntime
9
+ # from runtime.web_server import QuantumWebServer
10
+ #
11
+ # Lazy accessors are provided below for backward compatibility only.
12
+
13
+
14
+ def __getattr__(name):
15
+ """Lazy import to avoid circular dependency deadlock."""
16
+ if name == 'ComponentRuntime':
17
+ from .component import ComponentRuntime
18
+ return ComponentRuntime
19
+ if name == 'ComponentExecutionError':
20
+ from .component import ComponentExecutionError
21
+ return ComponentExecutionError
22
+ if name == 'QuantumWebServer':
23
+ from .web_server import QuantumWebServer
24
+ return QuantumWebServer
25
+ raise AttributeError(f"module 'runtime' has no attribute {name!r}")
@@ -0,0 +1,532 @@
1
+ """
2
+ Quantum Action Handler - Process server-side actions from forms
3
+
4
+ Phase A: Forms & Actions
5
+ Handles POST/PUT/DELETE requests, validates parameters, executes action body,
6
+ manages flash messages, and performs redirects.
7
+ """
8
+
9
+ from typing import Dict, Any, Optional, List, Tuple
10
+ from flask import request, session, redirect as flask_redirect
11
+ from quantum.core.ast_nodes import (
12
+ ActionNode, RedirectNode, FlashNode, QuantumParam, QueryNode, PythonNode,
13
+ HTMLNode, TextNode, CommentNode
14
+ )
15
+ from quantum.core.features.state_management.src.ast_node import SetNode
16
+ from quantum.core.features.conditionals.src.ast_node import IfNode
17
+ from quantum.runtime.execution_context import ExecutionContext
18
+ from quantum.runtime.component import ComponentExecutionError, ComponentRuntime
19
+ from quantum.runtime import param_validation
20
+ import re
21
+ import logging
22
+
23
+ logger = logging.getLogger('quantum.action')
24
+
25
+
26
+ class ValidationError(Exception):
27
+ """Parameter validation error.
28
+
29
+ `fields` maps each field that failed to its own message (UI-9): the form
30
+ shows each one next to its field. The exception's text is the first one,
31
+ the flash message it always was."""
32
+
33
+ def __init__(self, message: str, fields: Optional[Dict[str, str]] = None):
34
+ super().__init__(message)
35
+ self.fields = fields or {}
36
+
37
+
38
+ def _field_message(nome: str, mensagem: str) -> str:
39
+ """"Parameter 'titulo' must be at least 3 characters" -> "Must be at least 3 characters"."""
40
+ texto = re.sub(rf"^Parameter '{re.escape(nome)}' ", '', mensagem)
41
+ if texto == 'is required':
42
+ return 'Required'
43
+ return texto[:1].upper() + texto[1:]
44
+
45
+
46
+ class ActionHandler:
47
+ """
48
+ Handles server-side action execution.
49
+
50
+ Workflow:
51
+ 1. Extract form data from request
52
+ 2. Validate against q:param definitions
53
+ 3. Execute action body statements
54
+ 4. Handle flash messages
55
+ 5. Perform redirect
56
+ """
57
+
58
+ def __init__(self, component_runtime: Optional[ComponentRuntime] = None):
59
+ self.component_runtime = component_runtime or ComponentRuntime()
60
+
61
+ def handle_action(
62
+ self,
63
+ action: ActionNode,
64
+ context: Optional[ExecutionContext] = None,
65
+ route_params: Optional[Dict[str, str]] = None
66
+ ) -> Tuple[Optional[str], int]:
67
+ """
68
+ Handle action execution.
69
+
70
+ Returns:
71
+ Tuple[redirect_url, status_code] or None if no redirect
72
+ """
73
+
74
+ # Use provided context or create new one, seeded with the live Flask session
75
+ # so `q:set name="session.x"` persists and require_auth sees it on the next
76
+ # request. Without this, an action's ExecutionContext started empty and any
77
+ # session mutation made during the action was silently discarded.
78
+ owns_context = context is None
79
+ if context is None:
80
+ context = ExecutionContext()
81
+ context.session_vars = session.get('quantum_session', {})
82
+
83
+ try:
84
+ # 1. Extract form data
85
+ form_data = self._extract_form_data()
86
+
87
+ # 2. Validate parameters
88
+ validated_params = self._validate_parameters(self._params_of(action), form_data, action)
89
+
90
+ # 3. Add validated params to context
91
+ for key, value in validated_params.items():
92
+ context.set_variable(key, value)
93
+
94
+ # Os arquivos enviados entram no contexto mesmo sem um q:param
95
+ # que os declare. `_extract_form_data` ja os coleta e a
96
+ # documentacao promete `q.files`, mas so os parametros validados
97
+ # chegavam ao contexto — entao `<q:file action="upload"
98
+ # file="{avatar}">` sem um `<q:param name="avatar">` acima dizia
99
+ # "File variable 'avatar' not found". Um q:param declarado ganha:
100
+ # ele passou pela validacao.
101
+ for key, value in form_data.items():
102
+ if key in validated_params:
103
+ continue
104
+ if key == 'files' or param_validation.is_uploaded_file(value):
105
+ context.set_variable(key, value)
106
+
107
+ # ACT-6: the submitted fields as `form`, raw, like the page render
108
+ # gets them. {form.campo} inside an action used to evaluate to
109
+ # empty — only declared q:params reached the action — while the
110
+ # docs (data-fetching.md) and pages (rag.q) use {form.campo}.
111
+ context.set_variable('form', {
112
+ key: value for key, value in form_data.items()
113
+ if key != 'files' and not param_validation.is_uploaded_file(value)
114
+ })
115
+
116
+ # ROUTE-2: the page's route segments ([nome], [...caminho]) are
117
+ # variables of the action too, set last so a form field with the
118
+ # same name cannot replace them. A POST to /admin/app/loja used to
119
+ # run without `name`, so every form had to repeat it in a hidden
120
+ # field — one the browser can change.
121
+ for key, value in (route_params or {}).items():
122
+ context.set_variable(key, value)
123
+
124
+ # 4. Execute action body
125
+ try:
126
+ redirect_info = self._execute_action_body(action, context)
127
+ finally:
128
+ # M12: the /_dev panel shows the action's variables (no-op off debug).
129
+ from quantum.runtime.dev_panel import note_scopes
130
+ note_scopes(context, 'action')
131
+
132
+ # 5. Sync session mutations back to Flask (mirrors web_server.py's
133
+ # render path, which does the same after execute_component()).
134
+ if owns_context:
135
+ session['quantum_session'] = context.session_vars
136
+ session.modified = True
137
+
138
+ # 6. Return redirect if present
139
+ if redirect_info:
140
+ return redirect_info
141
+
142
+ return None, 200
143
+
144
+ except ValidationError as e:
145
+ # Validation failed - set error flash and redirect back
146
+ self._set_flash_message(str(e), 'error')
147
+ # UI-9: the next render of the form shows each error by its field
148
+ # and the values that were sent (never a password, never a file).
149
+ session['form_state'] = {
150
+ 'action': getattr(action, 'name', None),
151
+ 'errors': e.fields,
152
+ 'values': {n: v for n, v in request.form.items()
153
+ if n != 'action' and n not in self._secret_fields(action)},
154
+ }
155
+ session.modified = True
156
+ # Redirect to referer or home
157
+ referer = request.referrer or '/'
158
+ return referer, 302
159
+
160
+ except Exception as e:
161
+ # Unexpected error. Sync whatever session state changed before the
162
+ # failure, and report a real error instead of a redirect disguised
163
+ # as one (a 500 with a Location header confuses clients into
164
+ # thinking the action succeeded).
165
+ #
166
+ # And LOG it, with the traceback. The 500 page tells the user to
167
+ # "check server logs for details" and the logs recorded absolutely
168
+ # nothing — the failure existed only as a flash message the user
169
+ # might never see. Whoever is debugging gets the action name, the
170
+ # exception and the stack.
171
+ # ACT-9: the most common failure. The action runs without the
172
+ # page's statements, so a variable the page sets is not here.
173
+ mensagem = str(e)
174
+ if 'is not defined' in mensagem:
175
+ from quantum.core.docs_links import HOW_A_PAGE_RUNS
176
+ mensagem += (". A q:action does not run the page's statements (ACT-9): "
177
+ f"query or compute what it needs inside the action. See {HOW_A_PAGE_RUNS}")
178
+ logger.exception(
179
+ "q:action %r failed: %s", getattr(action, 'name', '?'), mensagem
180
+ )
181
+ if owns_context:
182
+ session['quantum_session'] = context.session_vars
183
+ session.modified = True
184
+ self._set_flash_message(f'Action error: {mensagem}', 'error')
185
+ return None, 500
186
+
187
+ def _extract_form_data(self) -> Dict[str, Any]:
188
+ """Extract data from POST/PUT/DELETE request, uploaded files included.
189
+
190
+ This returned request.form only. Uploaded files never entered the
191
+ context, so
192
+
193
+ <q:file action="upload" file="{avatar}" destination="..." />
194
+
195
+ raised "File variable 'avatar' not found" for every web request:
196
+ q:file action="upload" could not be reached from a browser at all,
197
+ which is the only place a file upload comes from.
198
+ """
199
+ data: Dict[str, Any] = {}
200
+
201
+ if request.method in ['POST', 'PUT', 'PATCH']:
202
+ if request.is_json:
203
+ data = request.get_json() or {}
204
+ else:
205
+ data = request.form.to_dict()
206
+
207
+ elif request.method == 'DELETE':
208
+ if request.is_json:
209
+ data = request.get_json() or {}
210
+
211
+ try:
212
+ uploaded = request.files
213
+ except Exception:
214
+ uploaded = None
215
+
216
+ if uploaded:
217
+ files = {}
218
+ for field, storage in uploaded.items(multi=False):
219
+ if not storage or not storage.filename:
220
+ continue # an empty file input posts a blank part
221
+ files[field] = storage
222
+ if field in data:
223
+ logger.warning(
224
+ "the upload field %r has the same name as a form "
225
+ "field; the file wins", field)
226
+ data[field] = storage
227
+ if files:
228
+ # Also as a group, which is what q.files documents.
229
+ data.setdefault('files', files)
230
+
231
+ return data
232
+
233
+ def _params_of(self, action) -> List[QuantumParam]:
234
+ """The action's q:params, plus its table's when it declares one (UI-10)."""
235
+ from quantum.runtime.table_params import params_of
236
+ return params_of(action, getattr(self.component_runtime, 'config', None) or {})
237
+
238
+ def _validate_parameters(
239
+ self,
240
+ params: List[QuantumParam],
241
+ form_data: Dict[str, Any],
242
+ action=None,
243
+ ) -> Dict[str, Any]:
244
+ """
245
+ Validate form data against q:param definitions.
246
+
247
+ Raises ValidationError if validation fails.
248
+ """
249
+
250
+ validated = {}
251
+ primeira = None
252
+ por_campo: Dict[str, str] = {}
253
+
254
+ for param in params:
255
+ value = form_data.get(param.name)
256
+ try:
257
+ # Check required
258
+ if param.required and (value is None or value == ''):
259
+ raise ValidationError(f"Parameter '{param.name}' is required")
260
+
261
+ # Skip validation if optional and not provided
262
+ if value is None or value == '':
263
+ if param.default is not None:
264
+ validated[param.name] = param.default
265
+ elif getattr(param, 'nullable', False):
266
+ validated[param.name] = None # UI-10: a NULL column left blank
267
+ continue
268
+
269
+ # Type validation and conversion
270
+ validated_value = self._validate_type(param, value)
271
+
272
+ # Additional validation rules
273
+ self._validate_rules(param, validated_value)
274
+
275
+ # UI-10: a foreign key names a row that exists.
276
+ referencia = getattr(param, 'references', None)
277
+ if referencia and action is not None and getattr(action, 'table_datasource', None):
278
+ from quantum.runtime.table_params import reference_exists
279
+ if not reference_exists(getattr(self.component_runtime, 'config', None) or {},
280
+ action.table_datasource, referencia, validated_value):
281
+ raise ValidationError(
282
+ f"Parameter '{param.name}' must name an existing row of {referencia[0]} "
283
+ f"(no {referencia[1]} = {validated_value})")
284
+
285
+ validated[param.name] = validated_value
286
+ except ValidationError as erro:
287
+ # UI-9: every field is checked, so the form can mark each one.
288
+ primeira = primeira or str(erro)
289
+ por_campo[param.name] = _field_message(param.name, str(erro))
290
+
291
+ if primeira:
292
+ raise ValidationError(primeira, por_campo)
293
+ return validated
294
+
295
+ @staticmethod
296
+ def _secret_fields(action) -> set:
297
+ """Fields never sent back to a form: passwords and uploads (UI-9)."""
298
+ nomes = set()
299
+ for param in getattr(action, 'params', None) or []: # table columns are never secrets
300
+ tipo = str(getattr(param, 'type', '') or '').lower()
301
+ if tipo in ('password', 'file') or 'senha' in param.name.lower() or 'password' in param.name.lower():
302
+ nomes.add(param.name)
303
+ return nomes
304
+
305
+ def _validate_type(self, param: QuantumParam, value: Any) -> Any:
306
+ """Converte o valor para o tipo declarado, ou levanta ValidationError.
307
+
308
+ Delega para quantum.runtime.param_validation, a MESMA implementacao
309
+ que o ComponentRuntime usa. Havia duas copias divergentes: esta so
310
+ conhecia `integer`, `decimal` e `float`, e o resto caia num
311
+ `return str(value)` final — `type="number"` (32 usos no repositorio)
312
+ virava string sem checar min/max, e `type="file"` (5 usos, incluindo
313
+ components/upload_demo.q) transformava o objeto de upload na sua
314
+ repr, `"<FileStorage: 'foto.png' ('image/png')>"`, que era o que o
315
+ <q:file action="upload"> recebia no lugar do arquivo.
316
+ """
317
+ convertido, erro = param_validation.coerce(param, value)
318
+ if erro:
319
+ raise ValidationError(erro)
320
+ return convertido
321
+
322
+ def _validate_rules(self, param: QuantumParam, value: Any):
323
+ """min/max/minlength/maxlength/pattern/enum — a mesma implementacao
324
+ que o ComponentRuntime aplica."""
325
+ erros = param_validation.check_rules(param, value)
326
+ if erros:
327
+ raise ValidationError(erros[0])
328
+
329
+ def _execute_action_body(
330
+ self,
331
+ action: ActionNode,
332
+ context: ExecutionContext
333
+ ) -> Optional[Tuple[str, int]]:
334
+ """
335
+ Execute action body statements.
336
+
337
+ Returns redirect info if redirect encountered, None otherwise.
338
+ """
339
+
340
+ return self._execute_statements(action.body, context)
341
+
342
+ def _execute_statements(
343
+ self,
344
+ statements,
345
+ context: ExecutionContext
346
+ ) -> Optional[Tuple[str, int]]:
347
+ """Execute a list of action statements, returning a redirect if one fires.
348
+
349
+ This used to be a hand-maintained if-elif chain listing the node types
350
+ an action was allowed to contain — Redirect, Flash, Set, Python, If —
351
+ with `QueryNode: pass # TODO` and NO else. Everything it did not
352
+ recognise was dropped without a word:
353
+
354
+ - q:query inside q:action did nothing at all, so a form handler
355
+ could not write to the database. That is the entire point of an
356
+ action.
357
+ - q:mail, q:log, q:file, q:dump, q:invoke, q:loop, q:llm — all
358
+ silently ignored, while the action returned HTTP 302 as if it had
359
+ worked.
360
+
361
+ Now everything except the two control-flow nodes goes through the SAME
362
+ ExecutorRegistry the rest of the engine uses, so an action can contain
363
+ whatever a component can, and anything genuinely unhandled is LOGGED
364
+ rather than dropped.
365
+ """
366
+ for statement in statements:
367
+ result = self._execute_one(statement, context)
368
+ if result:
369
+ return result
370
+ return None
371
+
372
+ def _execute_one(
373
+ self,
374
+ statement,
375
+ context: ExecutionContext
376
+ ) -> Optional[Tuple[str, int]]:
377
+ """Execute one statement; return redirect info if it was a redirect."""
378
+
379
+ # --- control flow that only makes sense inside an action ---
380
+ if isinstance(statement, RedirectNode):
381
+ url = self._resolve_databinding(statement.url, context)
382
+ if statement.flash:
383
+ flash_msg = self._resolve_databinding(statement.flash, context)
384
+ # flashType on q:redirect used to be ignored and hardcoded to
385
+ # 'success', so a failed login's message rendered green.
386
+ self._set_flash_message(
387
+ flash_msg, getattr(statement, 'flash_type', None) or 'success'
388
+ )
389
+ return url, statement.status
390
+
391
+ if isinstance(statement, FlashNode):
392
+ message = self._resolve_databinding(statement.message, context)
393
+ self._set_flash_message(message, statement.flash_type)
394
+ return None
395
+
396
+ if isinstance(statement, IfNode):
397
+ return self._execute_if(statement, context)
398
+
399
+ # --- everything else: the same registry the rest of the engine uses ---
400
+ registry = getattr(self.component_runtime, 'executor_registry', None)
401
+ if registry is not None and registry.can_execute(statement):
402
+ registry.execute(statement, context)
403
+ return None
404
+
405
+ if isinstance(statement, PythonNode):
406
+ # q:python has its own bridge here (q.form), and is gated by
407
+ # security.python_scripting.
408
+ self._execute_python(statement, context)
409
+ return None
410
+
411
+ # Render-only nodes legitimately do nothing in an action body.
412
+ if isinstance(statement, (HTMLNode, TextNode, CommentNode)):
413
+ return None
414
+
415
+ # Never drop something silently again.
416
+ logger.warning(
417
+ "q:action: no executor for %s — statement ignored. If this tag is "
418
+ "meant to work inside an action, it needs an executor in the "
419
+ "registry.", type(statement).__name__
420
+ )
421
+ return None
422
+
423
+ def _execute_if(
424
+ self,
425
+ statement: IfNode,
426
+ context: ExecutionContext
427
+ ) -> Optional[Tuple[str, int]]:
428
+ """q:if inside an action, including elseif/else."""
429
+ evaluate = self.component_runtime._evaluate_condition
430
+
431
+ if evaluate(statement.condition, context.get_all_variables()):
432
+ return self._execute_statements(statement.if_body, context)
433
+
434
+ # elseif_blocks are DICTS ({"condition":..., "body":...}). This used to
435
+ # unpack them as 2-tuples, which iterates the dict's KEYS — so the
436
+ # literal strings "condition" and "body" were used as the condition
437
+ # and the body. The same dict-versus-object mismatch that crashed the
438
+ # renderer's q:elseif.
439
+ for block in (statement.elseif_blocks or []):
440
+ if isinstance(block, dict):
441
+ cond, body = block.get('condition'), block.get('body')
442
+ else:
443
+ cond, body = getattr(block, 'condition', None), getattr(block, 'body', None)
444
+ if cond is not None and evaluate(cond, context.get_all_variables()):
445
+ return self._execute_statements(body or [], context)
446
+
447
+ return self._execute_statements(statement.else_body or [], context)
448
+
449
+ def _execute_python(self, node: PythonNode, context: ExecutionContext):
450
+ """
451
+ Execute a q:python block within an action.
452
+
453
+ Uses the same QuantumBridge pattern as the PythonExecutor
454
+ so that q.form, q.variable etc. work inside actions.
455
+ """
456
+ from quantum.runtime.executors.scripting.python_executor import QuantumBridge
457
+
458
+ # Honour security.python_scripting, the same as q:python outside an
459
+ # action. This check was missing, so the switch an operator flips to
460
+ # stop in-process Python execution could be walked around by putting
461
+ # the same q:python block inside a q:action — and a security switch
462
+ # that does not switch everything off is worse than none, because the
463
+ # operator believes they are covered. Found by an independent
464
+ # verification pass over this session's claims.
465
+ if not self.component_runtime.services.python_scripting_enabled:
466
+ raise ComponentExecutionError(
467
+ "q:python is disabled by security.python_scripting=false in "
468
+ "quantum.config.yaml (reached through q:action)"
469
+ )
470
+
471
+ all_vars = context.get_all_variables()
472
+
473
+ # Make form data available via q.form
474
+ form_data = {}
475
+ try:
476
+ form_data = request.form.to_dict()
477
+ except Exception:
478
+ pass
479
+ all_vars['form'] = form_data
480
+
481
+ bridge = QuantumBridge(context, all_vars)
482
+ bridge._exports['form'] = form_data
483
+
484
+ namespace = {
485
+ 'q': bridge,
486
+ '__quantum_context__': all_vars,
487
+ }
488
+ namespace.update(all_vars)
489
+
490
+ # Dedented, for the same reason as q:python's own executor: .strip()
491
+ # unindents only the first line, so markup-indented code raised
492
+ # "unexpected indent" on line 2.
493
+ from quantum.runtime.executors.scripting.python_executor import (
494
+ normalise_python_source,
495
+ )
496
+ exec(normalise_python_source(node.code), namespace)
497
+
498
+ # Export bridge changes back to context
499
+ for key, value in bridge._exports.items():
500
+ context.set_variable(key, value, scope="component")
501
+
502
+ def _resolve_databinding(self, text: str, context: ExecutionContext) -> str:
503
+ """Resolve {expressions} in a redirect URL or flash message (ACT-3).
504
+
505
+ This looked each {name} up with get_variable, so anything but a plain
506
+ name failed — `flash="{result.error}"` or `{len(itens)} saved` ended
507
+ the action with "Variable 'result.error' not found in any scope" and a
508
+ 500. It now goes through the same evaluator as every q: attribute.
509
+ """
510
+ if not text or '{' not in text:
511
+ return text
512
+ valor = self.component_runtime._apply_databinding(text, context.get_all_variables())
513
+ return '' if valor is None else str(valor)
514
+
515
+ def _set_flash_message(self, message: str, flash_type: str = 'info'):
516
+ """Set flash message in session"""
517
+ if 'flash' not in session:
518
+ session['flash'] = {}
519
+
520
+ session['flash'] = {
521
+ 'message': message,
522
+ 'type': flash_type
523
+ }
524
+ session.modified = True
525
+
526
+ def get_flash_message(self) -> Optional[Dict[str, str]]:
527
+ """Get and clear flash message from session"""
528
+ if 'flash' in session:
529
+ flash = session.pop('flash')
530
+ session.modified = True
531
+ return flash
532
+ return None
@@ -0,0 +1,88 @@
1
+ """
2
+ Message Broker Adapters
3
+
4
+ Provides implementations of MessageBroker for different backends:
5
+ - MemoryAdapter: In-memory implementation for testing and development
6
+ - RedisAdapter: Redis pub/sub and list-based queues
7
+ - RabbitMQAdapter: Full-featured AMQP implementation
8
+
9
+ Redis and RabbitMQ adapters are imported lazily to avoid blocking
10
+ startup when their dependencies (pika, redis) are slow to load.
11
+ """
12
+
13
+ from .memory_adapter import MemoryAdapter
14
+
15
+
16
+ def _get_redis_adapter():
17
+ """Lazily import RedisAdapter."""
18
+ try:
19
+ from .redis_adapter import RedisAdapter
20
+ return RedisAdapter
21
+ except ImportError:
22
+ return None
23
+
24
+
25
+ def _get_rabbitmq_adapter():
26
+ """Lazily import RabbitMQAdapter."""
27
+ try:
28
+ from .rabbitmq_adapter import RabbitMQAdapter
29
+ return RabbitMQAdapter
30
+ except ImportError:
31
+ return None
32
+
33
+
34
+ def get_adapter(adapter_type: str = 'memory'):
35
+ """
36
+ Factory function to get the appropriate message broker adapter.
37
+
38
+ Args:
39
+ adapter_type: 'memory', 'redis', or 'rabbitmq'
40
+
41
+ Returns:
42
+ MessageBroker instance
43
+
44
+ Raises:
45
+ ImportError: If required dependencies are not installed
46
+ ValueError: If adapter_type is not recognized
47
+ """
48
+ if adapter_type == 'memory':
49
+ return MemoryAdapter()
50
+ elif adapter_type == 'sqlite':
51
+ # The durable, server-less broker. It existed but was reachable only
52
+ # from `quantum mq`: MESSAGE_BROKER_TYPE=sqlite raised "Unknown
53
+ # adapter type", so the documented way to ask for it did not work.
54
+ from quantum.runtime.adapters.sqlite_adapter import SqliteAdapter
55
+ return SqliteAdapter()
56
+ elif adapter_type == 'redis':
57
+ RedisAdapter = _get_redis_adapter()
58
+ if RedisAdapter is None:
59
+ raise ImportError(
60
+ "Redis adapter requires 'redis' package. "
61
+ "Install with: pip install redis"
62
+ )
63
+ return RedisAdapter()
64
+ elif adapter_type == 'rabbitmq':
65
+ RabbitMQAdapter = _get_rabbitmq_adapter()
66
+ if RabbitMQAdapter is None:
67
+ raise ImportError(
68
+ "RabbitMQ adapter requires 'pika' package. "
69
+ "Install with: pip install pika"
70
+ )
71
+ return RabbitMQAdapter()
72
+ else:
73
+ raise ValueError(
74
+ f"Unknown adapter type: {adapter_type}. "
75
+ f"Known: memory, sqlite, redis, rabbitmq"
76
+ )
77
+
78
+
79
+ def __getattr__(name):
80
+ """Lazy access to RedisAdapter and RabbitMQAdapter by name."""
81
+ if name == 'RedisAdapter':
82
+ return _get_redis_adapter()
83
+ if name == 'RabbitMQAdapter':
84
+ return _get_rabbitmq_adapter()
85
+ raise AttributeError(f"module 'runtime.adapters' has no attribute {name!r}")
86
+
87
+
88
+ __all__ = ['MemoryAdapter', 'RedisAdapter', 'RabbitMQAdapter', 'get_adapter']