quantum-framework 0.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -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 +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,698 @@
1
+ """
2
+ Deploy Service for Quantum Admin
3
+ Handles building and deploying Quantum applications
4
+ """
5
+ import os
6
+ import subprocess
7
+ import logging
8
+ import threading
9
+ import uuid
10
+ from datetime import datetime
11
+ from typing import Optional, Dict, Any, List
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class DeployStatus(Enum):
19
+ PENDING = "pending"
20
+ RUNNING = "running"
21
+ COMPLETED = "completed"
22
+ FAILED = "failed"
23
+ CANCELLED = "cancelled"
24
+ ROLLED_BACK = "rolled_back"
25
+
26
+
27
+ class DeployStep(Enum):
28
+ PREPARE = "prepare"
29
+ BUILD = "build"
30
+ PUSH = "push"
31
+ DEPLOY = "deploy"
32
+ HEALTH = "health"
33
+
34
+
35
+ @dataclass
36
+ class StepResult:
37
+ name: str
38
+ status: str # pending, running, completed, failed, skipped
39
+ message: str = ""
40
+ started_at: Optional[datetime] = None
41
+ completed_at: Optional[datetime] = None
42
+
43
+
44
+ @dataclass
45
+ class Deployment:
46
+ id: str
47
+ project_id: int
48
+ project_name: str
49
+ environment: str
50
+ branch: str
51
+ strategy: str
52
+ status: DeployStatus
53
+ steps: List[StepResult] = field(default_factory=list)
54
+ logs: str = ""
55
+ started_at: Optional[datetime] = None
56
+ completed_at: Optional[datetime] = None
57
+ triggered_by: str = "user"
58
+ error_message: Optional[str] = None
59
+
60
+ def to_dict(self) -> Dict[str, Any]:
61
+ return {
62
+ "id": self.id,
63
+ "project_id": self.project_id,
64
+ "project_name": self.project_name,
65
+ "environment": self.environment,
66
+ "branch": self.branch,
67
+ "strategy": self.strategy,
68
+ "status": self.status.value,
69
+ "steps": [{"name": s.name, "status": s.status, "message": s.message} for s in self.steps],
70
+ "logs": self.logs,
71
+ "started_at": self.started_at.isoformat() if self.started_at else None,
72
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
73
+ "triggered_by": self.triggered_by,
74
+ "error_message": self.error_message
75
+ }
76
+
77
+
78
+ class DeployService:
79
+ """Service for deploying Quantum applications"""
80
+
81
+ def __init__(self, quantum_root: str = None):
82
+ """
83
+ Initialize deploy service
84
+
85
+ Args:
86
+ quantum_root: Root directory of Quantum installation
87
+ """
88
+ self.quantum_root = quantum_root or os.environ.get(
89
+ "QUANTUM_ROOT",
90
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
91
+ )
92
+ self._deployments: Dict[str, Deployment] = {}
93
+ self._lock = threading.Lock()
94
+
95
+ def _log(self, deployment: Deployment, message: str):
96
+ """Add log message to deployment"""
97
+ timestamp = datetime.now().strftime("%H:%M:%S")
98
+ deployment.logs += f"[{timestamp}] {message}\n"
99
+ logger.info(f"[Deploy {deployment.id}] {message}")
100
+
101
+ def _update_step(self, deployment: Deployment, step_name: str, status: str, message: str = ""):
102
+ """Update step status"""
103
+ for step in deployment.steps:
104
+ if step.name == step_name:
105
+ step.status = status
106
+ step.message = message
107
+ if status == "running":
108
+ step.started_at = datetime.now()
109
+ elif status in ("completed", "failed", "skipped"):
110
+ step.completed_at = datetime.now()
111
+ return
112
+
113
+ # A name that matches no step wrote nowhere and said nothing — a
114
+ # typo here made a whole step's status silently vanish from the
115
+ # screen, which then showed it as still pending.
116
+ logger.warning(
117
+ "deployment %s has no step named %r; the %r update was dropped",
118
+ deployment.id, step_name, status)
119
+
120
+ # =========================================================================
121
+ # Deployment Operations
122
+ # =========================================================================
123
+
124
+ def create_deployment(
125
+ self,
126
+ project_id: int,
127
+ project_name: str,
128
+ environment: str,
129
+ branch: str = "main",
130
+ strategy: str = "rolling",
131
+ triggered_by: str = "user"
132
+ ) -> Deployment:
133
+ """
134
+ Create a new deployment
135
+
136
+ Args:
137
+ project_id: Project database ID
138
+ project_name: Project name
139
+ environment: Target environment (dev, staging, production)
140
+ branch: Git branch to deploy
141
+ strategy: Deployment strategy (rolling, blue-green, recreate)
142
+ triggered_by: Who triggered the deployment
143
+
144
+ Returns:
145
+ Deployment object
146
+ """
147
+ deploy_id = str(uuid.uuid4())[:8]
148
+
149
+ deployment = Deployment(
150
+ id=deploy_id,
151
+ project_id=project_id,
152
+ project_name=project_name,
153
+ environment=environment,
154
+ branch=branch,
155
+ strategy=strategy,
156
+ status=DeployStatus.PENDING,
157
+ steps=[
158
+ StepResult(name="prepare", status="pending"),
159
+ StepResult(name="build", status="pending"),
160
+ StepResult(name="push", status="pending"),
161
+ StepResult(name="deploy", status="pending"),
162
+ StepResult(name="health", status="pending"),
163
+ ],
164
+ started_at=datetime.now(),
165
+ triggered_by=triggered_by
166
+ )
167
+
168
+ with self._lock:
169
+ self._deployments[deploy_id] = deployment
170
+
171
+ return deployment
172
+
173
+ def start_deployment(self, deploy_id: str, run_async: bool = True) -> bool:
174
+ """
175
+ Start a deployment
176
+
177
+ Args:
178
+ deploy_id: Deployment ID
179
+ run_async: Run in background thread
180
+
181
+ Returns:
182
+ True if started successfully
183
+ """
184
+ deployment = self._deployments.get(deploy_id)
185
+ if not deployment:
186
+ return False
187
+
188
+ if deployment.status != DeployStatus.PENDING:
189
+ return False
190
+
191
+ if run_async:
192
+ thread = threading.Thread(target=self._run_deployment, args=(deployment,))
193
+ thread.daemon = True
194
+ thread.start()
195
+ else:
196
+ self._run_deployment(deployment)
197
+
198
+ return True
199
+
200
+ def _mark_failed(self, deployment: Deployment, step_name: str):
201
+ """Registra a falha do deploy no proprio objeto.
202
+
203
+ Um passo que devolvia False fazia `return` daqui e mais nada: o
204
+ status continuava RUNNING para sempre. E a rota que a interface le
205
+ (`main.py`) responde
206
+
207
+ "completed": status in ("completed", "failed", "cancelled", ...)
208
+ "failed": status == "failed"
209
+
210
+ entao um build que falhou aparecia como `completed=false,
211
+ failed=false` — um spinner que nunca para, sem erro nenhum na tela,
212
+ para um deploy que ja tinha terminado e dado errado. So o
213
+ `_step_prepare` marcava FAILED por conta propria; os outros quatro
214
+ passos, nao.
215
+ """
216
+ deployment.status = DeployStatus.FAILED
217
+ deployment.completed_at = datetime.now()
218
+ if not deployment.error_message:
219
+ passo = next(
220
+ (s for s in deployment.steps if s.name == step_name), None)
221
+ deployment.error_message = (
222
+ getattr(passo, 'message', None)
223
+ or f"the '{step_name}' step failed"
224
+ )
225
+ self._log(deployment,
226
+ f"Deployment failed at the '{step_name}' step: "
227
+ f"{deployment.error_message}")
228
+
229
+ def _run_deployment(self, deployment: Deployment):
230
+ """Execute deployment steps"""
231
+ deployment.status = DeployStatus.RUNNING
232
+ self._log(deployment, f"Starting deployment to {deployment.environment}")
233
+
234
+ try:
235
+ # Step 1: Prepare
236
+ if not self._step_prepare(deployment):
237
+ return self._mark_failed(deployment, "prepare")
238
+
239
+ # Step 2: Build
240
+ if not self._step_build(deployment):
241
+ return self._mark_failed(deployment, "build")
242
+
243
+ # Step 3: Push (skip for local deployments)
244
+ if deployment.environment == "local":
245
+ self._update_step(deployment, "push", "skipped", "Skipped for local deployment")
246
+ self._log(deployment, "Push step skipped for local deployment")
247
+ else:
248
+ if not self._step_push(deployment):
249
+ return self._mark_failed(deployment, "push")
250
+
251
+ # Step 4: Deploy
252
+ if not self._step_deploy(deployment):
253
+ return self._mark_failed(deployment, "deploy")
254
+
255
+ # Step 5: Health Check
256
+ if not self._step_health(deployment):
257
+ return self._mark_failed(deployment, "health")
258
+
259
+ # Success!
260
+ deployment.status = DeployStatus.COMPLETED
261
+ deployment.completed_at = datetime.now()
262
+ self._log(deployment, "Deployment completed successfully!")
263
+
264
+ except Exception as e:
265
+ deployment.status = DeployStatus.FAILED
266
+ deployment.error_message = str(e)
267
+ deployment.completed_at = datetime.now()
268
+ self._log(deployment, f"Deployment failed: {e}")
269
+ logger.exception(f"Deployment {deployment.id} failed")
270
+
271
+ def _step_prepare(self, deployment: Deployment) -> bool:
272
+ """Prepare step: validate configuration and environment"""
273
+ self._update_step(deployment, "prepare", "running", "Validating configuration...")
274
+ self._log(deployment, "Preparing deployment...")
275
+
276
+ # Check project directory exists
277
+ project_dir = os.path.join(self.quantum_root, "projects", deployment.project_name)
278
+
279
+ # For now, we'll use the examples directory as a fallback
280
+ if not os.path.exists(project_dir):
281
+ project_dir = os.path.join(self.quantum_root, "examples")
282
+
283
+ if not os.path.exists(project_dir):
284
+ self._update_step(deployment, "prepare", "failed", "Project directory not found")
285
+ self._log(deployment, f"ERROR: Project directory not found: {project_dir}")
286
+ deployment.status = DeployStatus.FAILED
287
+ deployment.error_message = "Project directory not found"
288
+ return False
289
+
290
+ self._update_step(deployment, "prepare", "completed", "Configuration validated")
291
+ self._log(deployment, f"Project directory: {project_dir}")
292
+ return True
293
+
294
+ def _step_build(self, deployment: Deployment) -> bool:
295
+ """Build step: compile Quantum files"""
296
+ self._update_step(deployment, "build", "running", "Compiling Quantum files...")
297
+ self._log(deployment, "Building application...")
298
+
299
+ # The Quantum CLI moved from src/cli/runner.py to the installed
300
+ # package (FRAMEWORK_PLAN Fase 1.1), so this always took the
301
+ # "not found" branch.
302
+ cli_path = os.path.join(self.quantum_root, "quantum", "cli", "runner.py")
303
+
304
+ if not os.path.exists(cli_path):
305
+ # It used to log "Quantum CLI not found, skipping compilation" and
306
+ # then mark the step "completed / Build successful" anyway. A
307
+ # deployment that never compiled anything reported a green build.
308
+ self._log(deployment, f"Quantum CLI not found at {cli_path}")
309
+ self._update_step(
310
+ deployment, "build", "failed",
311
+ "Quantum CLI not found — nothing was compiled"
312
+ )
313
+ return False
314
+
315
+ # Really parse the project's .q files with the real parser, instead of
316
+ # the time.sleep(1) + "Build completed: 0 errors, 0 warnings" this used
317
+ # to print without invoking anything.
318
+ #
319
+ # Parsing is what "build" means for a .q project — there is no compile
320
+ # step to a binary — so this is the whole check, not a stand-in for
321
+ # one. A file that does not parse is a 400 in production, which is
322
+ # exactly what a deploy gate should stop.
323
+ project_dir = os.path.join(self.quantum_root, "projects", deployment.project_name)
324
+ if not os.path.exists(project_dir):
325
+ project_dir = os.path.join(self.quantum_root, "examples")
326
+
327
+ try:
328
+ from quantum.core.parser import QuantumParser
329
+ except ImportError as e:
330
+ self._log(deployment, f"ERROR: cannot import the Quantum parser: {e}")
331
+ self._update_step(deployment, "build", "failed",
332
+ "Quantum parser not importable")
333
+ return False
334
+
335
+ parser = QuantumParser()
336
+ ok, failures = 0, []
337
+ for root, _dirs, files in os.walk(project_dir):
338
+ for fname in files:
339
+ if not fname.endswith('.q'):
340
+ continue
341
+ path = os.path.join(root, fname)
342
+ try:
343
+ with open(path, encoding='utf-8', errors='ignore') as fh:
344
+ parser.parse(fh.read())
345
+ ok += 1
346
+ except Exception as e:
347
+ failures.append((os.path.relpath(path, project_dir), str(e).split('\n')[0]))
348
+
349
+ for rel, err in failures:
350
+ self._log(deployment, f" FAIL {rel}: {err[:160]}")
351
+
352
+ if failures:
353
+ self._log(deployment,
354
+ f"Build failed: {ok} file(s) parsed, {len(failures)} failed")
355
+ self._update_step(
356
+ deployment, "build", "failed",
357
+ f"{len(failures)} file(s) do not parse"
358
+ )
359
+ deployment.status = DeployStatus.FAILED
360
+ deployment.error_message = f"{len(failures)} .q file(s) failed to parse"
361
+ return False
362
+
363
+ if ok == 0:
364
+ self._log(deployment, f"No .q files found under {project_dir}")
365
+ self._update_step(deployment, "build", "failed",
366
+ "No .q files to build")
367
+ return False
368
+
369
+ self._log(deployment, f"Build completed: {ok} file(s) parsed, 0 errors")
370
+ self._update_step(deployment, "build", "completed",
371
+ f"{ok} file(s) parsed")
372
+ return True
373
+
374
+ # A note that covers this step and the deploy ones below.
375
+ #
376
+ # These slept for half a second and then reported "Image pushed to
377
+ # registry", "Deployment successful", "Local server started on
378
+ # http://localhost:8000" and "Health checks passed: Application is
379
+ # healthy". Nothing was pushed, deployed or checked. The comment inside
380
+ # each one said "in a real implementation, we would…" — invisible from
381
+ # the screen, which showed a green pipeline.
382
+ #
383
+ # Telling someone their application is deployed and healthy when nothing
384
+ # ran is the most expensive lie this codebase told. So: the health check
385
+ # is implemented for real below, and every step that cannot do its work
386
+ # now FAILS and says why. A deploy button that reports failure is
387
+ # useless; one that reports success is dangerous.
388
+ _NOT_IMPLEMENTED = (
389
+ "not implemented: this step does no work. It used to report success "
390
+ "anyway. See PRODUCTION_READINESS.md."
391
+ )
392
+
393
+ def _step_push(self, deployment: Deployment) -> bool:
394
+ """Push step: push a Docker image to the registry. Not implemented."""
395
+ self._update_step(deployment, "push", "running", "Pushing to registry...")
396
+
397
+ self._update_step(deployment, "push", "failed", self._NOT_IMPLEMENTED)
398
+ self._log(deployment,
399
+ "push: no image is built or pushed — step not implemented")
400
+ return False
401
+
402
+ def _step_deploy(self, deployment: Deployment) -> bool:
403
+ """Deploy step: deploy to target environment"""
404
+ self._update_step(deployment, "deploy", "running", f"Deploying to {deployment.environment}...")
405
+ self._log(deployment, f"Deploying to {deployment.environment} using {deployment.strategy} strategy...")
406
+
407
+ # Deployment logic based on environment
408
+ if deployment.environment == "local":
409
+ return self._deploy_local(deployment)
410
+ elif deployment.environment == "docker":
411
+ return self._deploy_docker(deployment)
412
+ elif deployment.environment == "ssh":
413
+ return self._deploy_ssh(deployment)
414
+ else:
415
+ self._update_step(
416
+ deployment, "deploy", "failed",
417
+ f"unknown environment {deployment.environment!r}")
418
+ self._log(deployment,
419
+ f"deploy: no handler for environment "
420
+ f"{deployment.environment!r}")
421
+ return False
422
+
423
+ def _deploy_local(self, deployment: Deployment) -> bool:
424
+ """Deploy to the local environment. Not implemented."""
425
+ self._update_step(deployment, "deploy", "failed", self._NOT_IMPLEMENTED)
426
+ self._log(deployment,
427
+ "deploy(local): no server is started — step not implemented. "
428
+ "Run the app yourself with `quantum start`.")
429
+ return False
430
+
431
+ def _deploy_docker(self, deployment: Deployment) -> bool:
432
+ """Deploy with Docker. Not implemented.
433
+
434
+ DockerService can pull, create, start and stop containers, but there
435
+ is no image build anywhere in the admin, so there is nothing to run.
436
+ """
437
+ self._update_step(deployment, "deploy", "failed", self._NOT_IMPLEMENTED)
438
+ self._log(deployment,
439
+ "deploy(docker): no image is built and no container is "
440
+ "started — step not implemented")
441
+ return False
442
+
443
+ def _deploy_ssh(self, deployment: Deployment) -> bool:
444
+ """Deploy over SSH. Not implemented."""
445
+ self._update_step(deployment, "deploy", "failed", self._NOT_IMPLEMENTED)
446
+ self._log(deployment,
447
+ "deploy(ssh): nothing is copied and nothing is restarted — "
448
+ "step not implemented")
449
+ return False
450
+
451
+ def _step_health(self, deployment: Deployment, timeout: float = 10.0) -> bool:
452
+ """Health check step: actually request the health endpoint.
453
+
454
+ This one CAN be done honestly — it is an HTTP GET — so it is. It was
455
+ a sleep followed by "Health checks passed: Application is healthy".
456
+ """
457
+ self._update_step(deployment, "health", "running", "Running health checks...")
458
+
459
+ url = self._health_url(deployment)
460
+ if not url:
461
+ self._update_step(
462
+ deployment, "health", "skipped",
463
+ "no health URL configured for this environment")
464
+ self._log(deployment,
465
+ "health: no URL configured — nothing was checked")
466
+ return True
467
+
468
+ self._log(deployment, f"health: GET {url}")
469
+ try:
470
+ import urllib.request
471
+ with urllib.request.urlopen(url, timeout=timeout) as response:
472
+ code = response.getcode()
473
+ except Exception as exc:
474
+ self._update_step(deployment, "health", "failed",
475
+ f"{url} did not answer: {exc}")
476
+ self._log(deployment, f"health: FAILED — {exc}")
477
+ return False
478
+
479
+ if 200 <= code < 400:
480
+ self._update_step(deployment, "health", "completed",
481
+ f"{url} answered {code}")
482
+ self._log(deployment, f"health: {url} answered {code}")
483
+ return True
484
+
485
+ self._update_step(deployment, "health", "failed",
486
+ f"{url} answered {code}")
487
+ self._log(deployment, f"health: FAILED — {url} answered {code}")
488
+ return False
489
+
490
+ def _health_url(self, deployment: Deployment) -> Optional[str]:
491
+ """The health URL for this deployment's environment, if configured."""
492
+ try:
493
+ from .settings_service import get_settings_service
494
+ except ImportError:
495
+ from settings_service import get_settings_service
496
+
497
+ try:
498
+ settings = get_settings_service().get_global_settings()
499
+ except Exception:
500
+ return None
501
+
502
+ deployment_cfg = settings.get("deployment") or {}
503
+ for env in deployment_cfg.get("environments") or []:
504
+ if isinstance(env, dict) and env.get("name") == deployment.environment:
505
+ if env.get("health_url"):
506
+ return env["health_url"]
507
+ host = env.get("host")
508
+ if host:
509
+ path = deployment_cfg.get("health_check_path", "/health")
510
+ port = deployment_cfg.get("default_port", 8080)
511
+ return f"http://{host}:{port}{path}"
512
+ return None
513
+
514
+ # =========================================================================
515
+ # Deployment Management
516
+ # =========================================================================
517
+
518
+ def get_deployment(self, deploy_id: str) -> Optional[Deployment]:
519
+ """Get deployment by ID"""
520
+ return self._deployments.get(deploy_id)
521
+
522
+ def list_deployments(self, project_id: int = None, limit: int = 20) -> List[Deployment]:
523
+ """List deployments, optionally filtered by project"""
524
+ deployments = list(self._deployments.values())
525
+
526
+ if project_id:
527
+ deployments = [d for d in deployments if d.project_id == project_id]
528
+
529
+ # Sort by started_at descending
530
+ deployments.sort(key=lambda d: d.started_at or datetime.min, reverse=True)
531
+
532
+ return deployments[:limit]
533
+
534
+ def cancel_deployment(self, deploy_id: str) -> bool:
535
+ """Cancel a running deployment"""
536
+ deployment = self._deployments.get(deploy_id)
537
+ if not deployment:
538
+ return False
539
+
540
+ if deployment.status not in (DeployStatus.PENDING, DeployStatus.RUNNING):
541
+ return False
542
+
543
+ deployment.status = DeployStatus.CANCELLED
544
+ deployment.completed_at = datetime.now()
545
+ self._log(deployment, "Deployment cancelled by user")
546
+ return True
547
+
548
+ def rollback_deployment(self, deploy_id: str) -> Optional[Deployment]:
549
+ """
550
+ Rollback a deployment
551
+
552
+ Returns:
553
+ New deployment object for rollback, or None if failed
554
+ """
555
+ deployment = self._deployments.get(deploy_id)
556
+ if not deployment:
557
+ return None
558
+
559
+ # Create rollback deployment
560
+ rollback = self.create_deployment(
561
+ project_id=deployment.project_id,
562
+ project_name=deployment.project_name,
563
+ environment=deployment.environment,
564
+ branch=deployment.branch,
565
+ strategy="recreate", # Rollbacks use recreate strategy
566
+ triggered_by="rollback"
567
+ )
568
+
569
+ self._log(rollback, f"Rolling back deployment {deploy_id}")
570
+
571
+ # Mark original as rolled back
572
+ deployment.status = DeployStatus.ROLLED_BACK
573
+
574
+ # Start rollback
575
+ self.start_deployment(rollback.id)
576
+
577
+ return rollback
578
+
579
+ def rollback_to_version(
580
+ self,
581
+ project_id: int,
582
+ project_name: str,
583
+ environment: str,
584
+ target_version_id: int,
585
+ docker_client=None,
586
+ docker_image: str = None,
587
+ docker_tag: str = None
588
+ ) -> Optional[Deployment]:
589
+ """
590
+ Rollback to a specific deployment version.
591
+
592
+ Args:
593
+ project_id: Project ID
594
+ project_name: Project name for logging
595
+ environment: Target environment
596
+ target_version_id: DeploymentVersion ID to rollback to
597
+ docker_client: Optional Docker client
598
+ docker_image: Docker image to use
599
+ docker_tag: Docker tag to use
600
+
601
+ Returns:
602
+ New deployment object for rollback
603
+ """
604
+ rollback = self.create_deployment(
605
+ project_id=project_id,
606
+ project_name=project_name,
607
+ environment=environment,
608
+ branch="rollback",
609
+ strategy="recreate",
610
+ triggered_by=f"rollback_to_v{target_version_id}"
611
+ )
612
+
613
+ self._log(rollback, f"Rolling back to version {target_version_id}")
614
+
615
+ # If we have docker info, we can do the actual rollback
616
+ if docker_client and docker_image and docker_tag:
617
+ try:
618
+ # Step 1: Stop current container
619
+ self._update_step(rollback, "prepare", "running")
620
+ container_name = f"{project_name.lower().replace(' ', '-')}-{environment}"
621
+ self._log(rollback, f"Stopping current container: {container_name}")
622
+
623
+ try:
624
+ current = docker_client.containers.get(container_name)
625
+ current.stop(timeout=30)
626
+ current.remove()
627
+ self._log(rollback, f"Stopped and removed: {container_name}")
628
+ except Exception as e:
629
+ self._log(rollback, f"No current container found: {e}")
630
+
631
+ self._update_step(rollback, "prepare", "completed")
632
+
633
+ # Step 2: Start container with old image
634
+ self._update_step(rollback, "deploy", "running")
635
+ full_image = f"{docker_image}:{docker_tag}"
636
+ self._log(rollback, f"Starting container with image: {full_image}")
637
+
638
+ container = docker_client.containers.run(
639
+ full_image,
640
+ name=container_name,
641
+ detach=True,
642
+ ports={'8000/tcp': None}, # Auto-assign port
643
+ restart_policy={"Name": "unless-stopped"}
644
+ )
645
+ self._log(rollback, f"Started container: {container.short_id}")
646
+ self._update_step(rollback, "deploy", "completed")
647
+
648
+ # Step 3: Health check
649
+ self._update_step(rollback, "health", "running")
650
+ import time
651
+ time.sleep(3) # Wait for startup
652
+
653
+ container.reload()
654
+ if container.status == "running":
655
+ self._update_step(rollback, "health", "completed")
656
+ rollback.status = DeployStatus.COMPLETED
657
+ self._log(rollback, "Rollback successful - container is running")
658
+ else:
659
+ self._update_step(rollback, "health", "failed", f"Container status: {container.status}")
660
+ rollback.status = DeployStatus.FAILED
661
+ rollback.error_message = f"Container not running: {container.status}"
662
+
663
+ except Exception as e:
664
+ logger.error(f"Rollback failed: {e}")
665
+ rollback.status = DeployStatus.FAILED
666
+ rollback.error_message = str(e)
667
+ self._log(rollback, f"Rollback failed: {e}")
668
+ else:
669
+ # No Docker client - just mark as completed (dry run)
670
+ self._log(rollback, "Dry run mode - no Docker client provided")
671
+ for step in ["prepare", "build", "push", "deploy", "health"]:
672
+ self._update_step(rollback, step, "skipped", "Dry run")
673
+ rollback.status = DeployStatus.COMPLETED
674
+
675
+ rollback.completed_at = datetime.now()
676
+ return rollback
677
+
678
+ def get_rollback_targets(self, project_id: int, environment_id: int, limit: int = 10) -> List[Dict]:
679
+ """
680
+ Get available rollback targets for a project/environment.
681
+
682
+ This should be called with database versions, not in-memory deployments.
683
+ """
684
+ # This is a placeholder - actual implementation needs DB access
685
+ # which should be done in the API endpoint
686
+ return []
687
+
688
+
689
+ # Singleton instance
690
+ _deploy_service = None
691
+
692
+
693
+ def get_deploy_service() -> DeployService:
694
+ """Get singleton instance of DeployService"""
695
+ global _deploy_service
696
+ if _deploy_service is None:
697
+ _deploy_service = DeployService()
698
+ return _deploy_service