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,76 @@
1
+ """
2
+ Terminal Engine - Builder/Orchestrator
3
+
4
+ Orchestrates the compilation pipeline:
5
+ ApplicationNode (type="terminal") -> extract screens/keybindings/css -> TerminalCodeGenerator -> .py file
6
+
7
+ Usage:
8
+ builder = TerminalBuilder()
9
+ python_code = builder.build(app_node)
10
+ builder.build_to_file(app_node, "output.py")
11
+ """
12
+
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from quantum.core.ast_nodes import ApplicationNode
18
+ from quantum.core.features.terminal_engine.src.ast_nodes import (
19
+ ScreenNode, KeybindingNode, ServiceNode, CssNode,
20
+ )
21
+ from quantum.runtime.terminal_code_generator import TerminalCodeGenerator
22
+
23
+
24
+ class TerminalBuildError(Exception):
25
+ """Error during terminal app build."""
26
+ pass
27
+
28
+
29
+ class TerminalBuilder:
30
+ """Builds a standalone Python TUI app from a Quantum terminal ApplicationNode."""
31
+
32
+ def build(self, app: ApplicationNode) -> str:
33
+ """Build Python source string from an ApplicationNode with type='terminal'.
34
+
35
+ The ApplicationNode is expected to have:
36
+ - app.screens: list of ScreenNode
37
+ - app.terminal_css: str (optional)
38
+ - app.keybindings: list of KeybindingNode (optional)
39
+ - app.services: list of ServiceNode (optional)
40
+ """
41
+ screens = getattr(app, 'screens', [])
42
+ terminal_css = getattr(app, 'terminal_css', '')
43
+ keybindings = getattr(app, 'keybindings', [])
44
+ services = getattr(app, 'services', [])
45
+
46
+ if not screens:
47
+ raise TerminalBuildError("No screens found in terminal application")
48
+
49
+ valid_screens = [s for s in screens if isinstance(s, ScreenNode)]
50
+ if not valid_screens:
51
+ raise TerminalBuildError("No valid ScreenNode found in terminal application")
52
+
53
+ generator = TerminalCodeGenerator()
54
+ python_code = generator.generate(
55
+ screens=valid_screens,
56
+ title=app.app_id,
57
+ terminal_css=terminal_css,
58
+ keybindings=[k for k in keybindings if isinstance(k, KeybindingNode)],
59
+ services=[s for s in services if isinstance(s, ServiceNode)],
60
+ functions=getattr(app, 'functions', None) or [],
61
+ state_vars=getattr(app, 'state_vars', None) or [],
62
+ )
63
+ return python_code
64
+
65
+ def build_to_file(self, app: ApplicationNode, output_path: Optional[str] = None) -> str:
66
+ """Build and write .py file. Returns the output file path."""
67
+ python_code = self.build(app)
68
+
69
+ if output_path is None:
70
+ output_path = f"{app.app_id}.py"
71
+
72
+ path = Path(output_path)
73
+ path.parent.mkdir(parents=True, exist_ok=True)
74
+ path.write_text(python_code, encoding='utf-8')
75
+
76
+ return str(path.resolve())
@@ -0,0 +1,607 @@
1
+ """
2
+ Terminal Engine - Code Generator
3
+
4
+ Transforms Terminal AST nodes into Python code that runs with Textual.
5
+ This is a compiler/transpiler: Python generates Python TUI apps.
6
+
7
+ Uses PyBuilder for structured, safe Python generation.
8
+ """
9
+
10
+ import json
11
+ import re
12
+ from typing import List, Dict, Any, Optional
13
+
14
+ from quantum.core.features.terminal_engine.src.ast_nodes import (
15
+ ScreenNode, PanelNode, LayoutNode, TableNode, ColumnNode,
16
+ TerminalInputNode, ButtonNode, MenuNode, OptionNode,
17
+ TextNode_Terminal, ProgressNode, TreeNode, TabsNode, TabNode,
18
+ LogNode_Terminal, HeaderNode_Terminal, FooterNode, StatusNode,
19
+ KeybindingNode, TimerNode_Terminal, ServiceNode, CssNode,
20
+ OnEventNode_Terminal, RawCodeNode_Terminal,
21
+ )
22
+ from quantum.core.ast_nodes import QuantumNode, QueryNode
23
+ from quantum.core.features.conditionals.src.ast_node import IfNode
24
+ from quantum.core.features.loops.src.ast_node import LoopNode
25
+ from quantum.core.features.state_management.src.ast_node import SetNode
26
+ from quantum.core.features.functions.src.ast_node import FunctionNode
27
+
28
+ from quantum.runtime.terminal_templates import (
29
+ APP_TEMPLATE, PyBuilder, py_string, py_id, py_bool, split_call,
30
+ )
31
+
32
+
33
+ class TerminalCodeGenerator:
34
+ """Generates standalone Python Textual app from a terminal AST."""
35
+
36
+ def __init__(self):
37
+ self._state_vars: List[Dict] = [] # {name, value, type}
38
+ self._functions: List[FunctionNode] = []
39
+ self._keybindings: List[Dict] = []
40
+ self._timers: List[Dict] = []
41
+ self._services: List[Dict] = []
42
+ self._css_blocks: List[str] = []
43
+ self._queries: List[Dict] = []
44
+ self._screens: List[ScreenNode] = []
45
+ self._extra_imports: List[str] = []
46
+
47
+ # ------------------------------------------------------------------
48
+ # Public API
49
+ # ------------------------------------------------------------------
50
+
51
+ def generate(self, screens: List[ScreenNode], title: str = "Quantum TUI",
52
+ terminal_css: str = "", keybindings: List = None,
53
+ services: List = None, functions: List = None,
54
+ state_vars: List = None) -> str:
55
+ """Generate full Python app from ScreenNodes.
56
+
57
+ `functions` e `state_vars` sao os <q:function>/<q:set> escritos no
58
+ topo do <q:application>. So os declarados DENTRO de um <qt:screen>
59
+ chegavam ate aqui, entao um `on-click` apontando para uma funcao
60
+ global caia no ramo "acao desconhecida" e virava um
61
+ `self.notify("Action: salvar")`: o botao anunciava a acao em vez de
62
+ executa-la.
63
+ """
64
+ for func in (functions or []):
65
+ if func not in self._functions:
66
+ self._functions.append(func)
67
+ for var in (state_vars or []):
68
+ self._state_vars.append({
69
+ 'name': var.name,
70
+ 'value': var.value,
71
+ 'type': getattr(var, 'type', 'string'),
72
+ })
73
+
74
+ if terminal_css:
75
+ self._css_blocks.append(terminal_css)
76
+
77
+ # Process top-level keybindings/services
78
+ for kb in (keybindings or []):
79
+ if isinstance(kb, KeybindingNode):
80
+ self._keybindings.append({
81
+ 'key': kb.key,
82
+ 'action': kb.action,
83
+ 'description': kb.description or kb.action,
84
+ })
85
+
86
+ for svc in (services or []):
87
+ if isinstance(svc, ServiceNode):
88
+ self._services.append({
89
+ 'id': svc.service_id,
90
+ 'handler': svc.handler,
91
+ })
92
+
93
+ self._screens = screens
94
+
95
+ # Process all screens
96
+ for screen in screens:
97
+ self._process_screen_children(screen.children)
98
+
99
+ # Build app code
100
+ py = PyBuilder()
101
+ self._build_app_class(py, screens, title)
102
+ app_code = py.build()
103
+
104
+ # Determine app class name
105
+ app_class = py_id(title.replace(' ', '')) + "App"
106
+
107
+ # Determine filename
108
+ filename = py_id(title.lower().replace(' ', '_')) + ".py"
109
+
110
+ # Extra imports
111
+ extra_imports = '\n'.join(self._extra_imports)
112
+
113
+ return APP_TEMPLATE.format(
114
+ title=title,
115
+ filename=filename,
116
+ extra_imports=extra_imports,
117
+ app_code=app_code,
118
+ app_class=app_class,
119
+ )
120
+
121
+ # ------------------------------------------------------------------
122
+ # Process AST children (collect metadata)
123
+ # ------------------------------------------------------------------
124
+
125
+ def _process_screen_children(self, children: List[QuantumNode]):
126
+ """Walk screen children to collect state vars, functions, etc."""
127
+ for child in children:
128
+ if isinstance(child, SetNode):
129
+ self._state_vars.append({
130
+ 'name': child.name,
131
+ 'value': child.value,
132
+ 'type': getattr(child, 'type', 'string'),
133
+ })
134
+ elif isinstance(child, FunctionNode):
135
+ self._functions.append(child)
136
+ elif isinstance(child, KeybindingNode):
137
+ self._keybindings.append({
138
+ 'key': child.key,
139
+ 'action': child.action,
140
+ 'description': child.description or child.action,
141
+ })
142
+ elif isinstance(child, TimerNode_Terminal):
143
+ self._timers.append({
144
+ 'id': child.timer_id,
145
+ 'interval': child.interval,
146
+ 'action': child.action,
147
+ })
148
+ elif isinstance(child, ServiceNode):
149
+ self._services.append({
150
+ 'id': child.service_id,
151
+ 'handler': child.handler,
152
+ })
153
+ elif isinstance(child, CssNode):
154
+ self._css_blocks.append(child.content)
155
+ elif isinstance(child, QueryNode):
156
+ self._queries.append({
157
+ 'name': child.name,
158
+ 'datasource': getattr(child, 'datasource', ''),
159
+ 'sql': getattr(child, 'sql', ''),
160
+ })
161
+ if 'import sqlite3' not in self._extra_imports:
162
+ self._extra_imports.append('import sqlite3')
163
+ elif hasattr(child, 'children'):
164
+ self._process_screen_children(child.children)
165
+
166
+ # ------------------------------------------------------------------
167
+ # Build the App class
168
+ # ------------------------------------------------------------------
169
+
170
+ def _build_app_class(self, py: PyBuilder, screens: List[ScreenNode], title: str):
171
+ """Build the main Textual App class."""
172
+ app_class = py_id(title.replace(' ', '')) + "App"
173
+
174
+ py.blank()
175
+ py.class_def(app_class, 'App')
176
+
177
+ # TITLE
178
+ py.assign('TITLE', py_string(title))
179
+
180
+ # CSS
181
+ if self._css_blocks:
182
+ css_combined = '\n'.join(self._css_blocks)
183
+ py.assign('CSS', f'"""{css_combined}"""')
184
+ py.blank()
185
+
186
+ # BINDINGS
187
+ if self._keybindings:
188
+ bindings_list = []
189
+ for kb in self._keybindings:
190
+ desc = kb.get('description', kb['action'])
191
+ bindings_list.append(
192
+ f'Binding({py_string(kb["key"])}, {py_string(kb["action"])}, {py_string(desc)})'
193
+ )
194
+ py.assign('BINDINGS', f'[\n ' + ',\n '.join(bindings_list) + ',\n ]')
195
+ py.blank()
196
+
197
+ # Reactive state vars
198
+ for var in self._state_vars:
199
+ default = self._get_default_value(var)
200
+ py.assign(var['name'], f'reactive({default})')
201
+ if self._state_vars:
202
+ py.blank()
203
+
204
+ # compose() method
205
+ self._emit_compose(py, screens)
206
+
207
+ # on_mount() for timers
208
+ if self._timers:
209
+ py.blank()
210
+ py.method_def('on_mount', 'self')
211
+ py.docstring('Set up timers on app mount.')
212
+ for timer in self._timers:
213
+ action = timer['action']
214
+ interval = timer['interval']
215
+ py.line(f'self.set_interval({interval}, '
216
+ f'self.action_{py_id(split_call(action)[0])})')
217
+ py.end_block()
218
+
219
+ # Event handlers
220
+ self._emit_event_handlers(py)
221
+
222
+ # User-defined functions
223
+ self._emit_functions(py)
224
+
225
+ # Query functions
226
+ self._emit_queries(py)
227
+
228
+ # Action methods for keybindings/timers
229
+ self._emit_action_methods(py)
230
+
231
+ py.end_block() # Close class
232
+
233
+ # ------------------------------------------------------------------
234
+ # compose() - Widget tree
235
+ # ------------------------------------------------------------------
236
+
237
+ def _emit_compose(self, py: PyBuilder, screens: List[ScreenNode]):
238
+ """Emit the compose() method that yields the widget tree."""
239
+ py.line('def compose(self) -> ComposeResult:')
240
+ py.indent()
241
+ py.docstring('Build the widget tree.')
242
+
243
+ # Use first screen as main compose
244
+ if screens:
245
+ screen = screens[0]
246
+ for child in screen.children:
247
+ self._emit_widget(py, child)
248
+
249
+ py.end_block()
250
+
251
+ def _emit_widget(self, py: PyBuilder, node: QuantumNode):
252
+ """Emit a yield statement for a widget node."""
253
+
254
+ if isinstance(node, HeaderNode_Terminal):
255
+ if node.show_clock:
256
+ py.line('yield Header(show_clock=True)')
257
+ else:
258
+ py.line('yield Header()')
259
+
260
+ elif isinstance(node, FooterNode):
261
+ py.line('yield Footer()')
262
+
263
+ elif isinstance(node, TextNode_Terminal):
264
+ content = node.content or ''
265
+ id_part = f', id={py_string(node.text_id)}' if node.text_id else ''
266
+ py.line(f'yield Static({py_string(content)}{id_part})')
267
+
268
+ elif isinstance(node, ProgressNode):
269
+ parts = []
270
+ parts.append(f'total={node.total}')
271
+ if node.progress_id:
272
+ parts.append(f'id={py_string(node.progress_id)}')
273
+ py.line(f'yield ProgressBar({", ".join(parts)})')
274
+
275
+ elif isinstance(node, TerminalInputNode):
276
+ parts = []
277
+ if node.placeholder:
278
+ parts.append(f'placeholder={py_string(node.placeholder)}')
279
+ if node.password:
280
+ parts.append('password=True')
281
+ if node.input_id:
282
+ parts.append(f'id={py_string(node.input_id)}')
283
+ py.line(f'yield Input({", ".join(parts)})')
284
+
285
+ elif isinstance(node, ButtonNode):
286
+ parts = [py_string(node.label)]
287
+ if node.variant and node.variant != 'default':
288
+ parts.append(f'variant={py_string(node.variant)}')
289
+ if node.button_id:
290
+ parts.append(f'id={py_string(node.button_id)}')
291
+ py.line(f'yield Button({", ".join(parts)})')
292
+
293
+ elif isinstance(node, LogNode_Terminal):
294
+ parts = []
295
+ if not node.auto_scroll:
296
+ parts.append('auto_scroll=False')
297
+ if not node.markup:
298
+ parts.append('markup=False')
299
+ if node.max_lines:
300
+ parts.append(f'max_lines={node.max_lines}')
301
+ if node.log_id:
302
+ parts.append(f'id={py_string(node.log_id)}')
303
+ py.line(f'yield RichLog({", ".join(parts)})')
304
+
305
+ elif isinstance(node, TreeNode):
306
+ label = node.label or 'Root'
307
+ parts = [py_string(label)]
308
+ if node.tree_id:
309
+ parts.append(f'id={py_string(node.tree_id)}')
310
+ py.line(f'yield Tree({", ".join(parts)})')
311
+
312
+ elif isinstance(node, MenuNode):
313
+ parts = []
314
+ if node.menu_id:
315
+ parts.append(f'id={py_string(node.menu_id)}')
316
+ py.line(f'yield OptionList({", ".join(parts)})')
317
+
318
+ elif isinstance(node, StatusNode):
319
+ content = node.content or ''
320
+ id_part = f', id={py_string(node.status_id)}' if node.status_id else ''
321
+ py.line(f'yield Static({py_string(content)}{id_part})')
322
+
323
+ elif isinstance(node, TableNode):
324
+ self._emit_table_widget(py, node)
325
+
326
+ elif isinstance(node, PanelNode):
327
+ self._emit_container(py, node, 'Vertical')
328
+
329
+ elif isinstance(node, LayoutNode):
330
+ container = 'Horizontal' if node.direction == 'horizontal' else 'Vertical'
331
+ self._emit_container(py, node, container)
332
+
333
+ elif isinstance(node, TabsNode):
334
+ self._emit_tabs_widget(py, node)
335
+
336
+ # Skip non-widget nodes (SetNode, FunctionNode, etc.)
337
+
338
+ def _emit_container(self, py: PyBuilder, node, container_type: str):
339
+ """Emit a container widget (Vertical/Horizontal) with children."""
340
+ node_id = getattr(node, 'panel_id', None) or getattr(node, 'layout_id', None)
341
+ id_part = f'id={py_string(node_id)}' if node_id else ''
342
+ py.line(f'with {container_type}({id_part}):')
343
+ py.indent()
344
+
345
+ has_children = False
346
+ for child in node.children:
347
+ if self._is_widget_node(child):
348
+ self._emit_widget(py, child)
349
+ has_children = True
350
+
351
+ if not has_children:
352
+ py.pass_stmt()
353
+ py.end_block()
354
+
355
+ def _emit_table_widget(self, py: PyBuilder, node: TableNode):
356
+ """Emit DataTable setup."""
357
+ id_part = f'id={py_string(node.table_id)}' if node.table_id else ''
358
+ if node.zebra:
359
+ if id_part:
360
+ id_part += ', '
361
+ id_part += 'zebra_stripes=True'
362
+ py.line(f'yield DataTable({id_part})')
363
+
364
+ def _emit_tabs_widget(self, py: PyBuilder, node: TabsNode):
365
+ """Emit TabbedContent with TabPanes."""
366
+ id_part = f'id={py_string(node.tabs_id)}' if node.tabs_id else ''
367
+ py.line(f'with TabbedContent({id_part}):')
368
+ py.indent()
369
+
370
+ for tab in node.tabs:
371
+ tab_id_part = f', id={py_string(tab.tab_id)}' if tab.tab_id else ''
372
+ py.line(f'with TabPane({py_string(tab.title)}{tab_id_part}):')
373
+ py.indent()
374
+ has_children = False
375
+ for child in tab.children:
376
+ if self._is_widget_node(child):
377
+ self._emit_widget(py, child)
378
+ has_children = True
379
+ if not has_children:
380
+ py.pass_stmt()
381
+ py.end_block()
382
+
383
+ if not node.tabs:
384
+ py.pass_stmt()
385
+ py.end_block()
386
+
387
+ def _is_widget_node(self, node: QuantumNode) -> bool:
388
+ """Check if a node produces a widget."""
389
+ return isinstance(node, (
390
+ HeaderNode_Terminal, FooterNode, TextNode_Terminal, ProgressNode,
391
+ TerminalInputNode, ButtonNode, LogNode_Terminal, TreeNode,
392
+ MenuNode, StatusNode, TableNode, PanelNode, LayoutNode,
393
+ TabsNode,
394
+ ))
395
+
396
+ # ------------------------------------------------------------------
397
+ # Event handlers
398
+ # ------------------------------------------------------------------
399
+
400
+ def _emit_event_handlers(self, py: PyBuilder):
401
+ """Emit Textual event handler methods (on_button_pressed, on_input_submitted)."""
402
+ # Collect button on_click handlers
403
+ button_handlers = self._collect_handlers_of_type(ButtonNode, 'on_click')
404
+ input_handlers = self._collect_handlers_of_type(TerminalInputNode, 'on_submit')
405
+
406
+ if button_handlers:
407
+ py.blank()
408
+ py.method_def('on_button_pressed', 'self, event: Button.Pressed')
409
+ py.docstring('Handle button press events.')
410
+ first = True
411
+ for btn_id, action in button_handlers:
412
+ if btn_id:
413
+ cond = f'event.button.id == {py_string(btn_id)}'
414
+ if first:
415
+ py.if_block(cond)
416
+ first = False
417
+ else:
418
+ py.elif_block(cond)
419
+ py.line(f'self.action_{py_id(split_call(action)[0])}()')
420
+ if not first:
421
+ py.end_block()
422
+ py.end_block()
423
+
424
+ if input_handlers:
425
+ py.blank()
426
+ py.method_def('on_input_submitted', 'self, event: Input.Submitted')
427
+ py.docstring('Handle input submit events.')
428
+ first = True
429
+ for input_id, action in input_handlers:
430
+ if input_id:
431
+ cond = f'event.input.id == {py_string(input_id)}'
432
+ if first:
433
+ py.if_block(cond)
434
+ first = False
435
+ else:
436
+ py.elif_block(cond)
437
+ py.line(f'self.action_{py_id(split_call(action)[0])}()')
438
+ if not first:
439
+ py.end_block()
440
+ py.end_block()
441
+
442
+ def _collect_handlers_of_type(self, node_type, attr_name: str) -> List[tuple]:
443
+ """Walk all screens to collect (id, handler) pairs for a widget type."""
444
+ handlers = []
445
+ for screen in self._screens:
446
+ self._walk_for_handlers(screen.children, node_type, attr_name, handlers)
447
+ return handlers
448
+
449
+ def _walk_for_handlers(self, children, node_type, attr_name, handlers):
450
+ for child in children:
451
+ if isinstance(child, node_type):
452
+ handler = getattr(child, attr_name, None)
453
+ node_id = getattr(child, 'button_id', None) or getattr(child, 'input_id', None)
454
+ if handler:
455
+ handlers.append((node_id, handler))
456
+ if hasattr(child, 'children'):
457
+ self._walk_for_handlers(child.children, node_type, attr_name, handlers)
458
+ if isinstance(child, TabsNode):
459
+ for tab in child.tabs:
460
+ self._walk_for_handlers(tab.children, node_type, attr_name, handlers)
461
+ if isinstance(child, TableNode):
462
+ pass # No children to walk
463
+
464
+ # ------------------------------------------------------------------
465
+ # User functions
466
+ # ------------------------------------------------------------------
467
+
468
+ def _emit_functions(self, py: PyBuilder):
469
+ """Emit user-defined q:function nodes as app methods."""
470
+ for func in self._functions:
471
+ py.blank()
472
+ params = 'self'
473
+ if hasattr(func, 'params') and func.params:
474
+ param_names = ', '.join(p.name for p in func.params)
475
+ params = f'self, {param_names}'
476
+
477
+ py.method_def(py_id(func.name), params)
478
+
479
+ if hasattr(func, 'body') and func.body:
480
+ for stmt in func.body:
481
+ if isinstance(stmt, RawCodeNode_Terminal):
482
+ py.line(stmt.code)
483
+ elif isinstance(stmt, SetNode):
484
+ py.line(f'self.{py_id(stmt.name)} = {self._format_value(stmt.value, getattr(stmt, "type", "string"))}')
485
+ else:
486
+ py.comment(f'TODO: {type(stmt).__name__}')
487
+ else:
488
+ py.pass_stmt()
489
+
490
+ py.end_block()
491
+
492
+ # ------------------------------------------------------------------
493
+ # Query functions
494
+ # ------------------------------------------------------------------
495
+
496
+ def _emit_queries(self, py: PyBuilder):
497
+ """Emit q:query nodes as database access methods."""
498
+ for query in self._queries:
499
+ py.blank()
500
+ py.method_def(f'query_{py_id(query["name"])}', 'self')
501
+ py.docstring(f'Execute query: {query["name"]}')
502
+ ds = query.get('datasource', 'app.db')
503
+ sql = query.get('sql', '').strip()
504
+ py.line(f'conn = sqlite3.connect({py_string(ds)})')
505
+ py.line('cursor = conn.cursor()')
506
+ py.line(f'cursor.execute({py_string(sql)})')
507
+ py.line('columns = [desc[0] for desc in cursor.description] if cursor.description else []')
508
+ py.line('rows = [dict(zip(columns, row)) for row in cursor.fetchall()]')
509
+ py.line('conn.close()')
510
+ py.return_stmt('rows')
511
+ py.end_block()
512
+
513
+ # ------------------------------------------------------------------
514
+ # Action methods for keybindings & timers
515
+ # ------------------------------------------------------------------
516
+
517
+ def _emit_action_methods(self, py: PyBuilder):
518
+ """Emit action_ methods for keybindings and timers."""
519
+ emitted = set()
520
+
521
+ # Built-in actions don't need methods
522
+ builtin_actions = {'quit', 'toggle_dark', 'bell', 'screenshot'}
523
+
524
+ all_actions = set()
525
+ for kb in self._keybindings:
526
+ all_actions.add(kb['action'])
527
+ for timer in self._timers:
528
+ all_actions.add(timer['action'])
529
+
530
+ # Collect actions from button/input handlers
531
+ button_handlers = self._collect_handlers_of_type(ButtonNode, 'on_click')
532
+ input_handlers = self._collect_handlers_of_type(TerminalInputNode, 'on_submit')
533
+ for _, action in button_handlers:
534
+ all_actions.add(action)
535
+ for _, action in input_handlers:
536
+ all_actions.add(action)
537
+
538
+ # Check which actions already have a user function
539
+ user_func_names = {f.name for f in self._functions}
540
+
541
+ for action in sorted(all_actions):
542
+ # `on-click="salvar()"` e a mesma acao que `on-click="salvar"`.
543
+ #
544
+ # Antes o texto CRU virava nome de metodo: `py_id("salvar()")`
545
+ # devolvia `salvar()` e o gerador escrevia
546
+ # `def action_salvar()(self):` — SyntaxError, o app inteiro
547
+ # deixava de importar por causa de um botao. E a comparacao
548
+ # `action in user_func_names` nunca casava com parenteses, entao
549
+ # nem a q:function certa era chamada.
550
+ nome, args = split_call(action)
551
+ if nome in builtin_actions:
552
+ continue
553
+ if nome in emitted:
554
+ continue
555
+ emitted.add(nome)
556
+
557
+ py.blank()
558
+ safe_action = py_id(nome)
559
+
560
+ # If user defined a function with this name, the action calls it
561
+ if nome in user_func_names:
562
+ py.method_def(f'action_{safe_action}', 'self')
563
+ py.docstring(f'Action: {action}')
564
+ py.line(f'self.{safe_action}({args or ""})')
565
+ py.end_block()
566
+ else:
567
+ py.method_def(f'action_{safe_action}', 'self')
568
+ py.docstring(f'Action: {action}')
569
+ py.line(f'self.notify({py_string(f"Action: {nome}")})')
570
+ py.end_block()
571
+
572
+ # ------------------------------------------------------------------
573
+ # Helpers
574
+ # ------------------------------------------------------------------
575
+
576
+ def _get_default_value(self, var: Dict) -> str:
577
+ """Convert a q:set variable to its Python reactive default."""
578
+ val = var.get('value', '')
579
+ vtype = var.get('type', 'string')
580
+ return self._format_value(val, vtype)
581
+
582
+ def _format_value(self, val: str, vtype: str) -> str:
583
+ """Format a value string according to its type for Python code."""
584
+ if vtype == 'integer':
585
+ try:
586
+ return str(int(val)) if val else '0'
587
+ except (ValueError, TypeError):
588
+ return '0'
589
+ elif vtype == 'number' or vtype == 'float':
590
+ try:
591
+ return str(float(val)) if val else '0.0'
592
+ except (ValueError, TypeError):
593
+ return '0.0'
594
+ elif vtype == 'boolean':
595
+ if isinstance(val, str):
596
+ return 'True' if val.lower() in ('true', '1', 'yes') else 'False'
597
+ return py_bool(bool(val))
598
+ elif vtype == 'array':
599
+ if val and val.strip().startswith('['):
600
+ return val
601
+ return '[]'
602
+ elif vtype == 'object':
603
+ if val and val.strip().startswith('{'):
604
+ return val
605
+ return '{}'
606
+ else:
607
+ return py_string(val) if val else py_string('')