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,1118 @@
1
+ """
2
+ Resource Manager for Quantum Admin
3
+ Centralized management of ports, secrets, services, and configs
4
+ """
5
+ import os
6
+ import json
7
+ import socket
8
+ import logging
9
+ from typing import Optional, List, Dict, Any
10
+ from datetime import datetime
11
+ from cryptography.fernet import Fernet
12
+ import base64
13
+ import hashlib
14
+
15
+ from sqlalchemy.orm import Session
16
+ from sqlalchemy.exc import IntegrityError
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ try:
21
+ from .models import PortAllocation, PortRange, Secret, ServiceRegistry, Project, Environment
22
+ from .database import SessionLocal
23
+ except ImportError:
24
+ from models import PortAllocation, PortRange, Secret, ServiceRegistry, Project, Environment
25
+ from database import SessionLocal
26
+
27
+
28
+ class ResourceExhaustedError(Exception):
29
+ """Raised when no resources are available"""
30
+ pass
31
+
32
+
33
+ class PortManager:
34
+ """Manages port allocation and tracking"""
35
+
36
+ DEFAULT_RANGES = {
37
+ "app": (8000, 8999),
38
+ "database": (5400, 5499),
39
+ "cache": (6370, 6399),
40
+ "web": (3000, 3099),
41
+ "queue": (5670, 5699),
42
+ }
43
+
44
+ def __init__(self, db: Session):
45
+ self.db = db
46
+ self._ensure_default_ranges()
47
+
48
+ def _ensure_default_ranges(self):
49
+ """Create default port ranges if they don't exist"""
50
+ for name, (start, end) in self.DEFAULT_RANGES.items():
51
+ existing = self.db.query(PortRange).filter(PortRange.name == name).first()
52
+ if not existing:
53
+ range_obj = PortRange(
54
+ name=name,
55
+ start_port=start,
56
+ end_port=end,
57
+ host='*',
58
+ description=f"Default range for {name} services"
59
+ )
60
+ self.db.add(range_obj)
61
+ try:
62
+ self.db.commit()
63
+ except IntegrityError:
64
+ self.db.rollback()
65
+
66
+ def allocate(
67
+ self,
68
+ service_name: str,
69
+ port_type: str = "app",
70
+ host: str = "localhost",
71
+ project_id: int = None,
72
+ environment_id: int = None,
73
+ preferred_port: int = None
74
+ ) -> Optional[PortAllocation]:
75
+ """
76
+ Allocate an available port for a service.
77
+
78
+ Returns the PortAllocation object, or None if no ports available.
79
+ """
80
+ # Check if already allocated
81
+ existing = self.db.query(PortAllocation).filter(
82
+ PortAllocation.service_name == service_name,
83
+ PortAllocation.host == host,
84
+ PortAllocation.status != 'released'
85
+ ).first()
86
+ if existing:
87
+ logger.info(f"Port already allocated for {service_name}@{host}: {existing.port}")
88
+ return existing
89
+
90
+ # Try preferred port first
91
+ if preferred_port and self.is_available(preferred_port, host):
92
+ return self._do_allocate(
93
+ service_name, preferred_port, port_type, host,
94
+ project_id, environment_id
95
+ )
96
+
97
+ # Find next available in range
98
+ range_obj = self._get_range(port_type, host)
99
+ if not range_obj:
100
+ logger.error(f"No port range defined for type '{port_type}'")
101
+ return None
102
+
103
+ for port in range(range_obj.start_port, range_obj.end_port + 1):
104
+ if self.is_available(port, host):
105
+ return self._do_allocate(
106
+ service_name, port, port_type, host,
107
+ project_id, environment_id
108
+ )
109
+
110
+ logger.error(f"No available ports in {port_type} range ({range_obj.start_port}-{range_obj.end_port}) for host {host}")
111
+ return None
112
+
113
+ def _do_allocate(
114
+ self,
115
+ service_name: str,
116
+ port: int,
117
+ port_type: str,
118
+ host: str,
119
+ project_id: int = None,
120
+ environment_id: int = None
121
+ ) -> PortAllocation:
122
+ """Create the allocation record"""
123
+ allocation = PortAllocation(
124
+ service_name=service_name,
125
+ port=port,
126
+ port_type=port_type,
127
+ host=host,
128
+ project_id=project_id,
129
+ environment_id=environment_id,
130
+ status='allocated'
131
+ )
132
+ self.db.add(allocation)
133
+ try:
134
+ self.db.commit()
135
+ logger.info(f"Allocated port {port} for {service_name}@{host}")
136
+ return allocation
137
+ except IntegrityError:
138
+ self.db.rollback()
139
+ raise ResourceExhaustedError(f"Port {port} on {host} is already in use")
140
+
141
+ def release(self, port_or_service: int | str, host: str = "localhost") -> bool:
142
+ """Release a port allocation by port number or service name"""
143
+ if isinstance(port_or_service, int):
144
+ # Release by port number
145
+ allocation = self.db.query(PortAllocation).filter(
146
+ PortAllocation.port == port_or_service,
147
+ PortAllocation.host == host,
148
+ PortAllocation.status != 'released'
149
+ ).first()
150
+ else:
151
+ # Release by service name
152
+ allocation = self.db.query(PortAllocation).filter(
153
+ PortAllocation.service_name == port_or_service,
154
+ PortAllocation.host == host,
155
+ PortAllocation.status != 'released'
156
+ ).first()
157
+
158
+ if not allocation:
159
+ return False
160
+
161
+ allocation.status = 'released'
162
+ allocation.released_at = datetime.utcnow()
163
+ self.db.commit()
164
+ logger.info(f"Released port {allocation.port}@{host}")
165
+ return True
166
+
167
+ def reserve(
168
+ self,
169
+ port: int,
170
+ host: str,
171
+ reason: str,
172
+ service_name: str = None
173
+ ) -> bool:
174
+ """Reserve a specific port (prevents auto-allocation)"""
175
+ # Check if already allocated
176
+ existing = self.db.query(PortAllocation).filter(
177
+ PortAllocation.port == port,
178
+ PortAllocation.host == host,
179
+ PortAllocation.status != 'released'
180
+ ).first()
181
+
182
+ if existing:
183
+ if existing.is_reserved:
184
+ return True # Already reserved
185
+ return False # In use by another service
186
+
187
+ # Create reservation
188
+ allocation = PortAllocation(
189
+ service_name=service_name or f"reserved-{port}",
190
+ port=port,
191
+ port_type='reserved',
192
+ host=host,
193
+ status='reserved',
194
+ is_reserved=True,
195
+ reserved_reason=reason
196
+ )
197
+ self.db.add(allocation)
198
+ self.db.commit()
199
+ logger.info(f"Reserved port {port}@{host}: {reason}")
200
+ return True
201
+
202
+ def get(self, service_name: str, host: str = "localhost") -> Optional[int]:
203
+ """Get allocated port for a service"""
204
+ allocation = self.db.query(PortAllocation).filter(
205
+ PortAllocation.service_name == service_name,
206
+ PortAllocation.host == host,
207
+ PortAllocation.status != 'released'
208
+ ).first()
209
+ return allocation.port if allocation else None
210
+
211
+ def is_available(self, port: int, host: str = "localhost") -> bool:
212
+ """Check if port is available (not allocated and not in use)"""
213
+ # Check database
214
+ allocation = self.db.query(PortAllocation).filter(
215
+ PortAllocation.port == port,
216
+ PortAllocation.host == host,
217
+ PortAllocation.status != 'released'
218
+ ).first()
219
+
220
+ if allocation:
221
+ return False
222
+
223
+ # Check if port is actually in use on localhost
224
+ if host == 'localhost' or host == '127.0.0.1':
225
+ return self._check_port_free_local(port)
226
+
227
+ return True
228
+
229
+ def _check_port_free_local(self, port: int) -> bool:
230
+ """Check if local port is free using socket"""
231
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
232
+ try:
233
+ s.settimeout(0.5)
234
+ s.bind(('127.0.0.1', port))
235
+ return True
236
+ except OSError:
237
+ return False
238
+
239
+ def _get_range(self, port_type: str, host: str) -> Optional[PortRange]:
240
+ """Get port range for a type"""
241
+ # First try host-specific range
242
+ range_obj = self.db.query(PortRange).filter(
243
+ PortRange.name == port_type,
244
+ PortRange.host == host
245
+ ).first()
246
+
247
+ if range_obj:
248
+ return range_obj
249
+
250
+ # Fall back to wildcard range
251
+ return self.db.query(PortRange).filter(
252
+ PortRange.name == port_type,
253
+ PortRange.host == '*'
254
+ ).first()
255
+
256
+ def get_ranges(self) -> List[PortRange]:
257
+ """Get all port ranges"""
258
+ return self.db.query(PortRange).all()
259
+
260
+ def set_range(
261
+ self,
262
+ name: str,
263
+ start: int,
264
+ end: int,
265
+ host: str = "*",
266
+ description: str = None
267
+ ) -> PortRange:
268
+ """Set or update a port range"""
269
+ existing = self.db.query(PortRange).filter(
270
+ PortRange.name == name,
271
+ PortRange.host == host
272
+ ).first()
273
+
274
+ if existing:
275
+ existing.start_port = start
276
+ existing.end_port = end
277
+ if description:
278
+ existing.description = description
279
+ else:
280
+ existing = PortRange(
281
+ name=name,
282
+ start_port=start,
283
+ end_port=end,
284
+ host=host,
285
+ description=description or f"Range for {name}"
286
+ )
287
+ self.db.add(existing)
288
+
289
+ self.db.commit()
290
+ return existing
291
+
292
+ def create_range(
293
+ self,
294
+ name: str,
295
+ start_port: int,
296
+ end_port: int,
297
+ host: str = "*",
298
+ description: str = None
299
+ ) -> PortRange:
300
+ """Create a new port range (alias for set_range)"""
301
+ return self.set_range(name, start_port, end_port, host, description)
302
+
303
+ def list_allocations(
304
+ self,
305
+ host: str = None,
306
+ project_id: int = None,
307
+ environment_id: int = None,
308
+ status: str = None,
309
+ include_released: bool = False
310
+ ) -> List[PortAllocation]:
311
+ """List all port allocations with optional filters"""
312
+ query = self.db.query(PortAllocation)
313
+
314
+ if host:
315
+ query = query.filter(PortAllocation.host == host)
316
+ if project_id:
317
+ query = query.filter(PortAllocation.project_id == project_id)
318
+ if environment_id:
319
+ query = query.filter(PortAllocation.environment_id == environment_id)
320
+ if status:
321
+ query = query.filter(PortAllocation.status == status)
322
+ elif not include_released:
323
+ query = query.filter(PortAllocation.status != 'released')
324
+
325
+ return query.order_by(PortAllocation.port).all()
326
+
327
+ def get_usage_stats(self, host: str = None) -> Dict[str, Dict]:
328
+ """Get port usage statistics by range"""
329
+ ranges = self.get_ranges()
330
+ stats = {}
331
+
332
+ for r in ranges:
333
+ if host and r.host != '*' and r.host != host:
334
+ continue
335
+
336
+ total = r.end_port - r.start_port + 1
337
+ used = self.db.query(PortAllocation).filter(
338
+ PortAllocation.port >= r.start_port,
339
+ PortAllocation.port <= r.end_port,
340
+ PortAllocation.status != 'released'
341
+ )
342
+ if host:
343
+ used = used.filter(PortAllocation.host == host)
344
+
345
+ used_count = used.count()
346
+
347
+ stats[r.name] = {
348
+ "range": f"{r.start_port}-{r.end_port}",
349
+ "total": total,
350
+ "used": used_count,
351
+ "available": total - used_count,
352
+ "percent_available": round((total - used_count) / total * 100, 1)
353
+ }
354
+
355
+ return stats
356
+
357
+
358
+ class SecretManager:
359
+ """Manages encrypted secrets"""
360
+
361
+ def __init__(self, db: Session):
362
+ self.db = db
363
+ self._fernet = self._init_encryption()
364
+
365
+ def _init_encryption(self) -> Fernet:
366
+ """Initialize Fernet encryption with key from env or generate"""
367
+ key = os.environ.get('QUANTUM_ENCRYPTION_KEY')
368
+ if not key:
369
+ # Generate a stable key from a passphrase (NOT SECURE for production)
370
+ passphrase = os.environ.get('QUANTUM_SECRET_PASSPHRASE', 'quantum-admin-default-key')
371
+ key = base64.urlsafe_b64encode(
372
+ hashlib.sha256(passphrase.encode()).digest()
373
+ )
374
+ logger.warning("Using derived encryption key. Set QUANTUM_ENCRYPTION_KEY for production!")
375
+ else:
376
+ key = key.encode() if isinstance(key, str) else key
377
+
378
+ return Fernet(key)
379
+
380
+ def set(
381
+ self,
382
+ key: str,
383
+ value: str,
384
+ scope: str = "global",
385
+ project_id: int = None,
386
+ environment_id: int = None,
387
+ description: str = None
388
+ ) -> Secret:
389
+ """Store an encrypted secret"""
390
+ # Encrypt the value
391
+ encrypted = self._fernet.encrypt(value.encode()).decode()
392
+
393
+ # Check if exists
394
+ existing = self.db.query(Secret).filter(
395
+ Secret.key == key,
396
+ Secret.project_id == project_id,
397
+ Secret.environment_id == environment_id
398
+ ).first()
399
+
400
+ if existing:
401
+ existing.encrypted_value = encrypted
402
+ existing.updated_at = datetime.utcnow()
403
+ if description:
404
+ existing.description = description
405
+ else:
406
+ existing = Secret(
407
+ key=key,
408
+ encrypted_value=encrypted,
409
+ scope=scope,
410
+ project_id=project_id,
411
+ environment_id=environment_id,
412
+ description=description
413
+ )
414
+ self.db.add(existing)
415
+
416
+ self.db.commit()
417
+ logger.info(f"Secret '{key}' stored (scope={scope})")
418
+ return existing
419
+
420
+ def get(
421
+ self,
422
+ key: str,
423
+ project_id: int = None,
424
+ environment_id: int = None
425
+ ) -> Optional[str]:
426
+ """Retrieve and decrypt a secret"""
427
+ # Try environment-specific first
428
+ if environment_id:
429
+ secret = self.db.query(Secret).filter(
430
+ Secret.key == key,
431
+ Secret.environment_id == environment_id
432
+ ).first()
433
+ if secret:
434
+ return self._decrypt(secret.encrypted_value)
435
+
436
+ # Try project-specific
437
+ if project_id:
438
+ secret = self.db.query(Secret).filter(
439
+ Secret.key == key,
440
+ Secret.project_id == project_id,
441
+ Secret.environment_id == None
442
+ ).first()
443
+ if secret:
444
+ return self._decrypt(secret.encrypted_value)
445
+
446
+ # Try global
447
+ secret = self.db.query(Secret).filter(
448
+ Secret.key == key,
449
+ Secret.project_id == None,
450
+ Secret.environment_id == None
451
+ ).first()
452
+ if secret:
453
+ return self._decrypt(secret.encrypted_value)
454
+
455
+ return None
456
+
457
+ def _decrypt(self, encrypted_value: str) -> str:
458
+ """Decrypt a value"""
459
+ return self._fernet.decrypt(encrypted_value.encode()).decode()
460
+
461
+ def list(
462
+ self,
463
+ project_id: int = None,
464
+ environment_id: int = None,
465
+ include_values: bool = False
466
+ ) -> List[Dict]:
467
+ """List secrets (optionally with decrypted values)"""
468
+ query = self.db.query(Secret)
469
+
470
+ if project_id is not None:
471
+ query = query.filter(
472
+ (Secret.project_id == project_id) | (Secret.project_id == None)
473
+ )
474
+ if environment_id is not None:
475
+ query = query.filter(
476
+ (Secret.environment_id == environment_id) | (Secret.environment_id == None)
477
+ )
478
+
479
+ secrets = query.order_by(Secret.scope, Secret.key).all()
480
+
481
+ result = []
482
+ for s in secrets:
483
+ data = s.to_dict(include_value=False)
484
+ if include_values:
485
+ data['value'] = self._decrypt(s.encrypted_value)
486
+ result.append(data)
487
+
488
+ return result
489
+
490
+ def list_secrets(
491
+ self,
492
+ project_id: int = None,
493
+ environment_id: int = None,
494
+ scope: str = None
495
+ ) -> List[Secret]:
496
+ """List secrets as Secret objects with optional filters"""
497
+ query = self.db.query(Secret)
498
+
499
+ if project_id is not None:
500
+ query = query.filter(
501
+ (Secret.project_id == project_id) | (Secret.project_id == None)
502
+ )
503
+ if environment_id is not None:
504
+ query = query.filter(
505
+ (Secret.environment_id == environment_id) | (Secret.environment_id == None)
506
+ )
507
+ if scope:
508
+ query = query.filter(Secret.scope == scope)
509
+
510
+ return query.order_by(Secret.scope, Secret.key).all()
511
+
512
+ def delete(self, secret_id_or_key, project_id: int = None, environment_id: int = None) -> bool:
513
+ """Delete a secret by id or key"""
514
+ if isinstance(secret_id_or_key, int):
515
+ # Delete by id
516
+ secret = self.db.query(Secret).filter(Secret.id == secret_id_or_key).first()
517
+ else:
518
+ # Delete by key
519
+ query = self.db.query(Secret).filter(Secret.key == secret_id_or_key)
520
+ if project_id is not None:
521
+ query = query.filter(Secret.project_id == project_id)
522
+ if environment_id is not None:
523
+ query = query.filter(Secret.environment_id == environment_id)
524
+ secret = query.first()
525
+
526
+ if secret:
527
+ key = secret.key
528
+ self.db.delete(secret)
529
+ self.db.commit()
530
+ logger.info(f"Secret '{key}' deleted")
531
+ return True
532
+ return False
533
+
534
+ def rotate(
535
+ self,
536
+ secret_id_or_key,
537
+ new_value: str,
538
+ project_id: int = None,
539
+ environment_id: int = None
540
+ ) -> bool:
541
+ """Rotate a secret value by id or key"""
542
+ if isinstance(secret_id_or_key, int):
543
+ # Find by id
544
+ secret = self.db.query(Secret).filter(Secret.id == secret_id_or_key).first()
545
+ else:
546
+ # Find by key
547
+ secret = self.db.query(Secret).filter(
548
+ Secret.key == secret_id_or_key,
549
+ Secret.project_id == project_id,
550
+ Secret.environment_id == environment_id
551
+ ).first()
552
+
553
+ if not secret:
554
+ return False
555
+
556
+ secret.encrypted_value = self._fernet.encrypt(new_value.encode()).decode()
557
+ secret.last_rotated = datetime.utcnow()
558
+ secret.updated_at = datetime.utcnow()
559
+ self.db.commit()
560
+
561
+ logger.info(f"Secret '{key}' rotated")
562
+ return True
563
+
564
+
565
+ class ServiceRegistryManager:
566
+ """Manages service discovery registry"""
567
+
568
+ def __init__(self, db: Session):
569
+ self.db = db
570
+
571
+ def register(
572
+ self,
573
+ name: str,
574
+ host: str,
575
+ port: int,
576
+ service_type: str = "app",
577
+ protocol: str = "http",
578
+ health_endpoint: str = "/health",
579
+ health_interval: int = 30,
580
+ project_id: int = None,
581
+ environment_id: int = None,
582
+ metadata: Dict = None
583
+ ) -> ServiceRegistry:
584
+ """Register a service for discovery"""
585
+ existing = self.db.query(ServiceRegistry).filter(
586
+ ServiceRegistry.name == name
587
+ ).first()
588
+
589
+ if existing:
590
+ existing.host = host
591
+ existing.port = port
592
+ existing.service_type = service_type
593
+ existing.protocol = protocol
594
+ existing.health_endpoint = health_endpoint
595
+ existing.health_interval = health_interval
596
+ existing.project_id = project_id
597
+ existing.environment_id = environment_id
598
+ existing.metadata_json = json.dumps(metadata) if metadata else None
599
+ existing.updated_at = datetime.utcnow()
600
+ else:
601
+ existing = ServiceRegistry(
602
+ name=name,
603
+ host=host,
604
+ port=port,
605
+ service_type=service_type,
606
+ protocol=protocol,
607
+ health_endpoint=health_endpoint,
608
+ health_interval=health_interval,
609
+ project_id=project_id,
610
+ environment_id=environment_id,
611
+ metadata_json=json.dumps(metadata) if metadata else None
612
+ )
613
+ self.db.add(existing)
614
+
615
+ self.db.commit()
616
+ logger.info(f"Service '{name}' registered at {protocol}://{host}:{port}")
617
+ return existing
618
+
619
+ def discover(
620
+ self,
621
+ service_type: str,
622
+ project_id: int = None,
623
+ environment_id: int = None,
624
+ healthy_only: bool = True
625
+ ) -> List[ServiceRegistry]:
626
+ """Discover services by type"""
627
+ query = self.db.query(ServiceRegistry).filter(
628
+ ServiceRegistry.service_type == service_type
629
+ )
630
+ if project_id:
631
+ query = query.filter(ServiceRegistry.project_id == project_id)
632
+ if environment_id:
633
+ query = query.filter(ServiceRegistry.environment_id == environment_id)
634
+ if healthy_only:
635
+ query = query.filter(ServiceRegistry.health_status == 'healthy')
636
+ return query.all()
637
+
638
+ def discover_by_name(self, name: str, healthy_only: bool = True) -> Optional[ServiceRegistry]:
639
+ """Discover a service by name"""
640
+ query = self.db.query(ServiceRegistry).filter(ServiceRegistry.name == name)
641
+ if healthy_only:
642
+ query = query.filter(ServiceRegistry.health_status == 'healthy')
643
+ return query.first()
644
+
645
+ def discover_by_type(
646
+ self,
647
+ service_type: str,
648
+ project_id: int = None,
649
+ healthy_only: bool = True
650
+ ) -> List[ServiceRegistry]:
651
+ """Discover all services of a type"""
652
+ return self.discover(service_type, project_id, healthy_only=healthy_only)
653
+
654
+ def deregister(self, name: str) -> bool:
655
+ """Remove a service from registry by name"""
656
+ service = self.db.query(ServiceRegistry).filter(
657
+ ServiceRegistry.name == name
658
+ ).first()
659
+ if service:
660
+ self.db.delete(service)
661
+ self.db.commit()
662
+ logger.info(f"Service '{name}' deregistered")
663
+ return True
664
+ return False
665
+
666
+ def unregister(self, service_id: int) -> bool:
667
+ """Remove a service from registry by id"""
668
+ service = self.db.query(ServiceRegistry).filter(
669
+ ServiceRegistry.id == service_id
670
+ ).first()
671
+ if service:
672
+ name = service.name
673
+ self.db.delete(service)
674
+ self.db.commit()
675
+ logger.info(f"Service '{name}' unregistered")
676
+ return True
677
+ return False
678
+
679
+ def list_all(self) -> List[ServiceRegistry]:
680
+ """List all registered services"""
681
+ return self.db.query(ServiceRegistry).order_by(
682
+ ServiceRegistry.service_type,
683
+ ServiceRegistry.name
684
+ ).all()
685
+
686
+ def list_services(
687
+ self,
688
+ project_id: int = None,
689
+ environment_id: int = None,
690
+ service_type: str = None,
691
+ healthy_only: bool = False
692
+ ) -> List[ServiceRegistry]:
693
+ """List services with optional filters"""
694
+ query = self.db.query(ServiceRegistry)
695
+
696
+ if project_id:
697
+ query = query.filter(ServiceRegistry.project_id == project_id)
698
+ if environment_id:
699
+ query = query.filter(ServiceRegistry.environment_id == environment_id)
700
+ if service_type:
701
+ query = query.filter(ServiceRegistry.service_type == service_type)
702
+ if healthy_only:
703
+ query = query.filter(ServiceRegistry.health_status == 'healthy')
704
+
705
+ return query.order_by(ServiceRegistry.service_type, ServiceRegistry.name).all()
706
+
707
+ async def check_health(self, service_id: int) -> Dict:
708
+ """Check health of a service by id and return result dict"""
709
+ import httpx
710
+ import time
711
+
712
+ service = self.db.query(ServiceRegistry).filter(
713
+ ServiceRegistry.id == service_id
714
+ ).first()
715
+
716
+ if not service:
717
+ return {"status": "error", "message": "Service not found"}
718
+
719
+ if not service.health_endpoint:
720
+ return {"status": "unknown", "message": "No health endpoint configured"}
721
+
722
+ url = f"{service.protocol}://{service.host}:{service.port}{service.health_endpoint}"
723
+
724
+ try:
725
+ start_time = time.time()
726
+ async with httpx.AsyncClient(timeout=5.0) as client:
727
+ response = await client.get(url)
728
+ response_time = int((time.time() - start_time) * 1000)
729
+ is_healthy = response.status_code == 200
730
+
731
+ service.health_status = 'healthy' if is_healthy else 'unhealthy'
732
+ service.last_health_check = datetime.utcnow()
733
+ service.consecutive_failures = 0 if is_healthy else service.consecutive_failures + 1
734
+ self.db.commit()
735
+
736
+ return {
737
+ "status": service.health_status,
738
+ "response_time": response_time,
739
+ "status_code": response.status_code,
740
+ "url": url
741
+ }
742
+ except Exception as e:
743
+ service.health_status = 'unhealthy'
744
+ service.last_health_check = datetime.utcnow()
745
+ service.consecutive_failures += 1
746
+ self.db.commit()
747
+ logger.warning(f"Health check failed for {service.name}: {e}")
748
+ return {
749
+ "status": "unhealthy",
750
+ "error": str(e),
751
+ "url": url
752
+ }
753
+
754
+ async def _health_check_service(self, service: ServiceRegistry) -> bool:
755
+ """Internal: Check health of a single service"""
756
+ import httpx
757
+
758
+ if not service.health_endpoint:
759
+ return True # No health check configured
760
+
761
+ url = f"{service.protocol}://{service.host}:{service.port}{service.health_endpoint}"
762
+
763
+ try:
764
+ async with httpx.AsyncClient(timeout=5.0) as client:
765
+ response = await client.get(url)
766
+ is_healthy = response.status_code == 200
767
+
768
+ service.health_status = 'healthy' if is_healthy else 'unhealthy'
769
+ service.last_health_check = datetime.utcnow()
770
+ service.consecutive_failures = 0 if is_healthy else service.consecutive_failures + 1
771
+ self.db.commit()
772
+
773
+ return is_healthy
774
+ except Exception as e:
775
+ service.health_status = 'unhealthy'
776
+ service.last_health_check = datetime.utcnow()
777
+ service.consecutive_failures += 1
778
+ self.db.commit()
779
+ logger.warning(f"Health check failed for {service.name}: {e}")
780
+ return False
781
+
782
+ async def run_health_checks(
783
+ self,
784
+ project_id: int = None,
785
+ environment_id: int = None
786
+ ) -> List[Dict]:
787
+ """Run health checks on services with optional filters"""
788
+ services = self.list_services(
789
+ project_id=project_id,
790
+ environment_id=environment_id
791
+ )
792
+ results = []
793
+
794
+ for service in services:
795
+ is_healthy = await self._health_check_service(service)
796
+ results.append({
797
+ "name": service.name,
798
+ "healthy": is_healthy,
799
+ "status": service.health_status,
800
+ "url": f"{service.protocol}://{service.host}:{service.port}{service.health_endpoint}",
801
+ "last_check": service.last_health_check.isoformat() if service.last_health_check else None
802
+ })
803
+
804
+ return results
805
+
806
+ async def health_check_all(self) -> Dict[str, Dict]:
807
+ """Run health checks on all registered services"""
808
+ services = self.list_all()
809
+ results = {}
810
+
811
+ for service in services:
812
+ is_healthy = await self._health_check_service(service)
813
+ results[service.name] = {
814
+ "healthy": is_healthy,
815
+ "status": service.health_status,
816
+ "url": f"{service.protocol}://{service.host}:{service.port}{service.health_endpoint}",
817
+ "last_check": service.last_health_check.isoformat() if service.last_health_check else None,
818
+ "failures": service.consecutive_failures
819
+ }
820
+
821
+ return results
822
+
823
+
824
+ class ResourceDiscovery:
825
+ """Auto-discovery of system resources"""
826
+
827
+ def __init__(self):
828
+ self._psutil_available = False
829
+ try:
830
+ import psutil
831
+ self._psutil_available = True
832
+ except ImportError:
833
+ logger.warning("psutil not installed - limited resource discovery")
834
+
835
+ def scan_ports_in_use(self, host: str = "localhost") -> List[Dict]:
836
+ """Scan for ports currently in use on the system"""
837
+ if not self._psutil_available:
838
+ return self._scan_ports_socket(host)
839
+
840
+ import psutil
841
+ ports = []
842
+
843
+ for conn in psutil.net_connections(kind='inet'):
844
+ if conn.status == 'LISTEN' and conn.laddr:
845
+ port_info = {
846
+ "port": conn.laddr.port,
847
+ "host": conn.laddr.ip or "0.0.0.0",
848
+ "pid": conn.pid,
849
+ "status": conn.status
850
+ }
851
+
852
+ # Try to get process name
853
+ if conn.pid:
854
+ try:
855
+ proc = psutil.Process(conn.pid)
856
+ port_info["process_name"] = proc.name()
857
+ port_info["process_cmdline"] = " ".join(proc.cmdline()[:3])
858
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
859
+ pass
860
+
861
+ ports.append(port_info)
862
+
863
+ return sorted(ports, key=lambda x: x["port"])
864
+
865
+ def _scan_ports_socket(self, host: str) -> List[Dict]:
866
+ """Without psutil there is no port list, and no scan either.
867
+
868
+ This used to connect_ex to ports 1-9999 one at a time with a 0.1s
869
+ timeout. On Linux a closed localhost port refuses at once, so nobody
870
+ noticed; on Windows each attempt waits out the timeout — measured at
871
+ ~109 ms a port, about 18 MINUTES for a single /api/resources/overview
872
+ request. It hung the admin test suite on any machine without psutil,
873
+ and it was a port scan of the host besides. psutil is declared in
874
+ backend/requirements.txt; without it, the overview reports no ports.
875
+ """
876
+ return []
877
+
878
+ def detect_quantum_processes(self) -> List[Dict]:
879
+ """Detect running Quantum framework processes"""
880
+ if not self._psutil_available:
881
+ return []
882
+
883
+ import psutil
884
+ quantum_procs = []
885
+
886
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']):
887
+ try:
888
+ cmdline = proc.info.get('cmdline', [])
889
+ if cmdline and any('quantum' in str(arg).lower() for arg in cmdline):
890
+ quantum_procs.append({
891
+ "pid": proc.info['pid'],
892
+ "name": proc.info['name'],
893
+ "cmdline": " ".join(cmdline[:5]),
894
+ "started": datetime.fromtimestamp(proc.info['create_time']).isoformat()
895
+ })
896
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
897
+ continue
898
+
899
+ return quantum_procs
900
+
901
+ def get_docker_containers(self, docker_client=None) -> List[Dict]:
902
+ """List Docker containers if available"""
903
+ containers = []
904
+
905
+ try:
906
+ import docker
907
+ client = docker_client or docker.from_env()
908
+
909
+ for container in client.containers.list(all=True):
910
+ ports = {}
911
+ if container.ports:
912
+ for internal, external in container.ports.items():
913
+ if external:
914
+ ports[internal] = external[0]['HostPort'] if external else None
915
+
916
+ containers.append({
917
+ "id": container.short_id,
918
+ "name": container.name,
919
+ "image": container.image.tags[0] if container.image.tags else "unknown",
920
+ "status": container.status,
921
+ "ports": ports,
922
+ "created": container.attrs.get('Created', '')[:19]
923
+ })
924
+ except ImportError:
925
+ logger.warning("Docker SDK not installed")
926
+ except Exception as e:
927
+ logger.warning(f"Docker not available: {e}")
928
+
929
+ return containers
930
+
931
+ def get_system_resources(self) -> Dict:
932
+ """Get system resource usage"""
933
+ if not self._psutil_available:
934
+ return {"available": False}
935
+
936
+ import psutil
937
+
938
+ return {
939
+ "available": True,
940
+ "cpu_percent": psutil.cpu_percent(interval=0.1),
941
+ "cpu_count": psutil.cpu_count(),
942
+ "memory": {
943
+ "total": psutil.virtual_memory().total,
944
+ "available": psutil.virtual_memory().available,
945
+ "percent": psutil.virtual_memory().percent
946
+ },
947
+ "disk": {
948
+ "total": psutil.disk_usage('/').total,
949
+ "free": psutil.disk_usage('/').free,
950
+ "percent": psutil.disk_usage('/').percent
951
+ }
952
+ }
953
+
954
+ def get_overview(self, docker_client=None) -> Dict:
955
+ """Get complete resource overview"""
956
+ return {
957
+ "ports_in_use": self.scan_ports_in_use(),
958
+ "quantum_processes": self.detect_quantum_processes(),
959
+ "docker_containers": self.get_docker_containers(docker_client),
960
+ "system": self.get_system_resources(),
961
+ "scanned_at": datetime.utcnow().isoformat()
962
+ }
963
+
964
+
965
+ class ResourceManager:
966
+ """Centralized resource management service"""
967
+
968
+ def __init__(self, db: Session):
969
+ self.db = db
970
+ self.port_manager = PortManager(db)
971
+ self.secret_manager = SecretManager(db)
972
+ self.service_registry = ServiceRegistryManager(db)
973
+ self.discovery = ResourceDiscovery()
974
+
975
+ # Property aliases for API convenience
976
+ @property
977
+ def ports(self) -> PortManager:
978
+ return self.port_manager
979
+
980
+ @property
981
+ def secrets(self) -> SecretManager:
982
+ return self.secret_manager
983
+
984
+ @property
985
+ def services(self) -> 'ServiceRegistryManager':
986
+ return self.service_registry
987
+
988
+ # Port Management
989
+ def allocate_port(self, *args, **kwargs) -> int:
990
+ return self.port_manager.allocate(*args, **kwargs)
991
+
992
+ def release_port(self, *args, **kwargs) -> bool:
993
+ return self.port_manager.release(*args, **kwargs)
994
+
995
+ def reserve_port(self, *args, **kwargs) -> bool:
996
+ return self.port_manager.reserve(*args, **kwargs)
997
+
998
+ def get_port(self, *args, **kwargs) -> Optional[int]:
999
+ return self.port_manager.get(*args, **kwargs)
1000
+
1001
+ def list_allocations(self, *args, **kwargs) -> List[PortAllocation]:
1002
+ return self.port_manager.list_allocations(*args, **kwargs)
1003
+
1004
+ def check_port_available(self, port: int, host: str = "localhost") -> bool:
1005
+ return self.port_manager.is_available(port, host)
1006
+
1007
+ def get_port_ranges(self) -> List[PortRange]:
1008
+ return self.port_manager.get_ranges()
1009
+
1010
+ def get_port_usage(self, host: str = None) -> Dict:
1011
+ return self.port_manager.get_usage_stats(host)
1012
+
1013
+ # Secret Management
1014
+ def set_secret(self, *args, **kwargs) -> Secret:
1015
+ return self.secret_manager.set(*args, **kwargs)
1016
+
1017
+ def get_secret(self, *args, **kwargs) -> Optional[str]:
1018
+ return self.secret_manager.get(*args, **kwargs)
1019
+
1020
+ def list_secrets(self, *args, **kwargs) -> List[Dict]:
1021
+ return self.secret_manager.list(*args, **kwargs)
1022
+
1023
+ def delete_secret(self, *args, **kwargs) -> bool:
1024
+ return self.secret_manager.delete(*args, **kwargs)
1025
+
1026
+ def rotate_secret(self, *args, **kwargs) -> bool:
1027
+ return self.secret_manager.rotate(*args, **kwargs)
1028
+
1029
+ # Service Registry
1030
+ def register_service(self, *args, **kwargs) -> ServiceRegistry:
1031
+ return self.service_registry.register(*args, **kwargs)
1032
+
1033
+ def discover(self, *args, **kwargs) -> Optional[ServiceRegistry]:
1034
+ return self.service_registry.discover(*args, **kwargs)
1035
+
1036
+ def discover_by_type(self, *args, **kwargs) -> List[ServiceRegistry]:
1037
+ return self.service_registry.discover_by_type(*args, **kwargs)
1038
+
1039
+ def deregister_service(self, name: str) -> bool:
1040
+ return self.service_registry.deregister(name)
1041
+
1042
+ async def health_check_all(self) -> Dict[str, Dict]:
1043
+ return await self.service_registry.health_check_all()
1044
+
1045
+ # Resource Discovery
1046
+ def scan_resources(self, docker_client=None) -> Dict:
1047
+ """Perform full resource scan"""
1048
+ return self.discovery.get_overview(docker_client)
1049
+
1050
+ def scan_ports(self, host: str = "localhost") -> List[Dict]:
1051
+ """Scan ports in use"""
1052
+ return self.discovery.scan_ports_in_use(host)
1053
+
1054
+ def scan_processes(self) -> List[Dict]:
1055
+ """Scan for Quantum processes"""
1056
+ return self.discovery.detect_quantum_processes()
1057
+
1058
+ def scan_containers(self, docker_client=None) -> List[Dict]:
1059
+ """Scan Docker containers"""
1060
+ return self.discovery.get_docker_containers(docker_client)
1061
+
1062
+ def get_system_resources(self) -> Dict:
1063
+ """Get system resource usage"""
1064
+ return self.discovery.get_system_resources()
1065
+
1066
+ def sync_discovered_ports(self, docker_client=None) -> Dict:
1067
+ """
1068
+ Sync discovered ports with database allocations.
1069
+ This reconciles what's actually running with what's tracked.
1070
+ """
1071
+ discovered = self.discovery.scan_ports_in_use()
1072
+ allocations = self.port_manager.list_allocations(include_released=False)
1073
+
1074
+ allocated_ports = {a.port: a for a in allocations}
1075
+ discovered_ports = {p['port']: p for p in discovered}
1076
+
1077
+ # Find orphaned allocations (tracked but not running)
1078
+ orphaned = []
1079
+ for port, alloc in allocated_ports.items():
1080
+ if port not in discovered_ports:
1081
+ orphaned.append({
1082
+ "port": port,
1083
+ "service": alloc.service_name,
1084
+ "status": "not_running"
1085
+ })
1086
+
1087
+ # Find untracked ports (running but not allocated)
1088
+ untracked = []
1089
+ for port, info in discovered_ports.items():
1090
+ if port not in allocated_ports and 1024 < port < 65535:
1091
+ untracked.append({
1092
+ "port": port,
1093
+ "process": info.get("process_name", "unknown"),
1094
+ "status": "untracked"
1095
+ })
1096
+
1097
+ return {
1098
+ "tracked": len(allocations),
1099
+ "discovered": len(discovered),
1100
+ "orphaned": orphaned,
1101
+ "untracked": untracked[:50], # Limit to 50
1102
+ "synced_at": datetime.utcnow().isoformat()
1103
+ }
1104
+
1105
+
1106
+ # Singleton instance
1107
+ _resource_manager = None
1108
+
1109
+
1110
+ def get_resource_manager(db: Session = None) -> ResourceManager:
1111
+ """Get resource manager instance"""
1112
+ if db:
1113
+ return ResourceManager(db)
1114
+
1115
+ global _resource_manager
1116
+ if _resource_manager is None:
1117
+ _resource_manager = ResourceManager(SessionLocal())
1118
+ return _resource_manager