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,289 @@
1
+ """
2
+ The websocket transport that q:websocket never had.
3
+
4
+ Before this, `quantum/runtime/websocket_service.py` was pure bookkeeping: it
5
+ created a connection record in state CONNECTING, nothing ever moved it to
6
+ OPEN, and send_message — which only queues when the state is OPEN — always
7
+ returned False into a list nothing drained. The tag returned a
8
+ connection-shaped dict for a connection that did not exist.
9
+
10
+ This opens real client connections with the `websockets` library, on a
11
+ background asyncio loop, and drives the state machine and the handlers the
12
+ service already defines. It is deliberately a CLIENT transport: `q:websocket
13
+ url="wss://..."` connects a Quantum app to somewhere else, which is what the
14
+ tag's url= attribute always meant.
15
+
16
+ Optional by design: if `websockets` is not installed the service keeps its
17
+ honest stub behaviour and says so, rather than failing to import.
18
+ """
19
+
20
+ import asyncio
21
+ import json
22
+ import logging
23
+ import threading
24
+ from concurrent.futures import ThreadPoolExecutor
25
+ from typing import Any, Callable, Dict, Optional
26
+
27
+ logger = logging.getLogger('quantum.websocket')
28
+
29
+ try:
30
+ import websockets as _ws
31
+ HAS_WEBSOCKETS = True
32
+ except ImportError: # pragma: no cover - environment dependent
33
+ _ws = None
34
+ HAS_WEBSOCKETS = False
35
+
36
+
37
+ class WebSocketTransport:
38
+ """Owns one background event loop and the live client connections on it."""
39
+
40
+ def __init__(self):
41
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
42
+ self._thread: Optional[threading.Thread] = None
43
+ self._sockets: Dict[str, Any] = {} # connection id -> protocol
44
+ self._lock = threading.RLock()
45
+ # q:websocket handlers are Quantum bodies: they run a component
46
+ # context and can block for a while. Running them on the event loop
47
+ # would stall every other connection, so they go to a worker thread.
48
+ #
49
+ # UM executor POR CONEXAO, com um unico worker cada.
50
+ #
51
+ # Era um executor unico com max_workers=1 para o transporte inteiro,
52
+ # justificado por "one, so message order is preserved". A ordem por
53
+ # conexao esta certa; o alcance nao estava. Com um so worker, um
54
+ # handler on-message lento de UMA conexao segurava a fila de TODAS —
55
+ # dez conexoes, e um <q:query> de 2s numa delas atrasava as outras
56
+ # nove em 2s cada. Nada em q:websocket ordena mensagens de conexoes
57
+ # diferentes entre si, entao nao ha ordem a preservar ali.
58
+ #
59
+ # Um worker por conexao mantem a ordem onde ela existe e isola as
60
+ # conexoes umas das outras.
61
+ self._dispatchers: Dict[str, ThreadPoolExecutor] = {}
62
+ self._closed = False
63
+
64
+ def _dispatcher_for(self, conn_id: str) -> Optional[ThreadPoolExecutor]:
65
+ with self._lock:
66
+ pool = self._dispatchers.get(conn_id)
67
+ if pool is not None:
68
+ # Um worker que JA existe continua aceitando trabalho mesmo
69
+ # durante o shutdown: e por ele que passa o on_close de cada
70
+ # conexao sendo fechada. Recusar aqui faria o corpo
71
+ # `on-close` do app nunca rodar num encerramento — que e
72
+ # justamente quando ele importa.
73
+ return pool
74
+ if self._closed:
75
+ return None
76
+ pool = ThreadPoolExecutor(
77
+ max_workers=1,
78
+ thread_name_prefix=f'quantum-ws-{conn_id[:8]}')
79
+ self._dispatchers[conn_id] = pool
80
+ return pool
81
+
82
+ def _fire(self, conn_id: str, fn: Callable, *args) -> None:
83
+ """Run a user handler off the event loop, never letting it escape."""
84
+ def guarded():
85
+ try:
86
+ fn(*args)
87
+ except Exception:
88
+ logger.exception("q:websocket handler raised")
89
+
90
+ pool = self._dispatcher_for(conn_id)
91
+ if pool is None:
92
+ return
93
+ try:
94
+ pool.submit(guarded)
95
+ except RuntimeError: # pool already shut down
96
+ pass
97
+
98
+ # -- loop lifecycle ----------------------------------------------------
99
+
100
+ def _ensure_loop(self) -> asyncio.AbstractEventLoop:
101
+ with self._lock:
102
+ # Um shutdown() nao pode aposentar o transporte: get_transport()
103
+ # devolve um singleton de processo, e reconectar depois de fechar
104
+ # tudo e uso normal.
105
+ self._closed = False
106
+ if self._loop and self._loop.is_running():
107
+ return self._loop
108
+ self._loop = asyncio.new_event_loop()
109
+
110
+ def run():
111
+ asyncio.set_event_loop(self._loop)
112
+ self._loop.run_forever()
113
+
114
+ self._thread = threading.Thread(
115
+ target=run, name="quantum-websocket", daemon=True)
116
+ self._thread.start()
117
+ return self._loop
118
+
119
+ def shutdown(self) -> None:
120
+ # As conexoes sao fechadas FORA do lock.
121
+ #
122
+ # Antes o laco inteiro rodava com self._lock preso. close() espera o
123
+ # `sock.close()` terminar no event loop — e o pump() daquela conexao,
124
+ # ao terminar, faz `with self._lock` no finally, na thread do loop.
125
+ # Com o lock preso aqui, o loop parava no finally, `sock.close()`
126
+ # nunca completava, e cada close() gastava os 5s inteiros do timeout
127
+ # antes de desistir. Cinco conexoes abertas = 25 segundos de shutdown.
128
+ with self._lock:
129
+ self._closed = True
130
+ conn_ids = list(self._sockets)
131
+
132
+ # Fechar PRIMEIRO, com os workers ainda vivos: o on_close de cada
133
+ # conexao roda no worker dela, e o pump, ao terminar, aposenta esse
134
+ # worker sozinho. Recolher os executores antes deixaria o corpo
135
+ # `on-close` do app sem ninguem para executa-lo.
136
+ for conn_id in conn_ids:
137
+ self.close(conn_id)
138
+
139
+ with self._lock:
140
+ if self._loop and self._loop.is_running():
141
+ self._loop.call_soon_threadsafe(self._loop.stop)
142
+ self._loop = None
143
+ self._thread = None
144
+ restantes = list(self._dispatchers.values())
145
+ self._dispatchers.clear()
146
+
147
+ # E os workers de handler tambem morrem. Ficavam vivos para sempre:
148
+ # cada reset_transport() em teste deixava mais uma thread
149
+ # 'quantum-ws-handler' parada no processo, e um app que fechasse suas
150
+ # conexoes nunca recuperava a thread.
151
+ for pool in restantes:
152
+ pool.shutdown(wait=False)
153
+
154
+ # -- connecting --------------------------------------------------------
155
+
156
+ def connect(
157
+ self,
158
+ conn_id: str,
159
+ url: str,
160
+ on_open: Callable[[], None],
161
+ on_message: Callable[[Any], None],
162
+ on_close: Callable[[], None],
163
+ on_error: Callable[[Exception], None],
164
+ timeout: float = 10.0,
165
+ ):
166
+ """Open a real connection. Returns (ok, error).
167
+
168
+ Blocks up to `timeout` so q:websocket can report a truthful state
169
+ instead of leaving the caller with a record that says CONNECTING
170
+ forever — and returns the failure reason with the answer, because
171
+ the on_error callback lands on the handler thread later and the
172
+ caller needs it now.
173
+ """
174
+ if not HAS_WEBSOCKETS:
175
+ logger.warning(
176
+ "q:websocket needs the 'websockets' package to open a real "
177
+ "connection (pip install websockets). Leaving %s unconnected.",
178
+ url
179
+ )
180
+ return False, "the 'websockets' package is not installed"
181
+
182
+ loop = self._ensure_loop()
183
+ opened = threading.Event()
184
+ result = {'ok': False, 'error': None}
185
+
186
+ async def pump():
187
+ try:
188
+ async with _ws.connect(url, open_timeout=timeout) as sock:
189
+ with self._lock:
190
+ self._sockets[conn_id] = sock
191
+ result['ok'] = True
192
+ opened.set()
193
+ self._fire(conn_id, on_open)
194
+ async for raw in sock:
195
+ # JSON when it parses, raw text otherwise — the same
196
+ # rule the send side uses.
197
+ try:
198
+ payload = json.loads(raw)
199
+ except (ValueError, TypeError):
200
+ payload = raw
201
+ self._fire(conn_id, on_message, payload)
202
+ except Exception as exc:
203
+ result['error'] = exc
204
+ opened.set()
205
+ self._fire(conn_id, on_error, exc)
206
+ finally:
207
+ with self._lock:
208
+ self._sockets.pop(conn_id, None)
209
+ self._fire(conn_id, on_close)
210
+ # O worker desta conexao acaba com ela. shutdown(wait=False)
211
+ # nao cancela o que ja foi submetido, entao o on_close acima
212
+ # ainda roda; so nao fica uma thread parada por conexao ja
213
+ # encerrada.
214
+ with self._lock:
215
+ pool = self._dispatchers.pop(conn_id, None)
216
+ if pool is not None:
217
+ pool.shutdown(wait=False)
218
+
219
+ asyncio.run_coroutine_threadsafe(pump(), loop)
220
+ if not opened.wait(timeout=timeout + 1):
221
+ logger.warning("q:websocket: connecting to %s timed out", url)
222
+ return False, f"connection to {url} timed out"
223
+
224
+ if result['error'] is not None:
225
+ logger.warning("q:websocket could not connect to %s: %s",
226
+ url, result['error'])
227
+ return False, str(result['error']) or type(result['error']).__name__
228
+ return bool(result['ok']), None
229
+
230
+ # -- sending -----------------------------------------------------------
231
+
232
+ def send(self, conn_id: str, data: Any, timeout: float = 5.0) -> bool:
233
+ """Actually transmit. Returns True only when the frame was written."""
234
+ with self._lock:
235
+ sock = self._sockets.get(conn_id)
236
+ if sock is None or self._loop is None:
237
+ return False
238
+
239
+ payload = data if isinstance(data, (str, bytes)) else json.dumps(
240
+ data, default=str)
241
+ fut = asyncio.run_coroutine_threadsafe(sock.send(payload), self._loop)
242
+ try:
243
+ fut.result(timeout=timeout)
244
+ return True
245
+ except Exception as exc:
246
+ logger.warning("q:websocket send failed on %s: %s", conn_id, exc)
247
+ return False
248
+
249
+ # -- closing -----------------------------------------------------------
250
+
251
+ def close(self, conn_id: str, timeout: float = 5.0) -> bool:
252
+ with self._lock:
253
+ sock = self._sockets.get(conn_id)
254
+ if sock is None or self._loop is None:
255
+ return False
256
+ fut = asyncio.run_coroutine_threadsafe(sock.close(), self._loop)
257
+ try:
258
+ fut.result(timeout=timeout)
259
+ except Exception:
260
+ pass
261
+ with self._lock:
262
+ self._sockets.pop(conn_id, None)
263
+ return True
264
+
265
+ def is_open(self, conn_id: str) -> bool:
266
+ with self._lock:
267
+ return conn_id in self._sockets
268
+
269
+
270
+ _transport: Optional[WebSocketTransport] = None
271
+ _transport_lock = threading.Lock()
272
+
273
+
274
+ def get_transport() -> WebSocketTransport:
275
+ """The process-wide transport (one event loop for every connection)."""
276
+ global _transport
277
+ with _transport_lock:
278
+ if _transport is None:
279
+ _transport = WebSocketTransport()
280
+ return _transport
281
+
282
+
283
+ def reset_transport() -> None:
284
+ """Drop the transport, closing whatever it holds (tests)."""
285
+ global _transport
286
+ with _transport_lock:
287
+ if _transport is not None:
288
+ _transport.shutdown()
289
+ _transport = None
@@ -0,0 +1,101 @@
1
+ """
2
+ Quantum WSGI Entry Point
3
+
4
+ Factory for creating Flask app instances for Gunicorn deployment.
5
+ Loads configuration from environment or config file.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ # Ensure src directory is in path
13
+
14
+ from quantum.runtime.web_server import QuantumWebServer
15
+
16
+
17
+ def _run_app_startup():
18
+ """
19
+ Run app-specific startup.py if present.
20
+
21
+ This allows apps to initialize databases, apply runtime patches,
22
+ and perform other setup before the web server starts.
23
+ The startup module should define a run() function.
24
+
25
+ Note that this executes a Python file from `/app` or from the CURRENT
26
+ DIRECTORY, and that `app = create_app()` at the bottom of this module
27
+ runs it at IMPORT time. Importing this module therefore executes the
28
+ working directory's startup.py. That is the documented Docker convention
29
+ and it is kept, but it is loud now: silently executing a file because it
30
+ happens to sit in the directory you launched from is the kind of thing
31
+ that should say so.
32
+ """
33
+ startup_path = Path('/app/startup.py')
34
+ if not startup_path.exists():
35
+ # Also check current working directory
36
+ startup_path = Path('startup.py')
37
+ if startup_path.exists():
38
+ print(f"[WSGI] executing {startup_path.resolve()}", flush=True)
39
+ import importlib.util
40
+ spec = importlib.util.spec_from_file_location('app_startup', str(startup_path))
41
+ mod = importlib.util.module_from_spec(spec)
42
+ spec.loader.exec_module(mod)
43
+ if hasattr(mod, 'run'):
44
+ mod.run()
45
+ print("[WSGI] App startup.py executed", flush=True)
46
+
47
+
48
+ def create_app(config_path: str = None) -> 'Flask':
49
+ """
50
+ Create and configure the Quantum Flask application.
51
+
52
+ This is the factory function used by Gunicorn.
53
+
54
+ Args:
55
+ config_path: Path to quantum.config.yaml. If not provided,
56
+ uses QUANTUM_CONFIG env var or default path.
57
+
58
+ Returns:
59
+ Configured Flask application instance
60
+ """
61
+ # Run app startup hooks before creating the server
62
+ _run_app_startup()
63
+
64
+ # Determine config path
65
+ if config_path is None:
66
+ config_path = os.environ.get('QUANTUM_CONFIG', '/app/quantum.config.yaml')
67
+
68
+ # Check for app-specific config first
69
+ if os.path.exists(config_path):
70
+ server = QuantumWebServer(config_path)
71
+ else:
72
+ # Fall back to default config
73
+ server = QuantumWebServer()
74
+
75
+ return server.app
76
+
77
+
78
+ # Create app instance for Gunicorn
79
+ #
80
+ # Usage: gunicorn "quantum.runtime.wsgi:app"
81
+ #
82
+ # This said `src.runtime.wsgi:app` — the layout that stopped existing when
83
+ # `src/` became the `quantum/` package. The command in the one file whose
84
+ # entire purpose is being that command did not import. (web_server.py's
85
+ # docstring had the same defect; this file was missed.)
86
+ app = create_app()
87
+
88
+
89
+ # Also support: gunicorn "quantum.runtime.wsgi:create_app()"
90
+ def get_app():
91
+ """Alternative factory for Gunicorn."""
92
+ return create_app()
93
+
94
+
95
+ if __name__ == '__main__':
96
+ # For local testing
97
+ app.run(
98
+ host=os.environ.get('QUANTUM_HOST', '0.0.0.0'),
99
+ port=int(os.environ.get('QUANTUM_PORT', 8080)),
100
+ debug=os.environ.get('QUANTUM_DEBUG', 'false').lower() == 'true'
101
+ )
quantum/services.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ Declared services: Python functions a .q file calls by name (SVC-1..3).
3
+
4
+ # myapp/services.py
5
+ from quantum.services import service
6
+
7
+ @service("projects.list")
8
+ def list_projects(status: str = "active"):
9
+ return [...]
10
+
11
+ # quantum.config.yaml
12
+ services:
13
+ - myapp.services
14
+
15
+ <!-- components/projects.q -->
16
+ <q:invoke name="projects" service="projects.list">
17
+ <q:param name="status" value="active" />
18
+ </q:invoke>
19
+
20
+ This is the alternative to <q:python> for logic that belongs in Python — file
21
+ system, processes, third-party APIs. The Python is written, reviewed and tested
22
+ as ordinary Python; the page only names what it calls. A module runs only when
23
+ quantum.config.yaml lists it: nothing is discovered or imported implicitly.
24
+ """
25
+
26
+ import importlib
27
+ import threading
28
+ from typing import Any, Callable, Dict, Iterable
29
+
30
+ _registry: Dict[str, Callable[..., Any]] = {}
31
+ _loaded_modules: set = set()
32
+ _lock = threading.Lock()
33
+
34
+
35
+ class ServiceError(Exception):
36
+ """A declared service could not be registered, loaded or found."""
37
+
38
+
39
+ def service(name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
40
+ """Register a function under `name` (SVC-1). Returns the function unchanged."""
41
+ if not isinstance(name, str) or not name.strip():
42
+ raise ServiceError("@service needs a name, like @service('projects.list')")
43
+
44
+ def register(func: Callable[..., Any]) -> Callable[..., Any]:
45
+ with _lock:
46
+ existing = _registry.get(name)
47
+ if existing is not None and existing is not func and \
48
+ (existing.__module__, existing.__qualname__) != (func.__module__, func.__qualname__):
49
+ raise ServiceError(
50
+ f"service '{name}' is already registered by "
51
+ f"{existing.__module__}.{existing.__qualname__}")
52
+ _registry[name] = func
53
+ return func
54
+
55
+ return register
56
+
57
+
58
+ def load(modules: Iterable[str], project_dir: str = None) -> None:
59
+ """Import the modules listed under `services:` in quantum.config.yaml (SVC-2).
60
+
61
+ The project folder goes on sys.path first: the `quantum` console script,
62
+ unlike `python -m`, does not put the current directory there, so
63
+ `services: [myapp.services]` could not be imported by `quantum start`.
64
+ """
65
+ import os
66
+ import sys
67
+ if isinstance(modules, str):
68
+ modules = [modules]
69
+ raiz = os.path.abspath(project_dir or os.getcwd())
70
+ if modules and raiz not in sys.path:
71
+ sys.path.insert(0, raiz)
72
+ for module in modules or []:
73
+ if module in _loaded_modules:
74
+ continue
75
+ try:
76
+ ja_importado = module in sys.modules
77
+ modulo = importlib.import_module(module)
78
+ if ja_importado and not any(f.__module__ == module for f in _registry.values()):
79
+ # Imported before the registry was cleared (or before it
80
+ # existed): its @service decorators already ran and would not
81
+ # run again on a plain import.
82
+ importlib.reload(modulo)
83
+ except ImportError as exc:
84
+ raise ServiceError(
85
+ f"quantum.config.yaml lists service module '{module}', which could not "
86
+ f"be imported: {exc}") from exc
87
+ _loaded_modules.add(module)
88
+
89
+
90
+ def get(name: str) -> Callable[..., Any]:
91
+ """The function registered as `name`, or an error listing what exists (SVC-3)."""
92
+ func = _registry.get(name)
93
+ if func is None:
94
+ disponiveis = ', '.join(sorted(_registry)) or 'none'
95
+ raise ServiceError(
96
+ f"no service named '{name}' (registered: {disponiveis}). Declare it with "
97
+ f"@service('{name}') in a module listed under services: in quantum.config.yaml")
98
+ return func
99
+
100
+
101
+ def registered() -> Dict[str, Callable[..., Any]]:
102
+ return dict(_registry)
103
+
104
+
105
+ def _reset() -> None:
106
+ """Tests only."""
107
+ with _lock:
108
+ _registry.clear()
109
+ _loaded_modules.clear()
@@ -0,0 +1 @@
1
+ # Utils - placeholder para futuras utilidades
@@ -0,0 +1,4 @@
1
+ """Quantum Admin: .q screens (components/admin) over declared services (services/).
2
+
3
+ Started with `quantum admin`; needs the [admin] extra.
4
+ """
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Quantum Admin - Connectors Page
3
+ * Dynamic Provider <-> Type filtering
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ var typeProviders = {
9
+ database: ['postgres', 'mysql', 'mariadb', 'mongodb', 'sqlite', 'influxdb'],
10
+ mq: ['rabbitmq', 'redis_queue', 'kafka'],
11
+ cache: ['redis', 'memcached'],
12
+ storage: ['s3', 'minio', 'local'],
13
+ ai: ['ollama', 'lmstudio', 'anthropic', 'openai', 'openrouter']
14
+ };
15
+
16
+ // Build reverse map: provider -> type
17
+ var providerType = {};
18
+ Object.keys(typeProviders).forEach(function (type) {
19
+ typeProviders[type].forEach(function (prov) {
20
+ providerType[prov] = type;
21
+ });
22
+ });
23
+
24
+ var typeSelect = document.querySelector('select[name="conn_type"]');
25
+ var providerSelect = document.querySelector('select[name="provider"]');
26
+
27
+ if (!typeSelect || !providerSelect) return;
28
+
29
+ // Store original options with their optgroup labels
30
+ var allOptions = [];
31
+ var optgroups = providerSelect.querySelectorAll('optgroup');
32
+ optgroups.forEach(function (og) {
33
+ var label = og.getAttribute('label') || '';
34
+ var opts = og.querySelectorAll('option');
35
+ opts.forEach(function (opt) {
36
+ allOptions.push({ value: opt.value, text: opt.textContent, group: label });
37
+ });
38
+ });
39
+
40
+ var syncing = false;
41
+
42
+ function rebuildProviderOptions(allowedType) {
43
+ // Remove everything except the placeholder
44
+ while (providerSelect.options.length > 1) {
45
+ providerSelect.remove(1);
46
+ }
47
+ // Remove old optgroups
48
+ var oldGroups = providerSelect.querySelectorAll('optgroup');
49
+ oldGroups.forEach(function (g) { g.remove(); });
50
+
51
+ if (!allowedType) {
52
+ // Show all grouped
53
+ var groups = {};
54
+ allOptions.forEach(function (o) {
55
+ if (!groups[o.group]) groups[o.group] = [];
56
+ groups[o.group].push(o);
57
+ });
58
+ Object.keys(groups).forEach(function (gName) {
59
+ var og = document.createElement('optgroup');
60
+ og.label = gName;
61
+ groups[gName].forEach(function (o) {
62
+ var opt = document.createElement('option');
63
+ opt.value = o.value;
64
+ opt.textContent = o.text;
65
+ og.appendChild(opt);
66
+ });
67
+ providerSelect.appendChild(og);
68
+ });
69
+ } else {
70
+ // Show only providers for this type, flat (no optgroup)
71
+ var allowed = typeProviders[allowedType] || [];
72
+ allOptions.forEach(function (o) {
73
+ if (allowed.indexOf(o.value) !== -1) {
74
+ var opt = document.createElement('option');
75
+ opt.value = o.value;
76
+ opt.textContent = o.text;
77
+ providerSelect.appendChild(opt);
78
+ }
79
+ });
80
+ }
81
+ }
82
+
83
+ typeSelect.addEventListener('change', function () {
84
+ if (syncing) return;
85
+ syncing = true;
86
+ var selectedType = typeSelect.value;
87
+ var currentProvider = providerSelect.value;
88
+ rebuildProviderOptions(selectedType);
89
+
90
+ // Try to keep current provider if it belongs to the new type
91
+ if (currentProvider) {
92
+ var allowed = typeProviders[selectedType] || [];
93
+ if (allowed.indexOf(currentProvider) !== -1) {
94
+ providerSelect.value = currentProvider;
95
+ } else {
96
+ providerSelect.value = '';
97
+ }
98
+ }
99
+ syncing = false;
100
+ });
101
+
102
+ providerSelect.addEventListener('change', function () {
103
+ if (syncing) return;
104
+ syncing = true;
105
+ var prov = providerSelect.value;
106
+ if (prov && providerType[prov]) {
107
+ typeSelect.value = providerType[prov];
108
+ // Also filter providers to match the auto-selected type
109
+ rebuildProviderOptions(providerType[prov]);
110
+ providerSelect.value = prov;
111
+ }
112
+ syncing = false;
113
+ });
114
+
115
+ // On load: if type is pre-selected (edit mode), filter providers
116
+ if (typeSelect.value) {
117
+ var editProvider = providerSelect.value;
118
+ rebuildProviderOptions(typeSelect.value);
119
+ if (editProvider) {
120
+ providerSelect.value = editProvider;
121
+ }
122
+ }
123
+ })();