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
quantum/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Quantum main package
|
quantum/cli/__init__.py
ADDED
quantum/cli/admin.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""`quantum admin` — serve the Quantum Admin (quantum_admin/components/admin).
|
|
2
|
+
|
|
3
|
+
The admin is a Quantum app: .q screens over declared services. This command
|
|
4
|
+
writes its configuration into a data folder and starts the same server as
|
|
5
|
+
`quantum start`, so nothing has to be set up by hand.
|
|
6
|
+
|
|
7
|
+
quantum admin # data in ./.quantum-admin, projects under ./
|
|
8
|
+
quantum admin --data quantum_admin # the repository's own admin data
|
|
9
|
+
|
|
10
|
+
The data folder holds the admin database, its settings (connectors, global
|
|
11
|
+
settings, process PIDs) and the generated config. Paths of projects are
|
|
12
|
+
relative to the folder the command runs in (--root to change it).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import secrets
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
MODULES = [
|
|
20
|
+
"quantum_admin.services.auth",
|
|
21
|
+
"quantum_admin.services.projects",
|
|
22
|
+
"quantum_admin.services.yaml_import",
|
|
23
|
+
"quantum_admin.services.connectors",
|
|
24
|
+
"quantum_admin.services.settings",
|
|
25
|
+
"quantum_admin.services.components",
|
|
26
|
+
"quantum_admin.services.apps",
|
|
27
|
+
"quantum_admin.services.repository",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
INSTALL_HINT = 'pip install "quantum-framework[admin]"'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_admin_parser(subparsers):
|
|
34
|
+
parser = subparsers.add_parser('admin', help='Start the Quantum Admin')
|
|
35
|
+
parser.add_argument('--port', type=int, default=8090, help='Port (default 8090)')
|
|
36
|
+
parser.add_argument('--data', default='.quantum-admin',
|
|
37
|
+
help='Folder for the admin database and settings (default ./.quantum-admin)')
|
|
38
|
+
parser.add_argument('--root', default='.',
|
|
39
|
+
help='Folder the project paths are relative to (default: current folder)')
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def missing_dependencies():
|
|
44
|
+
"""Names of the [admin] extra's packages that do not import."""
|
|
45
|
+
faltando = []
|
|
46
|
+
for modulo, pacote in (("sqlalchemy", "SQLAlchemy"), ("jwt", "PyJWT"),
|
|
47
|
+
("cryptography", "cryptography")):
|
|
48
|
+
try:
|
|
49
|
+
__import__(modulo)
|
|
50
|
+
except ImportError:
|
|
51
|
+
faltando.append(pacote)
|
|
52
|
+
return faltando
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _package() -> Path:
|
|
56
|
+
import quantum_admin
|
|
57
|
+
return Path(quantum_admin.__file__).resolve().parent
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def screens_folder() -> Path:
|
|
61
|
+
return _package() / "components"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def assets_folder() -> Path:
|
|
65
|
+
"""The screens' CSS and JS, served under /static/."""
|
|
66
|
+
return _package() / "assets"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _secret(arquivo: Path) -> str:
|
|
70
|
+
"""A key kept in the data folder, so sessions survive a restart."""
|
|
71
|
+
if arquivo.exists():
|
|
72
|
+
return arquivo.read_text(encoding="utf-8").strip()
|
|
73
|
+
chave = secrets.token_hex(32)
|
|
74
|
+
arquivo.write_text(chave, encoding="utf-8")
|
|
75
|
+
try:
|
|
76
|
+
arquivo.chmod(0o600)
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
return chave
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def prepare(data: str, root: str, port: int) -> Path:
|
|
83
|
+
"""Create the data folder, point the admin at it, write the config. Returns its path."""
|
|
84
|
+
import yaml
|
|
85
|
+
|
|
86
|
+
pasta = Path(data).resolve()
|
|
87
|
+
(pasta / "settings").mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
# Read by quantum_admin.backend at import time, so set before any import.
|
|
90
|
+
os.environ["QUANTUM_ADMIN_DATABASE_URL"] = f"sqlite:///{(pasta / 'quantum_admin.db').as_posix()}"
|
|
91
|
+
os.environ["QUANTUM_ADMIN_SETTINGS_DIR"] = str(pasta / "settings")
|
|
92
|
+
os.environ["QUANTUM_ADMIN_ROOT"] = str(Path(root).resolve())
|
|
93
|
+
os.environ.setdefault("JWT_SECRET_KEY", _secret(pasta / "jwt_secret"))
|
|
94
|
+
|
|
95
|
+
config = pasta / "quantum.config.yaml"
|
|
96
|
+
config.write_text(
|
|
97
|
+
"# Generated by `quantum admin` on every start; edits are overwritten.\n" +
|
|
98
|
+
yaml.safe_dump({
|
|
99
|
+
"server": {"host": "127.0.0.1", "port": port, "reload": False, "debug": False},
|
|
100
|
+
"paths": {"components": screens_folder().as_posix(),
|
|
101
|
+
"static": assets_folder().as_posix(),
|
|
102
|
+
"uploads": (pasta / "uploads").as_posix(),
|
|
103
|
+
"logs": (pasta / "logs").as_posix()},
|
|
104
|
+
"security": {"login_url": "/admin/login", "secret_key": _secret(pasta / "session_secret")},
|
|
105
|
+
"logging": {"level": "WARNING", "console": True, "file": False},
|
|
106
|
+
"services": MODULES,
|
|
107
|
+
}, sort_keys=False),
|
|
108
|
+
encoding="utf-8")
|
|
109
|
+
return config
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def handle_admin(args) -> int:
|
|
113
|
+
faltando = missing_dependencies()
|
|
114
|
+
if faltando:
|
|
115
|
+
print(f"[ERROR] The admin needs {', '.join(faltando)}. Install with:\n {INSTALL_HINT}")
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
config = prepare(args.data, args.root, args.port)
|
|
119
|
+
|
|
120
|
+
from quantum.runtime.web_server import QuantumWebServer
|
|
121
|
+
from quantum_admin.backend.auth_service import get_auth_service
|
|
122
|
+
|
|
123
|
+
# Created now, not at the first login, so a generated password is printed
|
|
124
|
+
# before the URL instead of in the middle of the request log.
|
|
125
|
+
get_auth_service()
|
|
126
|
+
print(f"\n Quantum Admin: http://127.0.0.1:{args.port}/admin"
|
|
127
|
+
f"\n data: {config.parent}\n")
|
|
128
|
+
|
|
129
|
+
server = QuantumWebServer(str(config))
|
|
130
|
+
server.PID_FILE = str(config.parent / "admin.pid")
|
|
131
|
+
return server.start() or 0
|
quantum/cli/check.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""`quantum check`: every page parses, every q:query compiles against the database,
|
|
2
|
+
and every `{query.field}` a page reads is a column the query returns (M8, DEV-3).
|
|
3
|
+
|
|
4
|
+
A typo in SQL or in a field name used to surface only when someone opened the
|
|
5
|
+
page — `no such column` as a 500, or, for a field, an empty cell. This reads
|
|
6
|
+
the project the way the server would and reports each problem with its file
|
|
7
|
+
and line, without running anything: a query is compiled by the database
|
|
8
|
+
(EXPLAIN) and a SELECT's columns come from running it with LIMIT 0.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Iterator, List, Optional
|
|
15
|
+
|
|
16
|
+
# Fields of `<name>_result` and of a query row that are not columns.
|
|
17
|
+
_META = {'recordCount', 'columnList', 'executionTime', 'success', 'error', 'sql', 'page',
|
|
18
|
+
'totalPages', 'hasMore', 'cached', 'affectedRows', 'lastInsertId', 'data', 'length'}
|
|
19
|
+
_EXPR = re.compile(r'\{([^{}]*)\}')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Problem:
|
|
24
|
+
file: str
|
|
25
|
+
line: Optional[int]
|
|
26
|
+
message: str
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return f'{self.file}:{self.line or "?"}: {self.message}'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _config(config_path: Path) -> dict:
|
|
33
|
+
import yaml
|
|
34
|
+
from quantum.core.config_env import expand_env
|
|
35
|
+
if not config_path.exists():
|
|
36
|
+
return {}
|
|
37
|
+
return expand_env(yaml.safe_load(config_path.read_text(encoding='utf-8')) or {})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _children(node) -> Iterator:
|
|
41
|
+
"""The nodes inside a node, whatever the field is called."""
|
|
42
|
+
for campo in ('statements', 'body', 'if_body', 'else_body', 'children'):
|
|
43
|
+
for filho in getattr(node, campo, None) or []:
|
|
44
|
+
if hasattr(filho, '__dict__'):
|
|
45
|
+
yield filho
|
|
46
|
+
for bloco in getattr(node, 'elseif_blocks', None) or []:
|
|
47
|
+
corpo = bloco.get('body') if isinstance(bloco, dict) else getattr(bloco, 'body', None)
|
|
48
|
+
for filho in corpo or []:
|
|
49
|
+
yield filho
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _texts(node) -> List[str]:
|
|
53
|
+
"""The strings of a node that may hold {expressions}."""
|
|
54
|
+
textos = []
|
|
55
|
+
for campo, valor in vars(node).items():
|
|
56
|
+
if campo.startswith('_') or campo in ('sql',):
|
|
57
|
+
continue
|
|
58
|
+
if isinstance(valor, str) and '{' in valor:
|
|
59
|
+
textos.append(valor)
|
|
60
|
+
elif isinstance(valor, dict):
|
|
61
|
+
textos += [v for v in valor.values() if isinstance(v, str) and '{' in v]
|
|
62
|
+
return textos
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ProjectChecker:
|
|
66
|
+
def __init__(self, project: Path, config_path: Path):
|
|
67
|
+
self.project = project
|
|
68
|
+
self.config = _config(config_path)
|
|
69
|
+
self.datasources = self.config.get('datasources') or {}
|
|
70
|
+
self._conexoes: Dict[str, object] = {}
|
|
71
|
+
self.problems: List[Problem] = []
|
|
72
|
+
self.notes: List[str] = []
|
|
73
|
+
self.queries_checked = 0
|
|
74
|
+
self.files = 0
|
|
75
|
+
|
|
76
|
+
# -- the database ---------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def _connection(self, nome: str):
|
|
79
|
+
from quantum.runtime.db_schema import SchemaError, connect_readonly
|
|
80
|
+
if nome not in self._conexoes:
|
|
81
|
+
try:
|
|
82
|
+
self._conexoes[nome] = connect_readonly(self.datasources[nome], self.project)
|
|
83
|
+
except SchemaError as exc:
|
|
84
|
+
self._conexoes[nome] = None
|
|
85
|
+
self.notes.append(f'datasource {nome!r} not checked: {exc}')
|
|
86
|
+
return self._conexoes[nome]
|
|
87
|
+
|
|
88
|
+
# -- a file ---------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def check_file(self, arquivo: Path) -> None:
|
|
91
|
+
from quantum.core.parser import QuantumParser
|
|
92
|
+
rel = arquivo.relative_to(self.project).as_posix() if arquivo.is_relative_to(self.project) else str(arquivo)
|
|
93
|
+
self.files += 1
|
|
94
|
+
try:
|
|
95
|
+
ast = QuantumParser(use_cache=False).parse_file(str(arquivo))
|
|
96
|
+
except Exception as exc: # a parse error is a problem like any other
|
|
97
|
+
primeira = str(exc).split('\n')[0]
|
|
98
|
+
self.problems.append(Problem(rel, getattr(exc, 'line', None), primeira))
|
|
99
|
+
return
|
|
100
|
+
colunas: Dict[str, List[str]] = {}
|
|
101
|
+
self._queries(ast, rel, colunas)
|
|
102
|
+
self._fields(ast, rel, colunas, {nome: nome for nome in colunas}, None)
|
|
103
|
+
|
|
104
|
+
def _queries(self, node, rel: str, colunas: Dict[str, List[str]]) -> None:
|
|
105
|
+
from quantum.core.ast_nodes import ActionNode, QueryNode
|
|
106
|
+
from quantum.runtime.db_schema import check_sql, read_schema
|
|
107
|
+
for filho in _children(node):
|
|
108
|
+
if isinstance(filho, ActionNode) and getattr(filho, 'table', None):
|
|
109
|
+
# UI-10: the table the action takes its params from exists, with those columns.
|
|
110
|
+
ds = filho.table_datasource
|
|
111
|
+
if ds not in self.datasources:
|
|
112
|
+
self.problems.append(Problem(rel, getattr(filho, 'source_line', None),
|
|
113
|
+
f'<q:action name="{filho.name}">: datasource {ds!r} is not '
|
|
114
|
+
f'declared in quantum.config.yaml'))
|
|
115
|
+
elif self._connection(ds) is not None:
|
|
116
|
+
esquema = read_schema(self._connection(ds))
|
|
117
|
+
tabela = esquema.get(filho.table)
|
|
118
|
+
if tabela is None:
|
|
119
|
+
self.problems.append(Problem(rel, getattr(filho, 'source_line', None),
|
|
120
|
+
f'<q:action name="{filho.name}" table="{filho.table}">: '
|
|
121
|
+
f'no such table (tables: {", ".join(esquema) or "none"})'))
|
|
122
|
+
else:
|
|
123
|
+
for coluna in filho.table_columns or []:
|
|
124
|
+
if coluna not in tabela.columns:
|
|
125
|
+
self.problems.append(Problem(
|
|
126
|
+
rel, getattr(filho, 'source_line', None),
|
|
127
|
+
f'<q:action name="{filho.name}" columns=...>: table "{filho.table}" '
|
|
128
|
+
f'has no column "{coluna}"'))
|
|
129
|
+
if isinstance(filho, QueryNode):
|
|
130
|
+
ds = filho.datasource
|
|
131
|
+
if not ds or ds.startswith('knowledge:') or getattr(filho, 'source', None):
|
|
132
|
+
pass # RAG or query of queries: not SQL on a database
|
|
133
|
+
elif ds not in self.datasources:
|
|
134
|
+
self.problems.append(Problem(rel, getattr(filho, 'source_line', None),
|
|
135
|
+
f'<q:query name="{filho.name}">: datasource {ds!r} is not '
|
|
136
|
+
f'declared in quantum.config.yaml'))
|
|
137
|
+
else:
|
|
138
|
+
conexao = self._connection(ds)
|
|
139
|
+
if conexao is not None:
|
|
140
|
+
self.queries_checked += 1
|
|
141
|
+
erro, cols = check_sql(conexao, filho.sql or '')
|
|
142
|
+
if erro:
|
|
143
|
+
self.problems.append(Problem(rel, getattr(filho, 'source_line', None),
|
|
144
|
+
f'<q:query name="{filho.name}">: {erro}'))
|
|
145
|
+
elif cols is not None:
|
|
146
|
+
colunas[filho.name] = cols
|
|
147
|
+
self._queries(filho, rel, colunas)
|
|
148
|
+
|
|
149
|
+
def _fields(self, node, rel: str, colunas: Dict[str, List[str]], ligados: Dict[str, str],
|
|
150
|
+
linha: Optional[int]) -> None:
|
|
151
|
+
"""Each `{var.field}` where var is a query, or a row of one, names a column it returns."""
|
|
152
|
+
from quantum.core.features.loops.src.ast_node import LoopNode
|
|
153
|
+
from quantum.core.features.ui_engine.src.ast_nodes import UIColumnNode, UIListNode, UITableNode
|
|
154
|
+
linha = getattr(node, 'source_line', None) or linha
|
|
155
|
+
for texto in _texts(node):
|
|
156
|
+
for expressao in _EXPR.findall(texto):
|
|
157
|
+
for var, campo in re.findall(r'(?<![\w.])(\w+)\.(\w+)', expressao):
|
|
158
|
+
self._field(rel, linha, colunas, ligados, var, campo)
|
|
159
|
+
if isinstance(node, UIColumnNode) and node.key and '$tabela' in ligados:
|
|
160
|
+
self._field(rel, linha, colunas, ligados, '$tabela', node.key, f'<ui:column key="{node.key}">')
|
|
161
|
+
|
|
162
|
+
for filho in _children(node):
|
|
163
|
+
dentro = dict(ligados)
|
|
164
|
+
origem = None
|
|
165
|
+
if isinstance(filho, LoopNode):
|
|
166
|
+
origem = filho.query_name if filho.loop_type == 'query' else _single_name(filho.items)
|
|
167
|
+
if origem in colunas:
|
|
168
|
+
dentro[filho.var_name] = origem
|
|
169
|
+
elif isinstance(filho, (UITableNode, UIListNode)):
|
|
170
|
+
origem = _single_name(filho.source)
|
|
171
|
+
if origem in colunas:
|
|
172
|
+
dentro[filho.as_var or ('row' if isinstance(filho, UITableNode) else 'item')] = origem
|
|
173
|
+
if isinstance(filho, UITableNode):
|
|
174
|
+
dentro['$tabela'] = origem
|
|
175
|
+
self._fields(filho, rel, colunas, dentro, linha)
|
|
176
|
+
|
|
177
|
+
def _field(self, rel, linha, colunas, ligados, var, campo, como=None) -> None:
|
|
178
|
+
consulta = ligados.get(var)
|
|
179
|
+
if consulta is None or campo in _META:
|
|
180
|
+
return
|
|
181
|
+
cols = colunas.get(consulta, [])
|
|
182
|
+
if campo not in cols:
|
|
183
|
+
quem = como or f'{{{var}.{campo}}}'
|
|
184
|
+
self.problems.append(Problem(
|
|
185
|
+
rel, linha, f'{quem}: query "{consulta}" returns no column "{campo}" '
|
|
186
|
+
f'(columns: {", ".join(cols) or "none"})'))
|
|
187
|
+
|
|
188
|
+
# -- the project ----------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def run(self) -> List[Problem]:
|
|
191
|
+
pasta = self.project / (self.config.get('paths', {}) or {}).get('components', 'components')
|
|
192
|
+
if not pasta.exists():
|
|
193
|
+
self.notes.append(f'no components folder at {pasta}')
|
|
194
|
+
return self.problems
|
|
195
|
+
for arquivo in sorted(pasta.rglob('*.q')):
|
|
196
|
+
self.check_file(arquivo)
|
|
197
|
+
for conexao in self._conexoes.values():
|
|
198
|
+
if conexao is not None:
|
|
199
|
+
conexao.close()
|
|
200
|
+
return self.problems
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _single_name(expr: Optional[str]) -> Optional[str]:
|
|
204
|
+
"""`{tarefas}` -> 'tarefas'; anything more complex -> None."""
|
|
205
|
+
m = re.fullmatch(r'\s*\{\s*(\w+)\s*\}\s*', expr or '')
|
|
206
|
+
return m.group(1) if m else None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def run_check(config_path: str = 'quantum.config.yaml') -> int:
|
|
210
|
+
"""`quantum check`: print each problem as file:line: message; exit 1 if there is any."""
|
|
211
|
+
caminho = Path(config_path).resolve()
|
|
212
|
+
checker = ProjectChecker(caminho.parent, caminho)
|
|
213
|
+
problemas = checker.run()
|
|
214
|
+
for nota in checker.notes:
|
|
215
|
+
print(f'[NOTE] {nota}')
|
|
216
|
+
for problema in problemas:
|
|
217
|
+
print(problema)
|
|
218
|
+
resumo = (f'{checker.files} file(s), {checker.queries_checked} query(ies) checked against the database')
|
|
219
|
+
if problemas:
|
|
220
|
+
print(f'\n{len(problemas)} problem(s) — {resumo}.')
|
|
221
|
+
return 1
|
|
222
|
+
print(f'OK — {resumo}.')
|
|
223
|
+
return 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum CLI Commands
|
|
3
|
+
|
|
4
|
+
Each command module provides a click command group or command.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from quantum.cli.commands.new import new
|
|
8
|
+
from quantum.cli.commands.dev import dev
|
|
9
|
+
from quantum.cli.commands.build import build
|
|
10
|
+
from quantum.cli.commands.serve import serve
|
|
11
|
+
from quantum.cli.commands.test import test
|
|
12
|
+
from quantum.cli.commands.lint import lint
|
|
13
|
+
from quantum.cli.commands.docs import docs
|
|
14
|
+
|
|
15
|
+
__all__ = ['new', 'dev', 'build', 'serve', 'test', 'lint', 'docs']
|