quantum-framework 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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']
|