quantum-framework 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parar o servidor iniciado por `quantum start` e CONFERIR que ele parou.
|
|
3
|
+
|
|
4
|
+
O `quantum stop` antigo matava o PID do `.quantum.pid`, imprimia "Server
|
|
5
|
+
stopped" e saia com 0 — sem olhar se alguma coisa tinha parado. Com
|
|
6
|
+
`reload: true` (o padrao do quantum.config.yaml) o Werkzeug serve a partir de
|
|
7
|
+
um processo FILHO; o arquivo tinha so o PID do pai. No Windows,
|
|
8
|
+
`taskkill /F` sem `/T` derruba so o pai, e o filho seguia vivo, orfao,
|
|
9
|
+
respondendo HTTP 200 na mesma porta.
|
|
10
|
+
|
|
11
|
+
Agora:
|
|
12
|
+
- o `.quantum.pid` guarda o pai e, quando ha reloader, o filho (uma linha
|
|
13
|
+
cada — o formato antigo, uma linha so, continua valido);
|
|
14
|
+
- o stop derruba a arvore de cada PID, espera, e so declara sucesso se
|
|
15
|
+
nenhum deles continuar vivo. Se sobrar algum, diz qual e sai com 1.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import signal
|
|
20
|
+
import subprocess
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Callable, List
|
|
24
|
+
|
|
25
|
+
PID_FILE = '.quantum.pid'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_pids(pid_file: Path) -> List[int]:
|
|
29
|
+
"""PIDs listados no arquivo, na ordem, sem repetir.
|
|
30
|
+
|
|
31
|
+
Levanta ValueError se o arquivo nao tiver nenhum inteiro valido.
|
|
32
|
+
"""
|
|
33
|
+
pids: List[int] = []
|
|
34
|
+
for linha in pid_file.read_text(encoding='utf-8').split():
|
|
35
|
+
pid = int(linha)
|
|
36
|
+
if pid not in pids:
|
|
37
|
+
pids.append(pid)
|
|
38
|
+
if not pids:
|
|
39
|
+
raise ValueError(f"{pid_file} is empty")
|
|
40
|
+
return pids
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def pid_alive(pid: int) -> bool:
|
|
44
|
+
"""O processo existe e ainda nao terminou?"""
|
|
45
|
+
if pid <= 0:
|
|
46
|
+
return False
|
|
47
|
+
if os.name == 'nt':
|
|
48
|
+
return _pid_alive_windows(pid)
|
|
49
|
+
try:
|
|
50
|
+
os.kill(pid, 0)
|
|
51
|
+
except ProcessLookupError:
|
|
52
|
+
return False
|
|
53
|
+
except PermissionError:
|
|
54
|
+
pass # existe, so nao e nosso
|
|
55
|
+
return not _exited_unreaped(pid)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _exited_unreaped(pid: int) -> bool:
|
|
59
|
+
"""O PID ainda existe na tabela, mas o processo ja terminou?"""
|
|
60
|
+
# Um processo que terminou e ainda nao foi recolhido pelo pai (zumbi)
|
|
61
|
+
# responde a os.kill(pid, 0) como se estivesse vivo. Nao esta: nao roda,
|
|
62
|
+
# nao segura porta. Sem isso, parar um servidor cujo pai demora a chamar
|
|
63
|
+
# wait() — um supervisor, ou o proprio pytest — dava "still running".
|
|
64
|
+
try:
|
|
65
|
+
with open(f'/proc/{pid}/stat', 'rb') as f:
|
|
66
|
+
estado = f.read().rsplit(b')', 1)[1].split()[0]
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
return os.path.isdir('/proc') # /proc existe e o pid nao: ja foi embora
|
|
69
|
+
except (OSError, IndexError):
|
|
70
|
+
return False # sem /proc (macOS): fica com o resultado do os.kill
|
|
71
|
+
return estado in (b'Z', b'X')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _pid_alive_windows(pid: int) -> bool:
|
|
75
|
+
# os.kill(pid, 0) NAO serve no Windows: qualquer sinal que nao seja
|
|
76
|
+
# CTRL_C/CTRL_BREAK vira TerminateProcess — a checagem mataria o processo.
|
|
77
|
+
import ctypes
|
|
78
|
+
from ctypes import wintypes
|
|
79
|
+
|
|
80
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
81
|
+
STILL_ACTIVE = 259
|
|
82
|
+
ERROR_ACCESS_DENIED = 5
|
|
83
|
+
|
|
84
|
+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
85
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
86
|
+
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
|
87
|
+
if not handle:
|
|
88
|
+
return ctypes.get_last_error() == ERROR_ACCESS_DENIED
|
|
89
|
+
try:
|
|
90
|
+
codigo = wintypes.DWORD()
|
|
91
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(codigo)):
|
|
92
|
+
return False
|
|
93
|
+
return codigo.value == STILL_ACTIVE
|
|
94
|
+
finally:
|
|
95
|
+
kernel32.CloseHandle(handle)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _terminate(pid: int, force: bool) -> None:
|
|
99
|
+
if os.name == 'nt':
|
|
100
|
+
# /T derruba a arvore inteira: o filho do reloader junto com o pai.
|
|
101
|
+
subprocess.run(['taskkill', '/F', '/T', '/PID', str(pid)],
|
|
102
|
+
capture_output=True)
|
|
103
|
+
return
|
|
104
|
+
try:
|
|
105
|
+
os.kill(pid, signal.SIGKILL if force else signal.SIGTERM)
|
|
106
|
+
except (ProcessLookupError, PermissionError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _wait_until_dead(pids: List[int], timeout: float,
|
|
111
|
+
alive: Callable[[int], bool]) -> List[int]:
|
|
112
|
+
limite = time.monotonic() + timeout
|
|
113
|
+
vivos = [p for p in pids if alive(p)]
|
|
114
|
+
while vivos and time.monotonic() < limite:
|
|
115
|
+
time.sleep(0.2)
|
|
116
|
+
vivos = [p for p in vivos if alive(p)]
|
|
117
|
+
return vivos
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def stop_server(pid_file: Path = Path(PID_FILE), grace: float = 5.0,
|
|
121
|
+
alive: Callable[[int], bool] = pid_alive,
|
|
122
|
+
terminate: Callable[[int, bool], None] = _terminate) -> int:
|
|
123
|
+
"""Para o servidor do `pid_file`. Devolve o codigo de saida do comando."""
|
|
124
|
+
if not pid_file.exists():
|
|
125
|
+
print(f"No running server found ({pid_file.name} not found)")
|
|
126
|
+
return 1
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
pids = read_pids(pid_file)
|
|
130
|
+
except (ValueError, OSError) as e:
|
|
131
|
+
print(f"Invalid PID file: {e}")
|
|
132
|
+
pid_file.unlink(missing_ok=True)
|
|
133
|
+
return 1
|
|
134
|
+
|
|
135
|
+
vivos = [p for p in pids if alive(p)]
|
|
136
|
+
if not vivos:
|
|
137
|
+
print(f"No running server found (PID {', '.join(map(str, pids))} "
|
|
138
|
+
f"is not running; removed stale {pid_file.name})")
|
|
139
|
+
pid_file.unlink(missing_ok=True)
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
for pid in vivos:
|
|
143
|
+
terminate(pid, False)
|
|
144
|
+
restantes = _wait_until_dead(vivos, grace, alive)
|
|
145
|
+
|
|
146
|
+
if restantes:
|
|
147
|
+
# SIGTERM ignorado (POSIX). No Windows o taskkill /F ja e forcado.
|
|
148
|
+
for pid in restantes:
|
|
149
|
+
terminate(pid, True)
|
|
150
|
+
restantes = _wait_until_dead(restantes, 2.0, alive)
|
|
151
|
+
|
|
152
|
+
if restantes:
|
|
153
|
+
print(f"Could not stop the server: PID {', '.join(map(str, restantes))} "
|
|
154
|
+
f"is still running. Stop it manually.")
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
print(f"Server stopped (PID {', '.join(map(str, vivos))})")
|
|
158
|
+
pid_file.unlink(missing_ok=True)
|
|
159
|
+
return 0
|
quantum/cli/utils.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum CLI Utilities
|
|
3
|
+
|
|
4
|
+
Color output, progress bars, and common CLI helpers using rich.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
|
|
13
|
+
# Try to import rich, provide helpful error if missing
|
|
14
|
+
try:
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
19
|
+
from rich.syntax import Syntax
|
|
20
|
+
from rich.tree import Tree
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
from rich.theme import Theme
|
|
23
|
+
from rich.markdown import Markdown
|
|
24
|
+
RICH_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
RICH_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Quantum theme colors
|
|
30
|
+
QUANTUM_THEME = Theme({
|
|
31
|
+
"info": "cyan",
|
|
32
|
+
"success": "green",
|
|
33
|
+
"warning": "yellow",
|
|
34
|
+
"error": "bold red",
|
|
35
|
+
"highlight": "bold magenta",
|
|
36
|
+
"dim": "dim white",
|
|
37
|
+
"command": "bold blue",
|
|
38
|
+
"path": "underline cyan",
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class QuantumConsole:
|
|
43
|
+
"""Rich console wrapper for Quantum CLI output."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, quiet: bool = False, no_color: bool = False):
|
|
46
|
+
"""Initialize console with optional quiet/no-color modes."""
|
|
47
|
+
self.quiet = quiet
|
|
48
|
+
self.no_color = no_color or os.environ.get('NO_COLOR', '') != ''
|
|
49
|
+
|
|
50
|
+
if RICH_AVAILABLE:
|
|
51
|
+
self.console = Console(
|
|
52
|
+
theme=QUANTUM_THEME,
|
|
53
|
+
force_terminal=not no_color,
|
|
54
|
+
no_color=no_color
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
self.console = None
|
|
58
|
+
|
|
59
|
+
def _fallback_print(self, message: str, prefix: str = ""):
|
|
60
|
+
"""Fallback print when rich is not available."""
|
|
61
|
+
if prefix:
|
|
62
|
+
print(f"[{prefix}] {message}")
|
|
63
|
+
else:
|
|
64
|
+
print(message)
|
|
65
|
+
|
|
66
|
+
def info(self, message: str) -> None:
|
|
67
|
+
"""Print info message."""
|
|
68
|
+
if self.quiet:
|
|
69
|
+
return
|
|
70
|
+
if self.console:
|
|
71
|
+
self.console.print(f"[info][INFO][/info] {message}")
|
|
72
|
+
else:
|
|
73
|
+
self._fallback_print(message, "INFO")
|
|
74
|
+
|
|
75
|
+
def success(self, message: str) -> None:
|
|
76
|
+
"""Print success message."""
|
|
77
|
+
if self.console:
|
|
78
|
+
self.console.print(f"[success][SUCCESS][/success] {message}")
|
|
79
|
+
else:
|
|
80
|
+
self._fallback_print(message, "SUCCESS")
|
|
81
|
+
|
|
82
|
+
def warning(self, message: str) -> None:
|
|
83
|
+
"""Print warning message."""
|
|
84
|
+
if self.console:
|
|
85
|
+
self.console.print(f"[warning][WARNING][/warning] {message}")
|
|
86
|
+
else:
|
|
87
|
+
self._fallback_print(message, "WARNING")
|
|
88
|
+
|
|
89
|
+
def error(self, message: str) -> None:
|
|
90
|
+
"""Print error message."""
|
|
91
|
+
if self.console:
|
|
92
|
+
self.console.print(f"[error][ERROR][/error] {message}", style="error")
|
|
93
|
+
else:
|
|
94
|
+
self._fallback_print(message, "ERROR")
|
|
95
|
+
|
|
96
|
+
def print(self, message: str = "", style: Optional[str] = None) -> None:
|
|
97
|
+
"""Print generic message."""
|
|
98
|
+
if self.quiet:
|
|
99
|
+
return
|
|
100
|
+
if self.console:
|
|
101
|
+
self.console.print(message, style=style)
|
|
102
|
+
else:
|
|
103
|
+
print(message)
|
|
104
|
+
|
|
105
|
+
def header(self, title: str, subtitle: Optional[str] = None) -> None:
|
|
106
|
+
"""Print header with title and optional subtitle."""
|
|
107
|
+
if self.quiet:
|
|
108
|
+
return
|
|
109
|
+
if self.console:
|
|
110
|
+
text = f"[bold magenta]{title}[/bold magenta]"
|
|
111
|
+
if subtitle:
|
|
112
|
+
text += f"\n[dim]{subtitle}[/dim]"
|
|
113
|
+
self.console.print(Panel(text, border_style="magenta"))
|
|
114
|
+
else:
|
|
115
|
+
print(f"\n=== {title} ===")
|
|
116
|
+
if subtitle:
|
|
117
|
+
print(f" {subtitle}")
|
|
118
|
+
print()
|
|
119
|
+
|
|
120
|
+
def panel(self, content: str, title: Optional[str] = None, style: str = "blue") -> None:
|
|
121
|
+
"""Print content in a panel."""
|
|
122
|
+
if self.quiet:
|
|
123
|
+
return
|
|
124
|
+
if self.console:
|
|
125
|
+
self.console.print(Panel(content, title=title, border_style=style))
|
|
126
|
+
else:
|
|
127
|
+
if title:
|
|
128
|
+
print(f"\n--- {title} ---")
|
|
129
|
+
print(content)
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
def table(self, headers: List[str], rows: List[List[str]], title: Optional[str] = None) -> None:
|
|
133
|
+
"""Print a table."""
|
|
134
|
+
if self.console:
|
|
135
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
136
|
+
for header in headers:
|
|
137
|
+
table.add_column(header)
|
|
138
|
+
for row in rows:
|
|
139
|
+
table.add_row(*row)
|
|
140
|
+
self.console.print(table)
|
|
141
|
+
else:
|
|
142
|
+
if title:
|
|
143
|
+
print(f"\n{title}")
|
|
144
|
+
# Simple table fallback
|
|
145
|
+
col_widths = [max(len(str(row[i])) for row in [headers] + rows) for i in range(len(headers))]
|
|
146
|
+
header_line = " | ".join(h.ljust(w) for h, w in zip(headers, col_widths))
|
|
147
|
+
print(header_line)
|
|
148
|
+
print("-" * len(header_line))
|
|
149
|
+
for row in rows:
|
|
150
|
+
print(" | ".join(str(c).ljust(w) for c, w in zip(row, col_widths)))
|
|
151
|
+
|
|
152
|
+
def tree(self, label: str, items: Dict[str, Any]) -> None:
|
|
153
|
+
"""Print a tree structure."""
|
|
154
|
+
if self.console:
|
|
155
|
+
tree = Tree(f"[bold]{label}[/bold]")
|
|
156
|
+
self._build_tree(tree, items)
|
|
157
|
+
self.console.print(tree)
|
|
158
|
+
else:
|
|
159
|
+
print(f"{label}")
|
|
160
|
+
self._print_tree_fallback(items, indent=2)
|
|
161
|
+
|
|
162
|
+
def _build_tree(self, parent, items: Dict[str, Any]) -> None:
|
|
163
|
+
"""Recursively build tree."""
|
|
164
|
+
for key, value in items.items():
|
|
165
|
+
if isinstance(value, dict):
|
|
166
|
+
branch = parent.add(f"[cyan]{key}/[/cyan]")
|
|
167
|
+
self._build_tree(branch, value)
|
|
168
|
+
elif isinstance(value, list):
|
|
169
|
+
branch = parent.add(f"[cyan]{key}/[/cyan]")
|
|
170
|
+
for item in value:
|
|
171
|
+
if isinstance(item, dict):
|
|
172
|
+
self._build_tree(branch, item)
|
|
173
|
+
else:
|
|
174
|
+
branch.add(str(item))
|
|
175
|
+
else:
|
|
176
|
+
parent.add(f"{key}: [dim]{value}[/dim]")
|
|
177
|
+
|
|
178
|
+
def _print_tree_fallback(self, items: Dict[str, Any], indent: int = 0) -> None:
|
|
179
|
+
"""Fallback tree printing."""
|
|
180
|
+
for key, value in items.items():
|
|
181
|
+
if isinstance(value, dict):
|
|
182
|
+
print(" " * indent + f"{key}/")
|
|
183
|
+
self._print_tree_fallback(value, indent + 2)
|
|
184
|
+
elif isinstance(value, list):
|
|
185
|
+
print(" " * indent + f"{key}/")
|
|
186
|
+
for item in value:
|
|
187
|
+
print(" " * (indent + 2) + str(item))
|
|
188
|
+
else:
|
|
189
|
+
print(" " * indent + f"{key}: {value}")
|
|
190
|
+
|
|
191
|
+
def code(self, code: str, language: str = "xml", title: Optional[str] = None) -> None:
|
|
192
|
+
"""Print syntax-highlighted code."""
|
|
193
|
+
if self.quiet:
|
|
194
|
+
return
|
|
195
|
+
if self.console:
|
|
196
|
+
syntax = Syntax(code, language, theme="monokai", line_numbers=True)
|
|
197
|
+
if title:
|
|
198
|
+
self.console.print(Panel(syntax, title=title, border_style="green"))
|
|
199
|
+
else:
|
|
200
|
+
self.console.print(syntax)
|
|
201
|
+
else:
|
|
202
|
+
if title:
|
|
203
|
+
print(f"\n--- {title} ---")
|
|
204
|
+
print(code)
|
|
205
|
+
|
|
206
|
+
@contextmanager
|
|
207
|
+
def progress(self, description: str = "Working...", total: Optional[int] = None):
|
|
208
|
+
"""Context manager for progress indication."""
|
|
209
|
+
if self.quiet or not self.console:
|
|
210
|
+
yield None
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
if total:
|
|
214
|
+
with Progress(
|
|
215
|
+
SpinnerColumn(),
|
|
216
|
+
TextColumn("[progress.description]{task.description}"),
|
|
217
|
+
BarColumn(),
|
|
218
|
+
TaskProgressColumn(),
|
|
219
|
+
console=self.console
|
|
220
|
+
) as progress:
|
|
221
|
+
task = progress.add_task(description, total=total)
|
|
222
|
+
yield lambda n=1: progress.update(task, advance=n)
|
|
223
|
+
else:
|
|
224
|
+
with Progress(
|
|
225
|
+
SpinnerColumn(),
|
|
226
|
+
TextColumn("[progress.description]{task.description}"),
|
|
227
|
+
console=self.console
|
|
228
|
+
) as progress:
|
|
229
|
+
task = progress.add_task(description, total=None)
|
|
230
|
+
yield lambda: None
|
|
231
|
+
|
|
232
|
+
def spinner(self, description: str = "Working..."):
|
|
233
|
+
"""Create a spinner context manager."""
|
|
234
|
+
if self.quiet or not self.console:
|
|
235
|
+
@contextmanager
|
|
236
|
+
def noop():
|
|
237
|
+
yield
|
|
238
|
+
return noop()
|
|
239
|
+
|
|
240
|
+
return self.console.status(description, spinner="dots")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Global console instance
|
|
244
|
+
console = QuantumConsole()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_console(quiet: bool = False, no_color: bool = False) -> QuantumConsole:
|
|
248
|
+
"""Get a configured console instance."""
|
|
249
|
+
return QuantumConsole(quiet=quiet, no_color=no_color)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def validate_project_name(name: str) -> tuple[bool, str]:
|
|
253
|
+
"""Validate project name.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Tuple of (is_valid, error_message)
|
|
257
|
+
"""
|
|
258
|
+
import re
|
|
259
|
+
|
|
260
|
+
if not name:
|
|
261
|
+
return False, "Project name cannot be empty"
|
|
262
|
+
|
|
263
|
+
if len(name) < 2:
|
|
264
|
+
return False, "Project name must be at least 2 characters"
|
|
265
|
+
|
|
266
|
+
if len(name) > 50:
|
|
267
|
+
return False, "Project name must be 50 characters or less"
|
|
268
|
+
|
|
269
|
+
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', name):
|
|
270
|
+
return False, "Project name must start with a letter and contain only letters, numbers, underscores, and hyphens"
|
|
271
|
+
|
|
272
|
+
# Check for reserved names
|
|
273
|
+
reserved = ['quantum', 'src', 'dist', 'build', 'node_modules', 'test', 'tests']
|
|
274
|
+
if name.lower() in reserved:
|
|
275
|
+
return False, f"'{name}' is a reserved name"
|
|
276
|
+
|
|
277
|
+
return True, ""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def find_project_root() -> Optional[Path]:
|
|
281
|
+
"""Find the project root by looking for quantum.config.yaml or .q files."""
|
|
282
|
+
current = Path.cwd()
|
|
283
|
+
|
|
284
|
+
# Check current and parent directories
|
|
285
|
+
for path in [current] + list(current.parents):
|
|
286
|
+
if (path / 'quantum.config.yaml').exists():
|
|
287
|
+
return path
|
|
288
|
+
if list(path.glob('*.q')):
|
|
289
|
+
return path
|
|
290
|
+
# Stop at home directory
|
|
291
|
+
if path == Path.home():
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def find_q_files(path: Path, recursive: bool = True) -> List[Path]:
|
|
298
|
+
"""Find all .q files in a directory."""
|
|
299
|
+
if recursive:
|
|
300
|
+
return list(path.rglob('*.q'))
|
|
301
|
+
return list(path.glob('*.q'))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_quantum_version() -> str:
|
|
305
|
+
"""Get Quantum version from package or default."""
|
|
306
|
+
try:
|
|
307
|
+
# Try to read from version file
|
|
308
|
+
version_file = Path(__file__).parent.parent / '__version__.py'
|
|
309
|
+
if version_file.exists():
|
|
310
|
+
content = version_file.read_text()
|
|
311
|
+
# Parse version = "x.y.z"
|
|
312
|
+
for line in content.splitlines():
|
|
313
|
+
if line.startswith('version'):
|
|
314
|
+
return line.split('=')[1].strip().strip('"\'')
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
return "1.0.0-dev"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def ensure_dependencies(*packages: str) -> bool:
|
|
322
|
+
"""Check if required packages are installed."""
|
|
323
|
+
missing = []
|
|
324
|
+
for pkg in packages:
|
|
325
|
+
try:
|
|
326
|
+
__import__(pkg.replace('-', '_'))
|
|
327
|
+
except ImportError:
|
|
328
|
+
missing.append(pkg)
|
|
329
|
+
|
|
330
|
+
if missing:
|
|
331
|
+
console.error(f"Missing dependencies: {', '.join(missing)}")
|
|
332
|
+
console.info(f"Install with: pip install {' '.join(missing)}")
|
|
333
|
+
return False
|
|
334
|
+
return True
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Transpiler
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
Transpiles Quantum (.q) files to native Python and JavaScript code.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from quantum.compiler import Transpiler
|
|
9
|
+
|
|
10
|
+
transpiler = Transpiler(target='python')
|
|
11
|
+
result = transpiler.compile_file('app.q')
|
|
12
|
+
print(result.code)
|
|
13
|
+
|
|
14
|
+
CLI:
|
|
15
|
+
quantum compile app.q -o app.py
|
|
16
|
+
quantum compile app.q -o app.js --target=javascript
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .transpiler import Transpiler, CompilationResult
|
|
20
|
+
from .base_generator import CodeGenerator
|
|
21
|
+
from .expression_transformer import ExpressionTransformer
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'Transpiler',
|
|
25
|
+
'CompilationResult',
|
|
26
|
+
'CodeGenerator',
|
|
27
|
+
'ExpressionTransformer',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
__version__ = '1.0.0'
|