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
quantum/cli/deploy.py
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum CLI Deploy Command
|
|
3
|
+
|
|
4
|
+
Deploy Quantum applications to remote servers.
|
|
5
|
+
Usage: quantum deploy ./my-app --name my-app --target forge
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import fnmatch
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
import tempfile
|
|
13
|
+
import json
|
|
14
|
+
import argparse
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Dict, List
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
# Try to import requests, provide helpful error if missing
|
|
20
|
+
try:
|
|
21
|
+
import requests
|
|
22
|
+
except ImportError:
|
|
23
|
+
print("Error: 'requests' package is required for deploy functionality.")
|
|
24
|
+
print("Install it with: pip install requests")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeployError(Exception):
|
|
29
|
+
"""Deployment error."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class QuantumDeployer:
|
|
34
|
+
"""Handles Quantum application deployments."""
|
|
35
|
+
|
|
36
|
+
# Files/directories to exclude from deployment
|
|
37
|
+
EXCLUDE_PATTERNS = [
|
|
38
|
+
'.git',
|
|
39
|
+
'.gitignore',
|
|
40
|
+
'__pycache__',
|
|
41
|
+
'*.pyc',
|
|
42
|
+
'*.pyo',
|
|
43
|
+
'.env',
|
|
44
|
+
'.env.local',
|
|
45
|
+
'.env.*.local',
|
|
46
|
+
# `.env` sozinho so casava com o arquivo com esse nome exato:
|
|
47
|
+
# `.env.production` e `.env.prod` iam para o servidor junto com o
|
|
48
|
+
# resto.
|
|
49
|
+
'.env.*',
|
|
50
|
+
'node_modules',
|
|
51
|
+
'.venv',
|
|
52
|
+
'venv',
|
|
53
|
+
'*.log',
|
|
54
|
+
'.DS_Store',
|
|
55
|
+
'Thumbs.db',
|
|
56
|
+
'.idea',
|
|
57
|
+
'.vscode',
|
|
58
|
+
'*.sqlite',
|
|
59
|
+
'*.db',
|
|
60
|
+
'tests',
|
|
61
|
+
'test_*.py',
|
|
62
|
+
'*_test.py',
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def __init__(self, config_path: str = 'quantum.config.yaml'):
|
|
66
|
+
"""Initialize deployer with configuration."""
|
|
67
|
+
self.config = self._load_config(config_path)
|
|
68
|
+
self.targets = self.config.get('deploy', {}).get('targets', {})
|
|
69
|
+
|
|
70
|
+
# O que `.env.*` NAO deve levar junto.
|
|
71
|
+
#
|
|
72
|
+
# `.env.example` e o oposto de um segredo: e a documentacao de quais
|
|
73
|
+
# variaveis o operador precisa definir, e e o arquivo que se QUER no
|
|
74
|
+
# servidor. Um `.env.*` cru arrastava esses tambem — trocar um vazamento
|
|
75
|
+
# por uma omissao silenciosa nao e conserto.
|
|
76
|
+
KEEP_PATTERNS = [
|
|
77
|
+
'.env.example',
|
|
78
|
+
'.env.sample',
|
|
79
|
+
'.env.template',
|
|
80
|
+
'.env.dist',
|
|
81
|
+
'.env.*.example',
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def _is_excluded(cls, name: str) -> bool:
|
|
86
|
+
"""O nome casa com algum EXCLUDE_PATTERN?
|
|
87
|
+
|
|
88
|
+
Isto era um casador escrito a mao que so entendia quatro formas:
|
|
89
|
+
`*x*`, `*.ext`, `prefixo*` e igualdade exata. Um `*` no MEIO caia no
|
|
90
|
+
ultimo caso, e tres patterns da lista tem um:
|
|
91
|
+
|
|
92
|
+
.env.*.local -> `.env.production.local` era EMPACOTADO E
|
|
93
|
+
ENVIADO ao servidor. Segredo de producao no
|
|
94
|
+
tarball, sem aviso nenhum.
|
|
95
|
+
test_*.py -> testes iam junto
|
|
96
|
+
*_test.py -> idem
|
|
97
|
+
|
|
98
|
+
Nenhum dos tres casava com nada, a nao ser com um arquivo chamado
|
|
99
|
+
literalmente `.env.*.local`. fnmatch entende as tres formas — e as
|
|
100
|
+
outras continuam funcionando igual.
|
|
101
|
+
|
|
102
|
+
`fnmatchcase`, nao `fnmatch`: o segundo normaliza a caixa conforme o
|
|
103
|
+
sistema, entao a mesma lista excluiria coisas diferentes no Windows e
|
|
104
|
+
no Linux, e um deploy feito de um lado subiria um conjunto de
|
|
105
|
+
arquivos diferente do outro.
|
|
106
|
+
"""
|
|
107
|
+
if any(fnmatch.fnmatchcase(name, pattern)
|
|
108
|
+
for pattern in cls.KEEP_PATTERNS):
|
|
109
|
+
return False
|
|
110
|
+
return any(fnmatch.fnmatchcase(name, pattern)
|
|
111
|
+
for pattern in cls.EXCLUDE_PATTERNS)
|
|
112
|
+
|
|
113
|
+
def _load_config(self, config_path: str) -> Dict:
|
|
114
|
+
"""Load configuration from file."""
|
|
115
|
+
config_file = Path(config_path)
|
|
116
|
+
if config_file.exists():
|
|
117
|
+
with open(config_file) as f:
|
|
118
|
+
return yaml.safe_load(f) or {}
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
def deploy(
|
|
122
|
+
self,
|
|
123
|
+
path: str,
|
|
124
|
+
name: Optional[str] = None,
|
|
125
|
+
target: str = 'forge',
|
|
126
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
127
|
+
force: bool = False,
|
|
128
|
+
verbose: bool = False
|
|
129
|
+
) -> Dict:
|
|
130
|
+
"""
|
|
131
|
+
Deploy a Quantum application.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
path: Path to the application directory
|
|
135
|
+
name: Application name (default: directory name)
|
|
136
|
+
target: Deployment target (default: forge)
|
|
137
|
+
env_vars: Environment variables to set
|
|
138
|
+
force: Overwrite existing deployment
|
|
139
|
+
verbose: Show detailed output
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Deployment result dictionary
|
|
143
|
+
"""
|
|
144
|
+
app_path = Path(path).resolve()
|
|
145
|
+
|
|
146
|
+
# Validate path
|
|
147
|
+
if not app_path.exists():
|
|
148
|
+
raise DeployError(f"Path does not exist: {app_path}")
|
|
149
|
+
|
|
150
|
+
if not app_path.is_dir():
|
|
151
|
+
raise DeployError(f"Path is not a directory: {app_path}")
|
|
152
|
+
|
|
153
|
+
# Validate it's a Quantum app
|
|
154
|
+
if not self._is_quantum_app(app_path):
|
|
155
|
+
raise DeployError(
|
|
156
|
+
f"Not a valid Quantum app: {app_path}\n"
|
|
157
|
+
"Must contain .q files or quantum.config.yaml"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Determine app name
|
|
161
|
+
if not name:
|
|
162
|
+
name = app_path.name.lower().replace('_', '-').replace(' ', '-')
|
|
163
|
+
|
|
164
|
+
# Validate name
|
|
165
|
+
if not self._validate_name(name):
|
|
166
|
+
raise DeployError(
|
|
167
|
+
f"Invalid app name: {name}\n"
|
|
168
|
+
"Use only lowercase letters, numbers, and hyphens. "
|
|
169
|
+
"Must start with a letter (3-50 chars)."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Get target configuration
|
|
173
|
+
target_config = self._get_target_config(target)
|
|
174
|
+
|
|
175
|
+
if verbose:
|
|
176
|
+
print(f"[INFO] Deploying {name} to {target}")
|
|
177
|
+
print(f"[INFO] Source: {app_path}")
|
|
178
|
+
print(f"[INFO] Target URL: {target_config['url']}")
|
|
179
|
+
|
|
180
|
+
# Create tarball
|
|
181
|
+
print(f"[PACK] Creating deployment package...")
|
|
182
|
+
tarball_path = self._create_tarball(app_path, name, verbose)
|
|
183
|
+
|
|
184
|
+
if verbose:
|
|
185
|
+
size_mb = tarball_path.stat().st_size / (1024 * 1024)
|
|
186
|
+
print(f"[INFO] Package size: {size_mb:.2f} MB")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Upload and deploy
|
|
190
|
+
print(f"[UPLOAD] Uploading to {target}...")
|
|
191
|
+
result = self._upload_deploy(
|
|
192
|
+
tarball_path=tarball_path,
|
|
193
|
+
name=name,
|
|
194
|
+
target_config=target_config,
|
|
195
|
+
env_vars=env_vars,
|
|
196
|
+
force=force,
|
|
197
|
+
verbose=verbose
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
print(f"[SUCCESS] Deployed successfully!")
|
|
201
|
+
print(f"[URL] {result.get('url', 'N/A')}")
|
|
202
|
+
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
finally:
|
|
206
|
+
# Cleanup tarball
|
|
207
|
+
if tarball_path.exists():
|
|
208
|
+
tarball_path.unlink()
|
|
209
|
+
|
|
210
|
+
def _is_quantum_app(self, path: Path) -> bool:
|
|
211
|
+
"""Check if path contains a Quantum application."""
|
|
212
|
+
# Check for .q files
|
|
213
|
+
q_files = list(path.glob('**/*.q'))
|
|
214
|
+
if q_files:
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
# Check for quantum.config.yaml
|
|
218
|
+
if (path / 'quantum.config.yaml').exists():
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
# Check for components directory with .q files
|
|
222
|
+
components_dir = path / 'components'
|
|
223
|
+
if components_dir.exists():
|
|
224
|
+
q_files = list(components_dir.glob('**/*.q'))
|
|
225
|
+
if q_files:
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
def _validate_name(self, name: str) -> bool:
|
|
231
|
+
"""Validate application name."""
|
|
232
|
+
import re
|
|
233
|
+
pattern = r'^[a-z][a-z0-9-]{2,49}$'
|
|
234
|
+
return bool(re.match(pattern, name))
|
|
235
|
+
|
|
236
|
+
def _get_target_config(self, target: str) -> Dict:
|
|
237
|
+
"""Get configuration for deployment target."""
|
|
238
|
+
# Check configured targets
|
|
239
|
+
if target in self.targets:
|
|
240
|
+
config = self.targets[target]
|
|
241
|
+
return {
|
|
242
|
+
'url': config.get('url'),
|
|
243
|
+
'api_key': config.get('api_key') or os.environ.get('QUANTUM_API_KEY'),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Check environment variable
|
|
247
|
+
env_url = os.environ.get(f'QUANTUM_DEPLOY_{target.upper()}_URL')
|
|
248
|
+
if env_url:
|
|
249
|
+
return {
|
|
250
|
+
'url': env_url,
|
|
251
|
+
'api_key': os.environ.get(f'QUANTUM_DEPLOY_{target.upper()}_KEY') or
|
|
252
|
+
os.environ.get('QUANTUM_API_KEY'),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Default forge configuration
|
|
256
|
+
if target == 'forge':
|
|
257
|
+
return {
|
|
258
|
+
'url': os.environ.get('QUANTUM_FORGE_URL', 'http://forge:9000'),
|
|
259
|
+
'api_key': os.environ.get('QUANTUM_API_KEY'),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
raise DeployError(
|
|
263
|
+
f"Unknown deployment target: {target}\n"
|
|
264
|
+
"Configure targets in quantum.config.yaml or set environment variables:\n"
|
|
265
|
+
f" QUANTUM_DEPLOY_{target.upper()}_URL\n"
|
|
266
|
+
f" QUANTUM_DEPLOY_{target.upper()}_KEY"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _create_tarball(self, app_path: Path, name: str, verbose: bool = False) -> Path:
|
|
270
|
+
"""Create deployment tarball."""
|
|
271
|
+
# Create temp file
|
|
272
|
+
fd, tarball_path = tempfile.mkstemp(suffix='.tar.gz', prefix=f'quantum-{name}-')
|
|
273
|
+
os.close(fd)
|
|
274
|
+
tarball_path = Path(tarball_path)
|
|
275
|
+
|
|
276
|
+
def should_exclude(item: Path, raiz: Path) -> bool:
|
|
277
|
+
"""O item, ou algum diretorio dele ATE `raiz`, e excluido?
|
|
278
|
+
|
|
279
|
+
So ate a raiz. Antes era `[item] + list(item.parents)`, e
|
|
280
|
+
`parents` sobe ate a raiz do disco: um app em
|
|
281
|
+
`C:\\...\\venv\\meuapp` ou `~/projetos/tests/app` casava com um
|
|
282
|
+
pattern num diretorio de FORA do app, e o tarball saia vazio —
|
|
283
|
+
"deploy com sucesso" de nada.
|
|
284
|
+
"""
|
|
285
|
+
return any(self._is_excluded(parte)
|
|
286
|
+
for parte in item.relative_to(raiz).parts)
|
|
287
|
+
|
|
288
|
+
with tarfile.open(tarball_path, 'w:gz') as tar:
|
|
289
|
+
# Add app files
|
|
290
|
+
for item in app_path.rglob('*'):
|
|
291
|
+
# Skip excluded items
|
|
292
|
+
if should_exclude(item, app_path):
|
|
293
|
+
if verbose:
|
|
294
|
+
print(f" [SKIP] {item.relative_to(app_path)}")
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Get relative path
|
|
298
|
+
rel_path = item.relative_to(app_path)
|
|
299
|
+
|
|
300
|
+
if item.is_file():
|
|
301
|
+
if verbose:
|
|
302
|
+
print(f" [ADD] {rel_path}")
|
|
303
|
+
tar.add(item, arcname=str(rel_path))
|
|
304
|
+
|
|
305
|
+
# Include quantum src/ if --include-runtime flag is used or if src/ doesn't exist in app
|
|
306
|
+
# This ensures the latest runtime code is deployed
|
|
307
|
+
include_runtime = self.config.get('deploy', {}).get('include_runtime', True)
|
|
308
|
+
app_has_src = (app_path / 'src').exists()
|
|
309
|
+
|
|
310
|
+
if include_runtime and not app_has_src:
|
|
311
|
+
# Find the quantum src directory
|
|
312
|
+
quantum_src = self._find_quantum_src()
|
|
313
|
+
if quantum_src and quantum_src.exists():
|
|
314
|
+
if verbose:
|
|
315
|
+
print(f" [INFO] Including quantum runtime from {quantum_src}")
|
|
316
|
+
for item in quantum_src.rglob('*'):
|
|
317
|
+
# Tambem pelos diretorios: antes so o NOME do arquivo
|
|
318
|
+
# era testado, entao `tests/` do runtime era pulado
|
|
319
|
+
# como diretorio mas todo arquivo dentro dele ia
|
|
320
|
+
# junto.
|
|
321
|
+
if should_exclude(item, quantum_src):
|
|
322
|
+
continue
|
|
323
|
+
if item.is_file():
|
|
324
|
+
rel_path = item.relative_to(quantum_src)
|
|
325
|
+
arcname = f'src/{rel_path}'
|
|
326
|
+
if verbose:
|
|
327
|
+
print(f" [ADD] {arcname}")
|
|
328
|
+
tar.add(item, arcname=arcname)
|
|
329
|
+
|
|
330
|
+
# Add cache bust file to invalidate Docker build cache
|
|
331
|
+
import time
|
|
332
|
+
cache_bust_content = f"deploy_timestamp={time.time()}\n".encode('utf-8')
|
|
333
|
+
import io
|
|
334
|
+
cache_info = tarfile.TarInfo(name='src/.cache_bust')
|
|
335
|
+
cache_info.size = len(cache_bust_content)
|
|
336
|
+
tar.addfile(cache_info, io.BytesIO(cache_bust_content))
|
|
337
|
+
|
|
338
|
+
return tarball_path
|
|
339
|
+
|
|
340
|
+
def _find_quantum_src(self) -> Optional[Path]:
|
|
341
|
+
"""Find the quantum src directory."""
|
|
342
|
+
# Check relative to this file (in case running from installed package)
|
|
343
|
+
cli_dir = Path(__file__).parent
|
|
344
|
+
quantum_src = cli_dir.parent # src/
|
|
345
|
+
if (quantum_src / 'runtime').exists():
|
|
346
|
+
return quantum_src
|
|
347
|
+
|
|
348
|
+
# Check current working directory structure
|
|
349
|
+
cwd = Path.cwd()
|
|
350
|
+
for candidate in [cwd / 'src', cwd.parent / 'src', cwd.parent.parent / 'src']:
|
|
351
|
+
if candidate.exists() and (candidate / 'runtime').exists():
|
|
352
|
+
return candidate
|
|
353
|
+
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
def _upload_deploy(
|
|
357
|
+
self,
|
|
358
|
+
tarball_path: Path,
|
|
359
|
+
name: str,
|
|
360
|
+
target_config: Dict,
|
|
361
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
362
|
+
force: bool = False,
|
|
363
|
+
verbose: bool = False
|
|
364
|
+
) -> Dict:
|
|
365
|
+
"""Upload tarball and trigger deployment."""
|
|
366
|
+
url = f"{target_config['url'].rstrip('/')}/api/deploy"
|
|
367
|
+
|
|
368
|
+
# Prepare headers
|
|
369
|
+
headers = {}
|
|
370
|
+
if target_config.get('api_key'):
|
|
371
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
372
|
+
|
|
373
|
+
# Prepare form data
|
|
374
|
+
data = {
|
|
375
|
+
'name': name,
|
|
376
|
+
'force': str(force).lower(),
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if env_vars:
|
|
380
|
+
data['env_vars'] = json.dumps(env_vars)
|
|
381
|
+
|
|
382
|
+
# Upload file
|
|
383
|
+
with open(tarball_path, 'rb') as f:
|
|
384
|
+
files = {'file': (f'{name}.tar.gz', f, 'application/gzip')}
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
response = requests.post(
|
|
388
|
+
url,
|
|
389
|
+
headers=headers,
|
|
390
|
+
data=data,
|
|
391
|
+
files=files,
|
|
392
|
+
timeout=300 # 5 minute timeout for large deploys
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if response.status_code == 401:
|
|
396
|
+
raise DeployError(
|
|
397
|
+
"Authentication failed. Check your API key.\n"
|
|
398
|
+
"Set QUANTUM_API_KEY environment variable or configure in quantum.config.yaml"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if response.status_code == 409:
|
|
402
|
+
raise DeployError(
|
|
403
|
+
f"App '{name}' already exists. Use --force to overwrite."
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if not response.ok:
|
|
407
|
+
try:
|
|
408
|
+
error = response.json().get('detail', response.text)
|
|
409
|
+
except Exception:
|
|
410
|
+
error = response.text
|
|
411
|
+
raise DeployError(f"Deployment failed: {error}")
|
|
412
|
+
|
|
413
|
+
return response.json()
|
|
414
|
+
|
|
415
|
+
except requests.exceptions.ConnectionError:
|
|
416
|
+
raise DeployError(
|
|
417
|
+
f"Could not connect to {target_config['url']}\n"
|
|
418
|
+
"Ensure the deploy service is running and accessible."
|
|
419
|
+
)
|
|
420
|
+
except requests.exceptions.Timeout:
|
|
421
|
+
raise DeployError("Deployment timed out. The server may be busy.")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class QuantumAppsManager:
|
|
425
|
+
"""Manages deployed Quantum applications."""
|
|
426
|
+
|
|
427
|
+
def __init__(self, config_path: str = 'quantum.config.yaml'):
|
|
428
|
+
"""Initialize apps manager."""
|
|
429
|
+
self.deployer = QuantumDeployer(config_path)
|
|
430
|
+
|
|
431
|
+
def list_apps(self, target: str = 'forge', verbose: bool = False) -> List[Dict]:
|
|
432
|
+
"""List deployed applications."""
|
|
433
|
+
target_config = self.deployer._get_target_config(target)
|
|
434
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps"
|
|
435
|
+
|
|
436
|
+
headers = {}
|
|
437
|
+
if target_config.get('api_key'):
|
|
438
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
442
|
+
response.raise_for_status()
|
|
443
|
+
return response.json().get('apps', [])
|
|
444
|
+
except requests.exceptions.ConnectionError as e:
|
|
445
|
+
# O alvo default e `http://forge:9000`, um host que so resolve na
|
|
446
|
+
# rede de quem escreveu isto. Para todo mundo, `quantum apps`
|
|
447
|
+
# falhava com um erro de DNS citando um nome nunca visto e sem
|
|
448
|
+
# dizer de onde ele veio.
|
|
449
|
+
raise DeployError(
|
|
450
|
+
f"Nao consegui falar com o servidor de deploy em {url}.\n"
|
|
451
|
+
f" Esse e o alvo '{target}'. Se voce nao tem um servidor "
|
|
452
|
+
f"Quantum Forge, este comando nao se aplica a voce.\n"
|
|
453
|
+
f" Para apontar para outro lugar: "
|
|
454
|
+
f"QUANTUM_FORGE_URL=http://seu-host:porta\n"
|
|
455
|
+
f" Detalhe: {e}"
|
|
456
|
+
)
|
|
457
|
+
except requests.exceptions.RequestException as e:
|
|
458
|
+
raise DeployError(f"Failed to list apps: {e}")
|
|
459
|
+
|
|
460
|
+
def get_status(self, name: str, target: str = 'forge') -> Dict:
|
|
461
|
+
"""Get application status."""
|
|
462
|
+
target_config = self.deployer._get_target_config(target)
|
|
463
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps/{name}"
|
|
464
|
+
|
|
465
|
+
headers = {}
|
|
466
|
+
if target_config.get('api_key'):
|
|
467
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
471
|
+
if response.status_code == 404:
|
|
472
|
+
raise DeployError(f"App '{name}' not found")
|
|
473
|
+
response.raise_for_status()
|
|
474
|
+
return response.json()
|
|
475
|
+
except requests.exceptions.RequestException as e:
|
|
476
|
+
raise DeployError(f"Failed to get status: {e}")
|
|
477
|
+
|
|
478
|
+
def get_logs(
|
|
479
|
+
self,
|
|
480
|
+
name: str,
|
|
481
|
+
target: str = 'forge',
|
|
482
|
+
lines: int = 100
|
|
483
|
+
) -> str:
|
|
484
|
+
"""Get application logs."""
|
|
485
|
+
target_config = self.deployer._get_target_config(target)
|
|
486
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps/{name}/logs"
|
|
487
|
+
|
|
488
|
+
headers = {}
|
|
489
|
+
if target_config.get('api_key'):
|
|
490
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
491
|
+
|
|
492
|
+
params = {'lines': lines}
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
response = requests.get(url, headers=headers, params=params, timeout=30)
|
|
496
|
+
if response.status_code == 404:
|
|
497
|
+
raise DeployError(f"App '{name}' not found")
|
|
498
|
+
response.raise_for_status()
|
|
499
|
+
return response.json().get('logs', '')
|
|
500
|
+
except requests.exceptions.RequestException as e:
|
|
501
|
+
raise DeployError(f"Failed to get logs: {e}")
|
|
502
|
+
|
|
503
|
+
def restart(self, name: str, target: str = 'forge') -> Dict:
|
|
504
|
+
"""Restart an application."""
|
|
505
|
+
target_config = self.deployer._get_target_config(target)
|
|
506
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps/{name}/restart"
|
|
507
|
+
|
|
508
|
+
headers = {}
|
|
509
|
+
if target_config.get('api_key'):
|
|
510
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
response = requests.post(url, headers=headers, timeout=60)
|
|
514
|
+
if response.status_code == 404:
|
|
515
|
+
raise DeployError(f"App '{name}' not found")
|
|
516
|
+
response.raise_for_status()
|
|
517
|
+
return response.json()
|
|
518
|
+
except requests.exceptions.RequestException as e:
|
|
519
|
+
raise DeployError(f"Failed to restart app: {e}")
|
|
520
|
+
|
|
521
|
+
def stop(self, name: str, target: str = 'forge') -> Dict:
|
|
522
|
+
"""Stop an application."""
|
|
523
|
+
target_config = self.deployer._get_target_config(target)
|
|
524
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps/{name}/stop"
|
|
525
|
+
|
|
526
|
+
headers = {}
|
|
527
|
+
if target_config.get('api_key'):
|
|
528
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
response = requests.post(url, headers=headers, timeout=60)
|
|
532
|
+
if response.status_code == 404:
|
|
533
|
+
raise DeployError(f"App '{name}' not found")
|
|
534
|
+
response.raise_for_status()
|
|
535
|
+
return response.json()
|
|
536
|
+
except requests.exceptions.RequestException as e:
|
|
537
|
+
raise DeployError(f"Failed to stop app: {e}")
|
|
538
|
+
|
|
539
|
+
def start(self, name: str, target: str = 'forge') -> Dict:
|
|
540
|
+
"""Start a stopped application."""
|
|
541
|
+
target_config = self.deployer._get_target_config(target)
|
|
542
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps/{name}/start"
|
|
543
|
+
|
|
544
|
+
headers = {}
|
|
545
|
+
if target_config.get('api_key'):
|
|
546
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
response = requests.post(url, headers=headers, timeout=60)
|
|
550
|
+
if response.status_code == 404:
|
|
551
|
+
raise DeployError(f"App '{name}' not found")
|
|
552
|
+
response.raise_for_status()
|
|
553
|
+
return response.json()
|
|
554
|
+
except requests.exceptions.RequestException as e:
|
|
555
|
+
raise DeployError(f"Failed to start app: {e}")
|
|
556
|
+
|
|
557
|
+
def remove(self, name: str, target: str = 'forge', keep_data: bool = False) -> Dict:
|
|
558
|
+
"""Remove an application."""
|
|
559
|
+
target_config = self.deployer._get_target_config(target)
|
|
560
|
+
url = f"{target_config['url'].rstrip('/')}/api/apps/{name}"
|
|
561
|
+
|
|
562
|
+
headers = {}
|
|
563
|
+
if target_config.get('api_key'):
|
|
564
|
+
headers['X-API-Key'] = target_config['api_key']
|
|
565
|
+
|
|
566
|
+
params = {'keep_data': str(keep_data).lower()}
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
response = requests.delete(url, headers=headers, params=params, timeout=60)
|
|
570
|
+
if response.status_code == 404:
|
|
571
|
+
raise DeployError(f"App '{name}' not found")
|
|
572
|
+
response.raise_for_status()
|
|
573
|
+
return response.json()
|
|
574
|
+
except requests.exceptions.RequestException as e:
|
|
575
|
+
raise DeployError(f"Failed to remove app: {e}")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def create_deploy_parser(subparsers) -> argparse.ArgumentParser:
|
|
579
|
+
"""Create deploy subcommand parser."""
|
|
580
|
+
deploy_parser = subparsers.add_parser(
|
|
581
|
+
'deploy',
|
|
582
|
+
help='Deploy a Quantum application',
|
|
583
|
+
description='Deploy a Quantum application to a remote server',
|
|
584
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
585
|
+
epilog="""
|
|
586
|
+
Examples:
|
|
587
|
+
quantum deploy . # Deploy current directory
|
|
588
|
+
quantum deploy ./my-app --name my-app # Deploy with custom name
|
|
589
|
+
quantum deploy . --target forge --force # Force redeploy to forge
|
|
590
|
+
quantum deploy . --env DB_HOST=postgres # Set environment variables
|
|
591
|
+
"""
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
deploy_parser.add_argument(
|
|
595
|
+
'path',
|
|
596
|
+
nargs='?',
|
|
597
|
+
default='.',
|
|
598
|
+
help='Path to the application directory (default: current directory)'
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
deploy_parser.add_argument(
|
|
602
|
+
'--name', '-n',
|
|
603
|
+
help='Application name (default: directory name)'
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
deploy_parser.add_argument(
|
|
607
|
+
'--target', '-t',
|
|
608
|
+
default='forge',
|
|
609
|
+
help='Deployment target (default: forge)'
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
deploy_parser.add_argument(
|
|
613
|
+
'--env', '-e',
|
|
614
|
+
action='append',
|
|
615
|
+
dest='env_vars',
|
|
616
|
+
metavar='KEY=VALUE',
|
|
617
|
+
help='Environment variable (can be used multiple times)'
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
deploy_parser.add_argument(
|
|
621
|
+
'--force', '-f',
|
|
622
|
+
action='store_true',
|
|
623
|
+
help='Overwrite existing deployment'
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
deploy_parser.add_argument(
|
|
627
|
+
'--verbose', '-v',
|
|
628
|
+
action='store_true',
|
|
629
|
+
help='Show detailed output'
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
return deploy_parser
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def create_apps_parser(subparsers) -> argparse.ArgumentParser:
|
|
636
|
+
"""Create apps subcommand parser."""
|
|
637
|
+
apps_parser = subparsers.add_parser(
|
|
638
|
+
'apps',
|
|
639
|
+
help='Manage deployed applications',
|
|
640
|
+
description='List and manage deployed Quantum applications',
|
|
641
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
642
|
+
epilog="""
|
|
643
|
+
Examples:
|
|
644
|
+
quantum apps # List all apps
|
|
645
|
+
quantum apps status my-app # Get app status
|
|
646
|
+
quantum apps logs my-app # View app logs
|
|
647
|
+
quantum apps restart my-app # Restart app
|
|
648
|
+
quantum apps stop my-app # Stop app
|
|
649
|
+
quantum apps start my-app # Start stopped app
|
|
650
|
+
quantum apps remove my-app # Remove app
|
|
651
|
+
"""
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
apps_subparsers = apps_parser.add_subparsers(dest='apps_command')
|
|
655
|
+
|
|
656
|
+
# List (default)
|
|
657
|
+
list_parser = apps_subparsers.add_parser('list', help='List deployed apps')
|
|
658
|
+
list_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
659
|
+
|
|
660
|
+
# Status
|
|
661
|
+
status_parser = apps_subparsers.add_parser('status', help='Get app status')
|
|
662
|
+
status_parser.add_argument('name', help='Application name')
|
|
663
|
+
status_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
664
|
+
|
|
665
|
+
# Logs
|
|
666
|
+
logs_parser = apps_subparsers.add_parser('logs', help='View app logs')
|
|
667
|
+
logs_parser.add_argument('name', help='Application name')
|
|
668
|
+
logs_parser.add_argument('--lines', '-n', type=int, default=100, help='Number of lines')
|
|
669
|
+
logs_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
670
|
+
|
|
671
|
+
# Restart
|
|
672
|
+
restart_parser = apps_subparsers.add_parser('restart', help='Restart app')
|
|
673
|
+
restart_parser.add_argument('name', help='Application name')
|
|
674
|
+
restart_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
675
|
+
|
|
676
|
+
# Stop
|
|
677
|
+
stop_parser = apps_subparsers.add_parser('stop', help='Stop app')
|
|
678
|
+
stop_parser.add_argument('name', help='Application name')
|
|
679
|
+
stop_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
680
|
+
|
|
681
|
+
# Start
|
|
682
|
+
start_parser = apps_subparsers.add_parser('start', help='Start stopped app')
|
|
683
|
+
start_parser.add_argument('name', help='Application name')
|
|
684
|
+
start_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
685
|
+
|
|
686
|
+
# Remove
|
|
687
|
+
remove_parser = apps_subparsers.add_parser('remove', help='Remove app')
|
|
688
|
+
remove_parser.add_argument('name', help='Application name')
|
|
689
|
+
remove_parser.add_argument('--keep-data', action='store_true', help='Keep app data files')
|
|
690
|
+
remove_parser.add_argument('--target', '-t', default='forge', help='Target server')
|
|
691
|
+
|
|
692
|
+
return apps_parser
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def handle_deploy(args) -> int:
|
|
696
|
+
"""Handle deploy command."""
|
|
697
|
+
try:
|
|
698
|
+
deployer = QuantumDeployer()
|
|
699
|
+
|
|
700
|
+
# Parse environment variables
|
|
701
|
+
env_vars = None
|
|
702
|
+
if args.env_vars:
|
|
703
|
+
env_vars = {}
|
|
704
|
+
for env in args.env_vars:
|
|
705
|
+
if '=' not in env:
|
|
706
|
+
print(f"[ERROR] Invalid env var format: {env}")
|
|
707
|
+
print("Use: --env KEY=VALUE")
|
|
708
|
+
return 1
|
|
709
|
+
key, value = env.split('=', 1)
|
|
710
|
+
env_vars[key] = value
|
|
711
|
+
|
|
712
|
+
deployer.deploy(
|
|
713
|
+
path=args.path,
|
|
714
|
+
name=args.name,
|
|
715
|
+
target=args.target,
|
|
716
|
+
env_vars=env_vars,
|
|
717
|
+
force=args.force,
|
|
718
|
+
verbose=args.verbose
|
|
719
|
+
)
|
|
720
|
+
return 0
|
|
721
|
+
|
|
722
|
+
except DeployError as e:
|
|
723
|
+
print(f"[ERROR] {e}")
|
|
724
|
+
return 1
|
|
725
|
+
except Exception as e:
|
|
726
|
+
print(f"[ERROR] Unexpected error: {e}")
|
|
727
|
+
return 1
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def handle_apps(args) -> int:
|
|
731
|
+
"""Handle apps command."""
|
|
732
|
+
try:
|
|
733
|
+
manager = QuantumAppsManager()
|
|
734
|
+
target = getattr(args, 'target', 'forge')
|
|
735
|
+
|
|
736
|
+
# Default to list if no subcommand
|
|
737
|
+
command = args.apps_command or 'list'
|
|
738
|
+
|
|
739
|
+
if command == 'list':
|
|
740
|
+
apps = manager.list_apps(target)
|
|
741
|
+
if not apps:
|
|
742
|
+
print("No apps deployed.")
|
|
743
|
+
return 0
|
|
744
|
+
|
|
745
|
+
print(f"{'NAME':<20} {'STATUS':<10} {'VERSION':<8} {'URL'}")
|
|
746
|
+
print('-' * 70)
|
|
747
|
+
for app in apps:
|
|
748
|
+
print(f"{app['name']:<20} {app['status']:<10} v{app['version']:<6} {app['url']}")
|
|
749
|
+
|
|
750
|
+
elif command == 'status':
|
|
751
|
+
status = manager.get_status(args.name, target)
|
|
752
|
+
print(f"Name: {status['name']}")
|
|
753
|
+
print(f"Status: {status['status']}")
|
|
754
|
+
print(f"Container: {status.get('container_id', 'N/A')}")
|
|
755
|
+
print(f"Version: v{status['version']}")
|
|
756
|
+
print(f"URL: {status['url']}")
|
|
757
|
+
if status.get('health'):
|
|
758
|
+
print(f"Health: {status['health'].get('status', 'N/A')}")
|
|
759
|
+
if status.get('resources'):
|
|
760
|
+
res = status['resources']
|
|
761
|
+
print(f"CPU: {res.get('cpu_percent', 0):.1f}%")
|
|
762
|
+
print(f"Memory: {res.get('memory_usage_mb', 0):.1f} MB ({res.get('memory_percent', 0):.1f}%)")
|
|
763
|
+
|
|
764
|
+
elif command == 'logs':
|
|
765
|
+
logs = manager.get_logs(args.name, target, args.lines)
|
|
766
|
+
print(logs)
|
|
767
|
+
|
|
768
|
+
elif command == 'restart':
|
|
769
|
+
result = manager.restart(args.name, target)
|
|
770
|
+
print(f"[SUCCESS] {result.get('message', 'App restarted')}")
|
|
771
|
+
|
|
772
|
+
elif command == 'stop':
|
|
773
|
+
result = manager.stop(args.name, target)
|
|
774
|
+
print(f"[SUCCESS] {result.get('message', 'App stopped')}")
|
|
775
|
+
|
|
776
|
+
elif command == 'start':
|
|
777
|
+
result = manager.start(args.name, target)
|
|
778
|
+
print(f"[SUCCESS] {result.get('message', 'App started')}")
|
|
779
|
+
|
|
780
|
+
elif command == 'remove':
|
|
781
|
+
result = manager.remove(args.name, target, getattr(args, 'keep_data', False))
|
|
782
|
+
print(f"[SUCCESS] {result.get('message', 'App removed')}")
|
|
783
|
+
|
|
784
|
+
return 0
|
|
785
|
+
|
|
786
|
+
except DeployError as e:
|
|
787
|
+
print(f"[ERROR] {e}")
|
|
788
|
+
return 1
|
|
789
|
+
except Exception as e:
|
|
790
|
+
print(f"[ERROR] Unexpected error: {e}")
|
|
791
|
+
return 1
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
if __name__ == '__main__':
|
|
795
|
+
# For standalone testing
|
|
796
|
+
parser = argparse.ArgumentParser(description='Quantum Deploy CLI')
|
|
797
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
798
|
+
|
|
799
|
+
create_deploy_parser(subparsers)
|
|
800
|
+
create_apps_parser(subparsers)
|
|
801
|
+
|
|
802
|
+
args = parser.parse_args()
|
|
803
|
+
|
|
804
|
+
if args.command == 'deploy':
|
|
805
|
+
sys.exit(handle_deploy(args))
|
|
806
|
+
elif args.command == 'apps':
|
|
807
|
+
sys.exit(handle_apps(args))
|
|
808
|
+
else:
|
|
809
|
+
parser.print_help()
|
|
810
|
+
sys.exit(1)
|