quantum-framework 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -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 +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,28 @@
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
+ if name == 'QuantumAPIServer':
26
+ from .api_server import QuantumAPIServer
27
+ return QuantumAPIServer
28
+ raise AttributeError(f"module 'runtime' has no attribute {name!r}")
@@ -0,0 +1,443 @@
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
+ pass
29
+
30
+
31
+ class ActionHandler:
32
+ """
33
+ Handles server-side action execution.
34
+
35
+ Workflow:
36
+ 1. Extract form data from request
37
+ 2. Validate against q:param definitions
38
+ 3. Execute action body statements
39
+ 4. Handle flash messages
40
+ 5. Perform redirect
41
+ """
42
+
43
+ def __init__(self, component_runtime: Optional[ComponentRuntime] = None):
44
+ self.component_runtime = component_runtime or ComponentRuntime()
45
+
46
+ def handle_action(
47
+ self,
48
+ action: ActionNode,
49
+ context: Optional[ExecutionContext] = None
50
+ ) -> Tuple[Optional[str], int]:
51
+ """
52
+ Handle action execution.
53
+
54
+ Returns:
55
+ Tuple[redirect_url, status_code] or None if no redirect
56
+ """
57
+
58
+ # Use provided context or create new one, seeded with the live Flask session
59
+ # so `q:set name="session.x"` persists and require_auth sees it on the next
60
+ # request. Without this, an action's ExecutionContext started empty and any
61
+ # session mutation made during the action was silently discarded.
62
+ owns_context = context is None
63
+ if context is None:
64
+ context = ExecutionContext()
65
+ context.session_vars = session.get('quantum_session', {})
66
+
67
+ try:
68
+ # 1. Extract form data
69
+ form_data = self._extract_form_data()
70
+
71
+ # 2. Validate parameters
72
+ validated_params = self._validate_parameters(action.params, form_data)
73
+
74
+ # 3. Add validated params to context
75
+ for key, value in validated_params.items():
76
+ context.set_variable(key, value)
77
+
78
+ # Os arquivos enviados entram no contexto mesmo sem um q:param
79
+ # que os declare. `_extract_form_data` ja os coleta e a
80
+ # documentacao promete `q.files`, mas so os parametros validados
81
+ # chegavam ao contexto — entao `<q:file action="upload"
82
+ # file="{avatar}">` sem um `<q:param name="avatar">` acima dizia
83
+ # "File variable 'avatar' not found". Um q:param declarado ganha:
84
+ # ele passou pela validacao.
85
+ for key, value in form_data.items():
86
+ if key in validated_params:
87
+ continue
88
+ if key == 'files' or param_validation.is_uploaded_file(value):
89
+ context.set_variable(key, value)
90
+
91
+ # 4. Execute action body
92
+ redirect_info = self._execute_action_body(action, context)
93
+
94
+ # 5. Sync session mutations back to Flask (mirrors web_server.py's
95
+ # render path, which does the same after execute_component()).
96
+ if owns_context:
97
+ session['quantum_session'] = context.session_vars
98
+ session.modified = True
99
+
100
+ # 6. Return redirect if present
101
+ if redirect_info:
102
+ return redirect_info
103
+
104
+ return None, 200
105
+
106
+ except ValidationError as e:
107
+ # Validation failed - set error flash and redirect back
108
+ self._set_flash_message(str(e), 'error')
109
+ # Redirect to referer or home
110
+ referer = request.referrer or '/'
111
+ return referer, 302
112
+
113
+ except Exception as e:
114
+ # Unexpected error. Sync whatever session state changed before the
115
+ # failure, and report a real error instead of a redirect disguised
116
+ # as one (a 500 with a Location header confuses clients into
117
+ # thinking the action succeeded).
118
+ #
119
+ # And LOG it, with the traceback. The 500 page tells the user to
120
+ # "check server logs for details" and the logs recorded absolutely
121
+ # nothing — the failure existed only as a flash message the user
122
+ # might never see. Whoever is debugging gets the action name, the
123
+ # exception and the stack.
124
+ logger.exception(
125
+ "q:action %r failed: %s", getattr(action, 'name', '?'), e
126
+ )
127
+ if owns_context:
128
+ session['quantum_session'] = context.session_vars
129
+ session.modified = True
130
+ self._set_flash_message(f'Action error: {str(e)}', 'error')
131
+ return None, 500
132
+
133
+ def _extract_form_data(self) -> Dict[str, Any]:
134
+ """Extract data from POST/PUT/DELETE request, uploaded files included.
135
+
136
+ This returned request.form only. Uploaded files never entered the
137
+ context, so
138
+
139
+ <q:file action="upload" file="{avatar}" destination="..." />
140
+
141
+ raised "File variable 'avatar' not found" for every web request:
142
+ q:file action="upload" could not be reached from a browser at all,
143
+ which is the only place a file upload comes from.
144
+ """
145
+ data: Dict[str, Any] = {}
146
+
147
+ if request.method in ['POST', 'PUT', 'PATCH']:
148
+ if request.is_json:
149
+ data = request.get_json() or {}
150
+ else:
151
+ data = request.form.to_dict()
152
+
153
+ elif request.method == 'DELETE':
154
+ if request.is_json:
155
+ data = request.get_json() or {}
156
+
157
+ try:
158
+ uploaded = request.files
159
+ except Exception:
160
+ uploaded = None
161
+
162
+ if uploaded:
163
+ files = {}
164
+ for field, storage in uploaded.items(multi=False):
165
+ if not storage or not storage.filename:
166
+ continue # an empty file input posts a blank part
167
+ files[field] = storage
168
+ if field in data:
169
+ logger.warning(
170
+ "the upload field %r has the same name as a form "
171
+ "field; the file wins", field)
172
+ data[field] = storage
173
+ if files:
174
+ # Also as a group, which is what q.files documents.
175
+ data.setdefault('files', files)
176
+
177
+ return data
178
+
179
+ def _validate_parameters(
180
+ self,
181
+ params: List[QuantumParam],
182
+ form_data: Dict[str, Any]
183
+ ) -> Dict[str, Any]:
184
+ """
185
+ Validate form data against q:param definitions.
186
+
187
+ Raises ValidationError if validation fails.
188
+ """
189
+
190
+ validated = {}
191
+
192
+ for param in params:
193
+ value = form_data.get(param.name)
194
+
195
+ # Check required
196
+ if param.required and (value is None or value == ''):
197
+ raise ValidationError(f"Parameter '{param.name}' is required")
198
+
199
+ # Skip validation if optional and not provided
200
+ if value is None or value == '':
201
+ if param.default is not None:
202
+ validated[param.name] = param.default
203
+ continue
204
+
205
+ # Type validation and conversion
206
+ validated_value = self._validate_type(param, value)
207
+
208
+ # Additional validation rules
209
+ self._validate_rules(param, validated_value)
210
+
211
+ validated[param.name] = validated_value
212
+
213
+ return validated
214
+
215
+ def _validate_type(self, param: QuantumParam, value: Any) -> Any:
216
+ """Converte o valor para o tipo declarado, ou levanta ValidationError.
217
+
218
+ Delega para quantum.runtime.param_validation, a MESMA implementacao
219
+ que o ComponentRuntime usa. Havia duas copias divergentes: esta so
220
+ conhecia `integer`, `decimal` e `float`, e o resto caia num
221
+ `return str(value)` final — `type="number"` (32 usos no repositorio)
222
+ virava string sem checar min/max, e `type="file"` (5 usos, incluindo
223
+ components/upload_demo.q) transformava o objeto de upload na sua
224
+ repr, `"<FileStorage: 'foto.png' ('image/png')>"`, que era o que o
225
+ <q:file action="upload"> recebia no lugar do arquivo.
226
+ """
227
+ convertido, erro = param_validation.coerce(param, value)
228
+ if erro:
229
+ raise ValidationError(erro)
230
+ return convertido
231
+
232
+ def _validate_rules(self, param: QuantumParam, value: Any):
233
+ """min/max/minlength/maxlength/pattern/enum — a mesma implementacao
234
+ que o ComponentRuntime aplica."""
235
+ erros = param_validation.check_rules(param, value)
236
+ if erros:
237
+ raise ValidationError(erros[0])
238
+
239
+ def _execute_action_body(
240
+ self,
241
+ action: ActionNode,
242
+ context: ExecutionContext
243
+ ) -> Optional[Tuple[str, int]]:
244
+ """
245
+ Execute action body statements.
246
+
247
+ Returns redirect info if redirect encountered, None otherwise.
248
+ """
249
+
250
+ return self._execute_statements(action.body, context)
251
+
252
+ def _execute_statements(
253
+ self,
254
+ statements,
255
+ context: ExecutionContext
256
+ ) -> Optional[Tuple[str, int]]:
257
+ """Execute a list of action statements, returning a redirect if one fires.
258
+
259
+ This used to be a hand-maintained if-elif chain listing the node types
260
+ an action was allowed to contain — Redirect, Flash, Set, Python, If —
261
+ with `QueryNode: pass # TODO` and NO else. Everything it did not
262
+ recognise was dropped without a word:
263
+
264
+ - q:query inside q:action did nothing at all, so a form handler
265
+ could not write to the database. That is the entire point of an
266
+ action.
267
+ - q:mail, q:log, q:file, q:dump, q:invoke, q:loop, q:llm — all
268
+ silently ignored, while the action returned HTTP 302 as if it had
269
+ worked.
270
+
271
+ Now everything except the two control-flow nodes goes through the SAME
272
+ ExecutorRegistry the rest of the engine uses, so an action can contain
273
+ whatever a component can, and anything genuinely unhandled is LOGGED
274
+ rather than dropped.
275
+ """
276
+ for statement in statements:
277
+ result = self._execute_one(statement, context)
278
+ if result:
279
+ return result
280
+ return None
281
+
282
+ def _execute_one(
283
+ self,
284
+ statement,
285
+ context: ExecutionContext
286
+ ) -> Optional[Tuple[str, int]]:
287
+ """Execute one statement; return redirect info if it was a redirect."""
288
+
289
+ # --- control flow that only makes sense inside an action ---
290
+ if isinstance(statement, RedirectNode):
291
+ url = self._resolve_databinding(statement.url, context)
292
+ if statement.flash:
293
+ flash_msg = self._resolve_databinding(statement.flash, context)
294
+ # flashType on q:redirect used to be ignored and hardcoded to
295
+ # 'success', so a failed login's message rendered green.
296
+ self._set_flash_message(
297
+ flash_msg, getattr(statement, 'flash_type', None) or 'success'
298
+ )
299
+ return url, statement.status
300
+
301
+ if isinstance(statement, FlashNode):
302
+ message = self._resolve_databinding(statement.message, context)
303
+ self._set_flash_message(message, statement.flash_type)
304
+ return None
305
+
306
+ if isinstance(statement, IfNode):
307
+ return self._execute_if(statement, context)
308
+
309
+ # --- everything else: the same registry the rest of the engine uses ---
310
+ registry = getattr(self.component_runtime, 'executor_registry', None)
311
+ if registry is not None and registry.can_execute(statement):
312
+ registry.execute(statement, context)
313
+ return None
314
+
315
+ if isinstance(statement, PythonNode):
316
+ # q:python has its own bridge here (q.form), and is gated by
317
+ # security.python_scripting.
318
+ self._execute_python(statement, context)
319
+ return None
320
+
321
+ # Render-only nodes legitimately do nothing in an action body.
322
+ if isinstance(statement, (HTMLNode, TextNode, CommentNode)):
323
+ return None
324
+
325
+ # Never drop something silently again.
326
+ logger.warning(
327
+ "q:action: no executor for %s — statement ignored. If this tag is "
328
+ "meant to work inside an action, it needs an executor in the "
329
+ "registry.", type(statement).__name__
330
+ )
331
+ return None
332
+
333
+ def _execute_if(
334
+ self,
335
+ statement: IfNode,
336
+ context: ExecutionContext
337
+ ) -> Optional[Tuple[str, int]]:
338
+ """q:if inside an action, including elseif/else."""
339
+ evaluate = self.component_runtime._evaluate_condition
340
+
341
+ if evaluate(statement.condition, context.get_all_variables()):
342
+ return self._execute_statements(statement.if_body, context)
343
+
344
+ # elseif_blocks are DICTS ({"condition":..., "body":...}). This used to
345
+ # unpack them as 2-tuples, which iterates the dict's KEYS — so the
346
+ # literal strings "condition" and "body" were used as the condition
347
+ # and the body. The same dict-versus-object mismatch that crashed the
348
+ # renderer's q:elseif.
349
+ for block in (statement.elseif_blocks or []):
350
+ if isinstance(block, dict):
351
+ cond, body = block.get('condition'), block.get('body')
352
+ else:
353
+ cond, body = getattr(block, 'condition', None), getattr(block, 'body', None)
354
+ if cond is not None and evaluate(cond, context.get_all_variables()):
355
+ return self._execute_statements(body or [], context)
356
+
357
+ return self._execute_statements(statement.else_body or [], context)
358
+
359
+ def _execute_python(self, node: PythonNode, context: ExecutionContext):
360
+ """
361
+ Execute a q:python block within an action.
362
+
363
+ Uses the same QuantumBridge pattern as the PythonExecutor
364
+ so that q.form, q.variable etc. work inside actions.
365
+ """
366
+ from quantum.runtime.executors.scripting.python_executor import QuantumBridge
367
+
368
+ # Honour security.python_scripting, the same as q:python outside an
369
+ # action. This check was missing, so the switch an operator flips to
370
+ # stop in-process Python execution could be walked around by putting
371
+ # the same q:python block inside a q:action — and a security switch
372
+ # that does not switch everything off is worse than none, because the
373
+ # operator believes they are covered. Found by an independent
374
+ # verification pass over this session's claims.
375
+ if not self.component_runtime.services.python_scripting_enabled:
376
+ raise ComponentExecutionError(
377
+ "q:python is disabled by security.python_scripting=false in "
378
+ "quantum.config.yaml (reached through q:action)"
379
+ )
380
+
381
+ all_vars = context.get_all_variables()
382
+
383
+ # Make form data available via q.form
384
+ form_data = {}
385
+ try:
386
+ form_data = request.form.to_dict()
387
+ except Exception:
388
+ pass
389
+ all_vars['form'] = form_data
390
+
391
+ bridge = QuantumBridge(context, all_vars)
392
+ bridge._exports['form'] = form_data
393
+
394
+ namespace = {
395
+ 'q': bridge,
396
+ '__quantum_context__': all_vars,
397
+ }
398
+ namespace.update(all_vars)
399
+
400
+ # Dedented, for the same reason as q:python's own executor: .strip()
401
+ # unindents only the first line, so markup-indented code raised
402
+ # "unexpected indent" on line 2.
403
+ from quantum.runtime.executors.scripting.python_executor import (
404
+ normalise_python_source,
405
+ )
406
+ exec(normalise_python_source(node.code), namespace)
407
+
408
+ # Export bridge changes back to context
409
+ for key, value in bridge._exports.items():
410
+ context.set_variable(key, value, scope="component")
411
+
412
+ def _resolve_databinding(self, text: str, context: ExecutionContext) -> str:
413
+ """Resolve {variable} databinding in text"""
414
+ if not text or '{' not in text:
415
+ return text
416
+
417
+ import re
418
+
419
+ def replace_var(match):
420
+ var_name = match.group(1)
421
+ value = context.get_variable(var_name)
422
+ return str(value) if value is not None else ''
423
+
424
+ return re.sub(r'\{([^}]+)\}', replace_var, text)
425
+
426
+ def _set_flash_message(self, message: str, flash_type: str = 'info'):
427
+ """Set flash message in session"""
428
+ if 'flash' not in session:
429
+ session['flash'] = {}
430
+
431
+ session['flash'] = {
432
+ 'message': message,
433
+ 'type': flash_type
434
+ }
435
+ session.modified = True
436
+
437
+ def get_flash_message(self) -> Optional[Dict[str, str]]:
438
+ """Get and clear flash message from session"""
439
+ if 'flash' in session:
440
+ flash = session.pop('flash')
441
+ session.modified = True
442
+ return flash
443
+ 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']