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,470 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service Container - Dependency injection for runtime services
|
|
3
|
+
|
|
4
|
+
Centralizes initialization and access to all runtime services
|
|
5
|
+
(database, email, LLM, etc.) instead of having them scattered
|
|
6
|
+
across ComponentRuntime.__init__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from quantum.core.config_env import ConfigEnvError, expand_env
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from quantum.runtime.database_service import DatabaseService
|
|
18
|
+
from quantum.runtime.llm_service import LLMService
|
|
19
|
+
from quantum.runtime.knowledge_service import KnowledgeService
|
|
20
|
+
from quantum.runtime.message_queue_service import MessageQueueService
|
|
21
|
+
from quantum.runtime.job_executor import JobExecutor
|
|
22
|
+
from quantum.runtime.file_upload_service import FileUploadService
|
|
23
|
+
from quantum.runtime.email_service import EmailService
|
|
24
|
+
from quantum.runtime.function_registry import FunctionRegistry
|
|
25
|
+
from quantum.core.features.invocation.src.runtime import InvocationService
|
|
26
|
+
from quantum.core.features.data_import.src.runtime import DataImportService
|
|
27
|
+
from quantum.core.features.logging.src import LoggingService
|
|
28
|
+
from quantum.core.features.dump.src import DumpService
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_DEFAULT_CONFIG_FILE = 'quantum.config.yaml'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_config_cache: Dict[str, Any] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_project_config(config_path: str = _DEFAULT_CONFIG_FILE) -> Dict[str, Any]:
|
|
39
|
+
"""Read quantum.config.yaml from the working directory, if it is there.
|
|
40
|
+
|
|
41
|
+
Missing file: empty configuration, no complaint — a .q file that uses no
|
|
42
|
+
datasource needs none. Unreadable or invalid file: empty configuration
|
|
43
|
+
and a warning, because silence there is how a typo in the YAML turns
|
|
44
|
+
into "datasource not declared" three layers down.
|
|
45
|
+
|
|
46
|
+
Cached by (path, mtime, size). Every ComponentRuntime() built without a
|
|
47
|
+
config re-read and re-parsed the file — 200 runtimes meant 200 disk reads
|
|
48
|
+
— and the suite builds thousands. Keying on mtime keeps the property that
|
|
49
|
+
matters: edit the file and the next runtime sees the change.
|
|
50
|
+
|
|
51
|
+
A copy is returned, not the cached dict: services mutate their config,
|
|
52
|
+
and handing out the same object would let one runtime's changes leak into
|
|
53
|
+
the next.
|
|
54
|
+
"""
|
|
55
|
+
import os
|
|
56
|
+
try:
|
|
57
|
+
stat = os.stat(config_path)
|
|
58
|
+
except OSError:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
key = (os.path.abspath(config_path), stat.st_mtime_ns, stat.st_size)
|
|
62
|
+
cached = _config_cache.get(key)
|
|
63
|
+
if cached is not None:
|
|
64
|
+
return copy.deepcopy(cached)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
import yaml
|
|
68
|
+
with open(config_path, 'r', encoding='utf-8') as handle:
|
|
69
|
+
loaded = yaml.safe_load(handle)
|
|
70
|
+
if loaded is None:
|
|
71
|
+
loaded = {}
|
|
72
|
+
if not isinstance(loaded, dict):
|
|
73
|
+
logger.warning("%s does not contain a mapping; ignoring it",
|
|
74
|
+
config_path)
|
|
75
|
+
loaded = {}
|
|
76
|
+
loaded = expand_env(loaded)
|
|
77
|
+
except ConfigEnvError:
|
|
78
|
+
raise
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
logger.warning("could not read %s: %s", config_path, exc)
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
_config_cache.clear() # uma entrada basta; o arquivo e um so
|
|
84
|
+
_config_cache[key] = loaded
|
|
85
|
+
return copy.deepcopy(loaded)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ServiceContainer:
|
|
89
|
+
"""
|
|
90
|
+
Container for all runtime services.
|
|
91
|
+
|
|
92
|
+
Provides lazy initialization and centralized access to services.
|
|
93
|
+
Services are initialized on first access to avoid unnecessary overhead.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
services = ServiceContainer(config)
|
|
97
|
+
db = services.database # Initialized on first access
|
|
98
|
+
llm = services.llm
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, config: Dict[str, Any] = None):
|
|
102
|
+
"""
|
|
103
|
+
Initialize service container.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
config: Configuration dictionary with service-specific settings.
|
|
107
|
+
Omit it (None) to read quantum.config.yaml from the working
|
|
108
|
+
directory; pass {} for a deliberately empty configuration.
|
|
109
|
+
|
|
110
|
+
`None` used to mean "no configuration", so anything that built a
|
|
111
|
+
runtime without passing one — the .q test plugin, library callers,
|
|
112
|
+
the admin — could not see the `datasources:` declared in
|
|
113
|
+
quantum.config.yaml, and every q:query and q:transaction fell through
|
|
114
|
+
to the optional Admin API at localhost:8000 and failed with a
|
|
115
|
+
connection error. `quantum run` was fixed to load the file; that fix
|
|
116
|
+
did not reach anybody else. Now the default is the same either way,
|
|
117
|
+
and passing {} still means empty.
|
|
118
|
+
"""
|
|
119
|
+
self._config = load_project_config() if config is None else config
|
|
120
|
+
self._services: Dict[str, Any] = {}
|
|
121
|
+
|
|
122
|
+
# ==========================================================================
|
|
123
|
+
# Core Services
|
|
124
|
+
# ==========================================================================
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def database(self) -> 'DatabaseService':
|
|
128
|
+
"""Database service for query execution"""
|
|
129
|
+
if 'database' not in self._services:
|
|
130
|
+
from quantum.runtime.database_service import DatabaseService
|
|
131
|
+
local_ds = self._config.get('datasources', {})
|
|
132
|
+
self._services['database'] = DatabaseService(local_datasources=local_ds)
|
|
133
|
+
logger.debug("Initialized DatabaseService")
|
|
134
|
+
return self._services['database']
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def function_registry(self) -> 'FunctionRegistry':
|
|
138
|
+
"""Function registry for q:function"""
|
|
139
|
+
if 'function_registry' not in self._services:
|
|
140
|
+
from quantum.runtime.function_registry import FunctionRegistry
|
|
141
|
+
self._services['function_registry'] = FunctionRegistry()
|
|
142
|
+
logger.debug("Initialized FunctionRegistry")
|
|
143
|
+
return self._services['function_registry']
|
|
144
|
+
|
|
145
|
+
# ==========================================================================
|
|
146
|
+
# Invocation & Data Services
|
|
147
|
+
# ==========================================================================
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def invocation(self) -> 'InvocationService':
|
|
151
|
+
"""Invocation service for q:invoke"""
|
|
152
|
+
if 'invocation' not in self._services:
|
|
153
|
+
from quantum.core.features.invocation.src.runtime import InvocationService
|
|
154
|
+
self._services['invocation'] = InvocationService()
|
|
155
|
+
logger.debug("Initialized InvocationService")
|
|
156
|
+
return self._services['invocation']
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def data_import(self) -> 'DataImportService':
|
|
160
|
+
"""Data import service for q:data"""
|
|
161
|
+
if 'data_import' not in self._services:
|
|
162
|
+
from quantum.core.features.data_import.src.runtime import DataImportService
|
|
163
|
+
self._services['data_import'] = DataImportService()
|
|
164
|
+
logger.debug("Initialized DataImportService")
|
|
165
|
+
return self._services['data_import']
|
|
166
|
+
|
|
167
|
+
# ==========================================================================
|
|
168
|
+
# AI Services
|
|
169
|
+
# ==========================================================================
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def llm(self) -> 'LLMService':
|
|
173
|
+
"""Ollama-only LLM service, used by q:knowledge and the RAG path.
|
|
174
|
+
|
|
175
|
+
It used to be constructed with NO arguments, so it fell back to its own
|
|
176
|
+
default of http://localhost:11434 and ignored llm.base_url entirely —
|
|
177
|
+
while multi_llm (q:llm, q:agent, q:team) honoured it. The two halves of
|
|
178
|
+
the AI stack therefore pointed at DIFFERENT Ollama servers.
|
|
179
|
+
|
|
180
|
+
Measured on this machine: config says http://localhost:11434, q:llm
|
|
181
|
+
reached that host and answered, and q:knowledge silently went to
|
|
182
|
+
localhost. In a deployment where only the configured host has models,
|
|
183
|
+
q:llm works and RAG fails, with nothing explaining why.
|
|
184
|
+
"""
|
|
185
|
+
if 'llm' not in self._services:
|
|
186
|
+
from quantum.runtime.llm_service import LLMService
|
|
187
|
+
llm_cfg = self._config.get('llm', {}) or {}
|
|
188
|
+
self._services['llm'] = LLMService(
|
|
189
|
+
base_url=self._llm_base_url(),
|
|
190
|
+
default_model=llm_cfg.get('default_model'),
|
|
191
|
+
timeout=llm_cfg.get('timeout', 60),
|
|
192
|
+
)
|
|
193
|
+
logger.debug("Initialized LLMService")
|
|
194
|
+
return self._services['llm']
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def knowledge(self) -> 'KnowledgeService':
|
|
198
|
+
"""Knowledge service for RAG/ChromaDB"""
|
|
199
|
+
if 'knowledge' not in self._services:
|
|
200
|
+
from quantum.runtime.knowledge_service import KnowledgeService
|
|
201
|
+
self._services['knowledge'] = KnowledgeService(self.llm)
|
|
202
|
+
logger.debug("Initialized KnowledgeService")
|
|
203
|
+
return self._services['knowledge']
|
|
204
|
+
|
|
205
|
+
def _llm_base_url(self):
|
|
206
|
+
"""Where the model server is: QUANTUM_LLM_BASE_URL, then llm.base_url, then the default.
|
|
207
|
+
|
|
208
|
+
One answer for every AI tag. They disagreed: q:llm read llm.base_url from
|
|
209
|
+
quantum.config.yaml, while q:agent and q:team built their own service
|
|
210
|
+
with no config and read only the environment. Measured with the repo's
|
|
211
|
+
config (localhost) and QUANTUM_LLM_BASE_URL pointing at another host,
|
|
212
|
+
in one program: q:llm went to the local Ollama and failed with "model
|
|
213
|
+
'phi3' not found", q:agent went to the other host and answered.
|
|
214
|
+
|
|
215
|
+
The environment wins over the file, so an operator can point a
|
|
216
|
+
deployment at another server without editing the project.
|
|
217
|
+
"""
|
|
218
|
+
llm_cfg = self._config.get('llm', {}) or {}
|
|
219
|
+
return os.environ.get('QUANTUM_LLM_BASE_URL') or llm_cfg.get('base_url')
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def agent(self):
|
|
223
|
+
"""Agent service for q:agent — on this container's model server."""
|
|
224
|
+
if 'agent' not in self._services:
|
|
225
|
+
from quantum.runtime.agent_service import AgentService
|
|
226
|
+
service = AgentService(self.llm)
|
|
227
|
+
# Built by the global get_agent_service() it made its own
|
|
228
|
+
# MultiProviderLLMService with no config; share this container's.
|
|
229
|
+
service._multi_llm_service = self.multi_llm
|
|
230
|
+
self._services['agent'] = service
|
|
231
|
+
logger.debug("Initialized AgentService")
|
|
232
|
+
return self._services['agent']
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def multi_agent(self):
|
|
236
|
+
"""Multi-agent service for q:team — on the same agent service."""
|
|
237
|
+
if 'multi_agent' not in self._services:
|
|
238
|
+
from quantum.runtime.agent_service import MultiAgentService
|
|
239
|
+
self._services['multi_agent'] = MultiAgentService(self.agent)
|
|
240
|
+
logger.debug("Initialized MultiAgentService")
|
|
241
|
+
return self._services['multi_agent']
|
|
242
|
+
|
|
243
|
+
# ==========================================================================
|
|
244
|
+
# Communication Services
|
|
245
|
+
# ==========================================================================
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def message_queue(self) -> 'MessageQueueService':
|
|
249
|
+
"""Message queue service for q:message, q:subscribe"""
|
|
250
|
+
if 'message_queue' not in self._services:
|
|
251
|
+
from quantum.runtime.message_queue_service import MessageQueueService
|
|
252
|
+
mq_config = self._config.get('message_queue', {})
|
|
253
|
+
self._services['message_queue'] = MessageQueueService(mq_config)
|
|
254
|
+
logger.debug("Initialized MessageQueueService")
|
|
255
|
+
return self._services['message_queue']
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def multi_llm(self) -> 'MultiProviderLLMService':
|
|
259
|
+
"""Multi-provider LLM service (Ollama, OpenAI, Anthropic, LM Studio).
|
|
260
|
+
|
|
261
|
+
q:agent already used this; q:llm was stuck on the Ollama-only
|
|
262
|
+
LLMService, which is why 'multi-provider' was only half true.
|
|
263
|
+
"""
|
|
264
|
+
if 'multi_llm' not in self._services:
|
|
265
|
+
from quantum.runtime.llm_providers import MultiProviderLLMService
|
|
266
|
+
llm_cfg = self._config.get('llm', {}) or {}
|
|
267
|
+
self._services['multi_llm'] = MultiProviderLLMService(
|
|
268
|
+
default_endpoint=self._llm_base_url(),
|
|
269
|
+
default_model=llm_cfg.get('default_model'),
|
|
270
|
+
timeout=llm_cfg.get('timeout', 60),
|
|
271
|
+
)
|
|
272
|
+
logger.debug("Initialized MultiProviderLLMService")
|
|
273
|
+
return self._services['multi_llm']
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def llm_cache(self) -> 'LLMCache':
|
|
277
|
+
"""TTL cache for q:llm responses (the `cache` attribute's backing)."""
|
|
278
|
+
if 'llm_cache' not in self._services:
|
|
279
|
+
from quantum.runtime.llm_cache import get_llm_cache
|
|
280
|
+
self._services['llm_cache'] = get_llm_cache()
|
|
281
|
+
return self._services['llm_cache']
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def config(self) -> Dict[str, Any]:
|
|
285
|
+
"""Raw configuration dict (quantum.config.yaml), for executors that
|
|
286
|
+
need policy settings rather than a service."""
|
|
287
|
+
return self._config
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def python_scripting_enabled(self) -> bool:
|
|
291
|
+
"""Whether q:python / q:pyclass / q:pyimport may execute.
|
|
292
|
+
|
|
293
|
+
q:python is a full-trust escape hatch: it runs arbitrary Python in
|
|
294
|
+
the server process with no sandbox. That is fine for code the
|
|
295
|
+
application author wrote, and unacceptable for code anything else
|
|
296
|
+
wrote — an LLM-generated or user-submitted template, a component
|
|
297
|
+
from a marketplace, a .q file someone sent you.
|
|
298
|
+
|
|
299
|
+
**Desligado por padrão.** Era ligado, o que fazia dele a última
|
|
300
|
+
falha-aberta de configuração do framework: bastava executar um .q
|
|
301
|
+
que você não escreveu para executar o Python dele. Um default
|
|
302
|
+
precisa ser seguro para quem não leu a documentação — e quem usa
|
|
303
|
+
q:python leu, porque teve de escrever a tag.
|
|
304
|
+
|
|
305
|
+
Ligue com `security.python_scripting: true` no quantum.config.yaml,
|
|
306
|
+
ou QUANTUM_PYTHON_SCRIPTING=1 no ambiente.
|
|
307
|
+
"""
|
|
308
|
+
security = self._config.get('security', {}) or {}
|
|
309
|
+
configured = security.get('python_scripting')
|
|
310
|
+
if configured is not None:
|
|
311
|
+
return configured is not False
|
|
312
|
+
|
|
313
|
+
import os
|
|
314
|
+
env = os.environ.get('QUANTUM_PYTHON_SCRIPTING', '').strip().lower()
|
|
315
|
+
return env in ('1', 'true', 'yes', 'on')
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def websocket(self) -> 'WebSocketService':
|
|
319
|
+
"""WebSocket service for q:websocket, q:send, q:close.
|
|
320
|
+
|
|
321
|
+
The service existed but was never exposed here, so every
|
|
322
|
+
q:websocket tag raised AttributeError on services.websocket.
|
|
323
|
+
"""
|
|
324
|
+
if 'websocket' not in self._services:
|
|
325
|
+
from quantum.runtime.websocket_service import get_websocket_service
|
|
326
|
+
self._services['websocket'] = get_websocket_service()
|
|
327
|
+
logger.debug("Initialized WebSocketService")
|
|
328
|
+
return self._services['websocket']
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def email(self) -> 'EmailService':
|
|
332
|
+
"""Email service for q:mail"""
|
|
333
|
+
if 'email' not in self._services:
|
|
334
|
+
from quantum.runtime.email_service import EmailService
|
|
335
|
+
self._services['email'] = EmailService()
|
|
336
|
+
logger.debug("Initialized EmailService")
|
|
337
|
+
return self._services['email']
|
|
338
|
+
|
|
339
|
+
# ==========================================================================
|
|
340
|
+
# Job & Scheduling Services
|
|
341
|
+
# ==========================================================================
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def job_executor(self) -> 'JobExecutor':
|
|
345
|
+
"""Job executor for q:schedule, q:thread, q:job"""
|
|
346
|
+
if 'job_executor' not in self._services:
|
|
347
|
+
from quantum.runtime.job_executor import JobExecutor
|
|
348
|
+
job_db_path = self._config.get('job_db_path', 'quantum_jobs.db')
|
|
349
|
+
max_workers = self._config.get('max_thread_workers', 10)
|
|
350
|
+
self._services['job_executor'] = JobExecutor(
|
|
351
|
+
max_thread_workers=max_workers,
|
|
352
|
+
job_db_path=job_db_path
|
|
353
|
+
)
|
|
354
|
+
logger.debug("Initialized JobExecutor")
|
|
355
|
+
return self._services['job_executor']
|
|
356
|
+
|
|
357
|
+
# ==========================================================================
|
|
358
|
+
# File & Upload Services
|
|
359
|
+
# ==========================================================================
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def file_upload(self) -> 'FileUploadService':
|
|
363
|
+
"""File upload service for q:file"""
|
|
364
|
+
if 'file_upload' not in self._services:
|
|
365
|
+
from quantum.runtime.file_upload_service import FileUploadService
|
|
366
|
+
# Every path q:file touches is confined to this root. Configurable
|
|
367
|
+
# via paths.uploads; ./uploads by default, which is where every
|
|
368
|
+
# destination= in this repo already points.
|
|
369
|
+
root = (self._config.get('paths', {}) or {}).get('uploads', 'uploads')
|
|
370
|
+
self._services['file_upload'] = FileUploadService(root=root)
|
|
371
|
+
logger.debug("Initialized FileUploadService")
|
|
372
|
+
return self._services['file_upload']
|
|
373
|
+
|
|
374
|
+
# ==========================================================================
|
|
375
|
+
# Developer Experience Services
|
|
376
|
+
# ==========================================================================
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def logging(self) -> 'LoggingService':
|
|
380
|
+
"""Logging service for q:log"""
|
|
381
|
+
if 'logging' not in self._services:
|
|
382
|
+
from quantum.core.features.logging.src import LoggingService
|
|
383
|
+
self._services['logging'] = LoggingService()
|
|
384
|
+
logger.debug("Initialized LoggingService")
|
|
385
|
+
return self._services['logging']
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def dump(self) -> 'DumpService':
|
|
389
|
+
"""Dump service for q:dump"""
|
|
390
|
+
if 'dump' not in self._services:
|
|
391
|
+
from quantum.core.features.dump.src import DumpService
|
|
392
|
+
self._services['dump'] = DumpService()
|
|
393
|
+
logger.debug("Initialized DumpService")
|
|
394
|
+
return self._services['dump']
|
|
395
|
+
|
|
396
|
+
# ==========================================================================
|
|
397
|
+
# Cache Services
|
|
398
|
+
# ==========================================================================
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def expression_cache(self):
|
|
402
|
+
"""Expression cache for performance"""
|
|
403
|
+
if 'expression_cache' not in self._services:
|
|
404
|
+
from quantum.runtime.expression_cache import get_expression_cache
|
|
405
|
+
self._services['expression_cache'] = get_expression_cache()
|
|
406
|
+
logger.debug("Initialized ExpressionCache")
|
|
407
|
+
return self._services['expression_cache']
|
|
408
|
+
|
|
409
|
+
# ==========================================================================
|
|
410
|
+
# Utility Methods
|
|
411
|
+
# ==========================================================================
|
|
412
|
+
|
|
413
|
+
def get(self, name: str, default: Any = None) -> Any:
|
|
414
|
+
"""
|
|
415
|
+
Get a service by name.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
name: Service name (e.g., 'database', 'llm')
|
|
419
|
+
default: Default value if not found
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Service instance or default
|
|
423
|
+
"""
|
|
424
|
+
# Try property access first
|
|
425
|
+
if hasattr(self, name):
|
|
426
|
+
return getattr(self, name)
|
|
427
|
+
return self._services.get(name, default)
|
|
428
|
+
|
|
429
|
+
def register(self, name: str, service: Any) -> 'ServiceContainer':
|
|
430
|
+
"""
|
|
431
|
+
Register a custom service.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
name: Service name
|
|
435
|
+
service: Service instance
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Self for chaining
|
|
439
|
+
"""
|
|
440
|
+
self._services[name] = service
|
|
441
|
+
return self
|
|
442
|
+
|
|
443
|
+
def is_initialized(self, name: str) -> bool:
|
|
444
|
+
"""Check if a service has been initialized"""
|
|
445
|
+
return name in self._services
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def initialized_services(self) -> list:
|
|
449
|
+
"""Get list of initialized service names"""
|
|
450
|
+
return list(self._services.keys())
|
|
451
|
+
|
|
452
|
+
def shutdown(self):
|
|
453
|
+
"""
|
|
454
|
+
Shutdown all services that support it.
|
|
455
|
+
|
|
456
|
+
Call this when the runtime is being destroyed.
|
|
457
|
+
"""
|
|
458
|
+
for name, service in self._services.items():
|
|
459
|
+
if hasattr(service, 'shutdown'):
|
|
460
|
+
try:
|
|
461
|
+
service.shutdown()
|
|
462
|
+
logger.debug(f"Shutdown {name}")
|
|
463
|
+
except Exception as e:
|
|
464
|
+
logger.warning(f"Error shutting down {name}: {e}")
|
|
465
|
+
|
|
466
|
+
self._services.clear()
|
|
467
|
+
|
|
468
|
+
def __repr__(self) -> str:
|
|
469
|
+
initialized = list(self._services.keys())
|
|
470
|
+
return f"ServiceContainer(initialized={initialized})"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""An action's params from a table's schema (M17, UI-10).
|
|
2
|
+
|
|
3
|
+
`<q:action name="criar" table="posts" datasource="db">` validates what it
|
|
4
|
+
receives by the table it writes to: a NOT NULL column without a default is
|
|
5
|
+
required, `CHECK (col IN (...))` is an enum, `VARCHAR(n)` a maxlength, the
|
|
6
|
+
column type the param type, and a foreign key must name an existing row. A
|
|
7
|
+
`q:param` written in the action wins over the one from the schema.
|
|
8
|
+
|
|
9
|
+
The schema is read from the database (quantum/runtime/db_schema.py) the first
|
|
10
|
+
time, and again when the database file changes. A form that posts to such an
|
|
11
|
+
action and has no fields of its own draws one for each param (UI-10).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import threading
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from quantum.core.ast_nodes import QuantumParam
|
|
21
|
+
|
|
22
|
+
_CACHE: Dict[tuple, Tuple[float, list]] = {}
|
|
23
|
+
_TRAVA = threading.Lock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TableParamsError(Exception):
|
|
27
|
+
"""The table cannot be read (missing, unsupported driver)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _tipo(coluna) -> str:
|
|
31
|
+
t = (coluna.type or '').upper()
|
|
32
|
+
if 'BOOL' in t:
|
|
33
|
+
return 'boolean'
|
|
34
|
+
if 'INT' in t:
|
|
35
|
+
return 'integer'
|
|
36
|
+
if any(x in t for x in ('REAL', 'FLOA', 'DOUB', 'NUMERIC', 'DECIMAL')):
|
|
37
|
+
return 'decimal'
|
|
38
|
+
if t.startswith('DATE') and 'TIME' not in t:
|
|
39
|
+
return 'date'
|
|
40
|
+
return 'string'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _datasource(config: dict, nome: str) -> dict:
|
|
44
|
+
fontes = (config or {}).get('datasources') or {}
|
|
45
|
+
if nome not in fontes:
|
|
46
|
+
raise TableParamsError(f'datasource {nome!r} is not declared in quantum.config.yaml')
|
|
47
|
+
return fontes[nome]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _connect(config: dict, nome: str):
|
|
51
|
+
from quantum.runtime.db_schema import SchemaError, connect_readonly
|
|
52
|
+
try:
|
|
53
|
+
return connect_readonly(_datasource(config, nome), Path.cwd())
|
|
54
|
+
except SchemaError as exc:
|
|
55
|
+
raise TableParamsError(f'datasource {nome!r}: {exc}') from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _read_columns(config: dict, nome: str, tabela: str):
|
|
59
|
+
from quantum.runtime.db_schema import read_schema
|
|
60
|
+
conexao = _connect(config, nome)
|
|
61
|
+
try:
|
|
62
|
+
esquema = read_schema(conexao)
|
|
63
|
+
finally:
|
|
64
|
+
conexao.close()
|
|
65
|
+
if tabela not in esquema:
|
|
66
|
+
raise TableParamsError(f'table {tabela!r} does not exist in datasource {nome!r} '
|
|
67
|
+
f'(tables: {", ".join(esquema) or "none"})')
|
|
68
|
+
return list(esquema[tabela].columns.values())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _mtime(config: dict, nome: str) -> float:
|
|
72
|
+
database = str(_datasource(config, nome).get('database') or '')
|
|
73
|
+
try:
|
|
74
|
+
return os.path.getmtime(Path(database) if Path(database).is_absolute() else Path.cwd() / database)
|
|
75
|
+
except OSError:
|
|
76
|
+
return 0.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def columns_of(config: dict, nome: str, tabela: str) -> list:
|
|
80
|
+
chave = (str(Path.cwd()), nome, tabela)
|
|
81
|
+
mtime = _mtime(config, nome)
|
|
82
|
+
with _TRAVA:
|
|
83
|
+
guardado = _CACHE.get(chave)
|
|
84
|
+
if guardado and guardado[0] == mtime:
|
|
85
|
+
return guardado[1]
|
|
86
|
+
colunas = _read_columns(config, nome, tabela)
|
|
87
|
+
with _TRAVA:
|
|
88
|
+
_CACHE[chave] = (mtime, colunas)
|
|
89
|
+
return colunas
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def param_from_column(coluna) -> QuantumParam:
|
|
93
|
+
tipo = _tipo(coluna)
|
|
94
|
+
param = QuantumParam(
|
|
95
|
+
name=coluna.name, type=tipo,
|
|
96
|
+
required=bool(coluna.not_null and coluna.default is None and tipo != 'boolean'),
|
|
97
|
+
enum=','.join(coluna.options) if coluna.options else None,
|
|
98
|
+
)
|
|
99
|
+
tamanho = re.search(r'\((\d+)\)', coluna.type or '')
|
|
100
|
+
if tamanho and tipo == 'string':
|
|
101
|
+
param.maxlength = int(tamanho.group(1))
|
|
102
|
+
if tipo == 'boolean':
|
|
103
|
+
param.default = 'false'
|
|
104
|
+
param.references = coluna.references # (table, column) of a foreign key
|
|
105
|
+
# A column that takes NULL: left blank, the action gets None (and writes NULL).
|
|
106
|
+
param.nullable = not coluna.not_null
|
|
107
|
+
return param
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def params_of(action, config: dict) -> List[QuantumParam]:
|
|
111
|
+
"""The params an action validates: its own q:params, plus its table's (UI-10)."""
|
|
112
|
+
proprios = list(getattr(action, 'params', None) or [])
|
|
113
|
+
tabela = getattr(action, 'table', None)
|
|
114
|
+
if not tabela:
|
|
115
|
+
return proprios
|
|
116
|
+
escritos = {p.name for p in proprios}
|
|
117
|
+
permitidas = getattr(action, 'table_columns', None)
|
|
118
|
+
do_esquema = []
|
|
119
|
+
for coluna in columns_of(config, action.table_datasource, tabela):
|
|
120
|
+
if coluna.primary_key or coluna.name in escritos:
|
|
121
|
+
continue
|
|
122
|
+
if permitidas is not None and coluna.name not in permitidas:
|
|
123
|
+
continue
|
|
124
|
+
do_esquema.append(param_from_column(coluna))
|
|
125
|
+
if permitidas is not None:
|
|
126
|
+
faltando = [c for c in permitidas if c not in {p.name for p in do_esquema} | escritos]
|
|
127
|
+
if faltando:
|
|
128
|
+
raise TableParamsError(f'<q:action name="{action.name}" columns=...>: table {tabela!r} has no '
|
|
129
|
+
f'column {", ".join(faltando)}')
|
|
130
|
+
ordem = {nome: i for i, nome in enumerate(permitidas)}
|
|
131
|
+
do_esquema.sort(key=lambda p: ordem.get(p.name, len(ordem)))
|
|
132
|
+
return do_esquema + proprios
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def reference_options(config: dict, datasource: str, referencia: Tuple[str, str]) -> List[Tuple[str, str]]:
|
|
136
|
+
"""(value, label) of the rows a foreign key may point to — label is the first text column."""
|
|
137
|
+
tabela, coluna = referencia
|
|
138
|
+
colunas = columns_of(config, datasource, tabela)
|
|
139
|
+
preferidas = ('nome', 'name', 'titulo', 'title', 'label', 'descricao', 'description')
|
|
140
|
+
textos = [c.name for c in colunas if _tipo(c) == 'string']
|
|
141
|
+
rotulo = next((p for p in preferidas if p in textos), textos[0] if textos else coluna)
|
|
142
|
+
conexao = _connect(config, datasource)
|
|
143
|
+
try:
|
|
144
|
+
linhas = conexao.execute(
|
|
145
|
+
f'SELECT "{coluna}", "{rotulo}" FROM "{tabela}" ORDER BY "{rotulo}" LIMIT 1000').fetchall()
|
|
146
|
+
finally:
|
|
147
|
+
conexao.close()
|
|
148
|
+
return [(str(v), str(r)) for v, r in linhas]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def reference_exists(config: dict, datasource: str, referencia: Tuple[str, str], valor) -> bool:
|
|
152
|
+
tabela, coluna = referencia
|
|
153
|
+
conexao = _connect(config, datasource)
|
|
154
|
+
try:
|
|
155
|
+
return conexao.execute(f'SELECT 1 FROM "{tabela}" WHERE "{coluna}" = ? LIMIT 1', (valor,)).fetchone() is not None
|
|
156
|
+
finally:
|
|
157
|
+
conexao.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def humanize(nome: str) -> str:
|
|
161
|
+
"""'autor_id' -> 'Autor'; 'data_publicacao' -> 'Data publicacao'."""
|
|
162
|
+
base = re.sub(r'_id$', '', nome).replace('_', ' ').strip()
|
|
163
|
+
return base[:1].upper() + base[1:] if base else nome
|