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.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. 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)