quantum-framework 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,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']
|