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,470 @@
1
+ """
2
+ Service Container - Dependency injection for runtime services
3
+
4
+ Centralizes initialization and access to all runtime services
5
+ (database, email, LLM, etc.) instead of having them scattered
6
+ across ComponentRuntime.__init__.
7
+ """
8
+
9
+ import copy
10
+ from typing import Any, Dict, Optional, TYPE_CHECKING
11
+ import logging
12
+ import os
13
+
14
+ from quantum.core.config_env import ConfigEnvError, expand_env
15
+
16
+ if TYPE_CHECKING:
17
+ from quantum.runtime.database_service import DatabaseService
18
+ from quantum.runtime.llm_service import LLMService
19
+ from quantum.runtime.knowledge_service import KnowledgeService
20
+ from quantum.runtime.message_queue_service import MessageQueueService
21
+ from quantum.runtime.job_executor import JobExecutor
22
+ from quantum.runtime.file_upload_service import FileUploadService
23
+ from quantum.runtime.email_service import EmailService
24
+ from quantum.runtime.function_registry import FunctionRegistry
25
+ from quantum.core.features.invocation.src.runtime import InvocationService
26
+ from quantum.core.features.data_import.src.runtime import DataImportService
27
+ from quantum.core.features.logging.src import LoggingService
28
+ from quantum.core.features.dump.src import DumpService
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _DEFAULT_CONFIG_FILE = 'quantum.config.yaml'
33
+
34
+
35
+ _config_cache: Dict[str, Any] = {}
36
+
37
+
38
+ def load_project_config(config_path: str = _DEFAULT_CONFIG_FILE) -> Dict[str, Any]:
39
+ """Read quantum.config.yaml from the working directory, if it is there.
40
+
41
+ Missing file: empty configuration, no complaint — a .q file that uses no
42
+ datasource needs none. Unreadable or invalid file: empty configuration
43
+ and a warning, because silence there is how a typo in the YAML turns
44
+ into "datasource not declared" three layers down.
45
+
46
+ Cached by (path, mtime, size). Every ComponentRuntime() built without a
47
+ config re-read and re-parsed the file — 200 runtimes meant 200 disk reads
48
+ — and the suite builds thousands. Keying on mtime keeps the property that
49
+ matters: edit the file and the next runtime sees the change.
50
+
51
+ A copy is returned, not the cached dict: services mutate their config,
52
+ and handing out the same object would let one runtime's changes leak into
53
+ the next.
54
+ """
55
+ import os
56
+ try:
57
+ stat = os.stat(config_path)
58
+ except OSError:
59
+ return {}
60
+
61
+ key = (os.path.abspath(config_path), stat.st_mtime_ns, stat.st_size)
62
+ cached = _config_cache.get(key)
63
+ if cached is not None:
64
+ return copy.deepcopy(cached)
65
+
66
+ try:
67
+ import yaml
68
+ with open(config_path, 'r', encoding='utf-8') as handle:
69
+ loaded = yaml.safe_load(handle)
70
+ if loaded is None:
71
+ loaded = {}
72
+ if not isinstance(loaded, dict):
73
+ logger.warning("%s does not contain a mapping; ignoring it",
74
+ config_path)
75
+ loaded = {}
76
+ loaded = expand_env(loaded)
77
+ except ConfigEnvError:
78
+ raise
79
+ except Exception as exc:
80
+ logger.warning("could not read %s: %s", config_path, exc)
81
+ return {}
82
+
83
+ _config_cache.clear() # uma entrada basta; o arquivo e um so
84
+ _config_cache[key] = loaded
85
+ return copy.deepcopy(loaded)
86
+
87
+
88
+ class ServiceContainer:
89
+ """
90
+ Container for all runtime services.
91
+
92
+ Provides lazy initialization and centralized access to services.
93
+ Services are initialized on first access to avoid unnecessary overhead.
94
+
95
+ Example:
96
+ services = ServiceContainer(config)
97
+ db = services.database # Initialized on first access
98
+ llm = services.llm
99
+ """
100
+
101
+ def __init__(self, config: Dict[str, Any] = None):
102
+ """
103
+ Initialize service container.
104
+
105
+ Args:
106
+ config: Configuration dictionary with service-specific settings.
107
+ Omit it (None) to read quantum.config.yaml from the working
108
+ directory; pass {} for a deliberately empty configuration.
109
+
110
+ `None` used to mean "no configuration", so anything that built a
111
+ runtime without passing one — the .q test plugin, library callers,
112
+ the admin — could not see the `datasources:` declared in
113
+ quantum.config.yaml, and every q:query and q:transaction fell through
114
+ to the optional Admin API at localhost:8000 and failed with a
115
+ connection error. `quantum run` was fixed to load the file; that fix
116
+ did not reach anybody else. Now the default is the same either way,
117
+ and passing {} still means empty.
118
+ """
119
+ self._config = load_project_config() if config is None else config
120
+ self._services: Dict[str, Any] = {}
121
+
122
+ # ==========================================================================
123
+ # Core Services
124
+ # ==========================================================================
125
+
126
+ @property
127
+ def database(self) -> 'DatabaseService':
128
+ """Database service for query execution"""
129
+ if 'database' not in self._services:
130
+ from quantum.runtime.database_service import DatabaseService
131
+ local_ds = self._config.get('datasources', {})
132
+ self._services['database'] = DatabaseService(local_datasources=local_ds)
133
+ logger.debug("Initialized DatabaseService")
134
+ return self._services['database']
135
+
136
+ @property
137
+ def function_registry(self) -> 'FunctionRegistry':
138
+ """Function registry for q:function"""
139
+ if 'function_registry' not in self._services:
140
+ from quantum.runtime.function_registry import FunctionRegistry
141
+ self._services['function_registry'] = FunctionRegistry()
142
+ logger.debug("Initialized FunctionRegistry")
143
+ return self._services['function_registry']
144
+
145
+ # ==========================================================================
146
+ # Invocation & Data Services
147
+ # ==========================================================================
148
+
149
+ @property
150
+ def invocation(self) -> 'InvocationService':
151
+ """Invocation service for q:invoke"""
152
+ if 'invocation' not in self._services:
153
+ from quantum.core.features.invocation.src.runtime import InvocationService
154
+ self._services['invocation'] = InvocationService()
155
+ logger.debug("Initialized InvocationService")
156
+ return self._services['invocation']
157
+
158
+ @property
159
+ def data_import(self) -> 'DataImportService':
160
+ """Data import service for q:data"""
161
+ if 'data_import' not in self._services:
162
+ from quantum.core.features.data_import.src.runtime import DataImportService
163
+ self._services['data_import'] = DataImportService()
164
+ logger.debug("Initialized DataImportService")
165
+ return self._services['data_import']
166
+
167
+ # ==========================================================================
168
+ # AI Services
169
+ # ==========================================================================
170
+
171
+ @property
172
+ def llm(self) -> 'LLMService':
173
+ """Ollama-only LLM service, used by q:knowledge and the RAG path.
174
+
175
+ It used to be constructed with NO arguments, so it fell back to its own
176
+ default of http://localhost:11434 and ignored llm.base_url entirely —
177
+ while multi_llm (q:llm, q:agent, q:team) honoured it. The two halves of
178
+ the AI stack therefore pointed at DIFFERENT Ollama servers.
179
+
180
+ Measured on this machine: config says http://localhost:11434, q:llm
181
+ reached that host and answered, and q:knowledge silently went to
182
+ localhost. In a deployment where only the configured host has models,
183
+ q:llm works and RAG fails, with nothing explaining why.
184
+ """
185
+ if 'llm' not in self._services:
186
+ from quantum.runtime.llm_service import LLMService
187
+ llm_cfg = self._config.get('llm', {}) or {}
188
+ self._services['llm'] = LLMService(
189
+ base_url=self._llm_base_url(),
190
+ default_model=llm_cfg.get('default_model'),
191
+ timeout=llm_cfg.get('timeout', 60),
192
+ )
193
+ logger.debug("Initialized LLMService")
194
+ return self._services['llm']
195
+
196
+ @property
197
+ def knowledge(self) -> 'KnowledgeService':
198
+ """Knowledge service for RAG/ChromaDB"""
199
+ if 'knowledge' not in self._services:
200
+ from quantum.runtime.knowledge_service import KnowledgeService
201
+ self._services['knowledge'] = KnowledgeService(self.llm)
202
+ logger.debug("Initialized KnowledgeService")
203
+ return self._services['knowledge']
204
+
205
+ def _llm_base_url(self):
206
+ """Where the model server is: QUANTUM_LLM_BASE_URL, then llm.base_url, then the default.
207
+
208
+ One answer for every AI tag. They disagreed: q:llm read llm.base_url from
209
+ quantum.config.yaml, while q:agent and q:team built their own service
210
+ with no config and read only the environment. Measured with the repo's
211
+ config (localhost) and QUANTUM_LLM_BASE_URL pointing at another host,
212
+ in one program: q:llm went to the local Ollama and failed with "model
213
+ 'phi3' not found", q:agent went to the other host and answered.
214
+
215
+ The environment wins over the file, so an operator can point a
216
+ deployment at another server without editing the project.
217
+ """
218
+ llm_cfg = self._config.get('llm', {}) or {}
219
+ return os.environ.get('QUANTUM_LLM_BASE_URL') or llm_cfg.get('base_url')
220
+
221
+ @property
222
+ def agent(self):
223
+ """Agent service for q:agent — on this container's model server."""
224
+ if 'agent' not in self._services:
225
+ from quantum.runtime.agent_service import AgentService
226
+ service = AgentService(self.llm)
227
+ # Built by the global get_agent_service() it made its own
228
+ # MultiProviderLLMService with no config; share this container's.
229
+ service._multi_llm_service = self.multi_llm
230
+ self._services['agent'] = service
231
+ logger.debug("Initialized AgentService")
232
+ return self._services['agent']
233
+
234
+ @property
235
+ def multi_agent(self):
236
+ """Multi-agent service for q:team — on the same agent service."""
237
+ if 'multi_agent' not in self._services:
238
+ from quantum.runtime.agent_service import MultiAgentService
239
+ self._services['multi_agent'] = MultiAgentService(self.agent)
240
+ logger.debug("Initialized MultiAgentService")
241
+ return self._services['multi_agent']
242
+
243
+ # ==========================================================================
244
+ # Communication Services
245
+ # ==========================================================================
246
+
247
+ @property
248
+ def message_queue(self) -> 'MessageQueueService':
249
+ """Message queue service for q:message, q:subscribe"""
250
+ if 'message_queue' not in self._services:
251
+ from quantum.runtime.message_queue_service import MessageQueueService
252
+ mq_config = self._config.get('message_queue', {})
253
+ self._services['message_queue'] = MessageQueueService(mq_config)
254
+ logger.debug("Initialized MessageQueueService")
255
+ return self._services['message_queue']
256
+
257
+ @property
258
+ def multi_llm(self) -> 'MultiProviderLLMService':
259
+ """Multi-provider LLM service (Ollama, OpenAI, Anthropic, LM Studio).
260
+
261
+ q:agent already used this; q:llm was stuck on the Ollama-only
262
+ LLMService, which is why 'multi-provider' was only half true.
263
+ """
264
+ if 'multi_llm' not in self._services:
265
+ from quantum.runtime.llm_providers import MultiProviderLLMService
266
+ llm_cfg = self._config.get('llm', {}) or {}
267
+ self._services['multi_llm'] = MultiProviderLLMService(
268
+ default_endpoint=self._llm_base_url(),
269
+ default_model=llm_cfg.get('default_model'),
270
+ timeout=llm_cfg.get('timeout', 60),
271
+ )
272
+ logger.debug("Initialized MultiProviderLLMService")
273
+ return self._services['multi_llm']
274
+
275
+ @property
276
+ def llm_cache(self) -> 'LLMCache':
277
+ """TTL cache for q:llm responses (the `cache` attribute's backing)."""
278
+ if 'llm_cache' not in self._services:
279
+ from quantum.runtime.llm_cache import get_llm_cache
280
+ self._services['llm_cache'] = get_llm_cache()
281
+ return self._services['llm_cache']
282
+
283
+ @property
284
+ def config(self) -> Dict[str, Any]:
285
+ """Raw configuration dict (quantum.config.yaml), for executors that
286
+ need policy settings rather than a service."""
287
+ return self._config
288
+
289
+ @property
290
+ def python_scripting_enabled(self) -> bool:
291
+ """Whether q:python / q:pyclass / q:pyimport may execute.
292
+
293
+ q:python is a full-trust escape hatch: it runs arbitrary Python in
294
+ the server process with no sandbox. That is fine for code the
295
+ application author wrote, and unacceptable for code anything else
296
+ wrote — an LLM-generated or user-submitted template, a component
297
+ from a marketplace, a .q file someone sent you.
298
+
299
+ **Desligado por padrão.** Era ligado, o que fazia dele a última
300
+ falha-aberta de configuração do framework: bastava executar um .q
301
+ que você não escreveu para executar o Python dele. Um default
302
+ precisa ser seguro para quem não leu a documentação — e quem usa
303
+ q:python leu, porque teve de escrever a tag.
304
+
305
+ Ligue com `security.python_scripting: true` no quantum.config.yaml,
306
+ ou QUANTUM_PYTHON_SCRIPTING=1 no ambiente.
307
+ """
308
+ security = self._config.get('security', {}) or {}
309
+ configured = security.get('python_scripting')
310
+ if configured is not None:
311
+ return configured is not False
312
+
313
+ import os
314
+ env = os.environ.get('QUANTUM_PYTHON_SCRIPTING', '').strip().lower()
315
+ return env in ('1', 'true', 'yes', 'on')
316
+
317
+ @property
318
+ def websocket(self) -> 'WebSocketService':
319
+ """WebSocket service for q:websocket, q:send, q:close.
320
+
321
+ The service existed but was never exposed here, so every
322
+ q:websocket tag raised AttributeError on services.websocket.
323
+ """
324
+ if 'websocket' not in self._services:
325
+ from quantum.runtime.websocket_service import get_websocket_service
326
+ self._services['websocket'] = get_websocket_service()
327
+ logger.debug("Initialized WebSocketService")
328
+ return self._services['websocket']
329
+
330
+ @property
331
+ def email(self) -> 'EmailService':
332
+ """Email service for q:mail"""
333
+ if 'email' not in self._services:
334
+ from quantum.runtime.email_service import EmailService
335
+ self._services['email'] = EmailService()
336
+ logger.debug("Initialized EmailService")
337
+ return self._services['email']
338
+
339
+ # ==========================================================================
340
+ # Job & Scheduling Services
341
+ # ==========================================================================
342
+
343
+ @property
344
+ def job_executor(self) -> 'JobExecutor':
345
+ """Job executor for q:schedule, q:thread, q:job"""
346
+ if 'job_executor' not in self._services:
347
+ from quantum.runtime.job_executor import JobExecutor
348
+ job_db_path = self._config.get('job_db_path', 'quantum_jobs.db')
349
+ max_workers = self._config.get('max_thread_workers', 10)
350
+ self._services['job_executor'] = JobExecutor(
351
+ max_thread_workers=max_workers,
352
+ job_db_path=job_db_path
353
+ )
354
+ logger.debug("Initialized JobExecutor")
355
+ return self._services['job_executor']
356
+
357
+ # ==========================================================================
358
+ # File & Upload Services
359
+ # ==========================================================================
360
+
361
+ @property
362
+ def file_upload(self) -> 'FileUploadService':
363
+ """File upload service for q:file"""
364
+ if 'file_upload' not in self._services:
365
+ from quantum.runtime.file_upload_service import FileUploadService
366
+ # Every path q:file touches is confined to this root. Configurable
367
+ # via paths.uploads; ./uploads by default, which is where every
368
+ # destination= in this repo already points.
369
+ root = (self._config.get('paths', {}) or {}).get('uploads', 'uploads')
370
+ self._services['file_upload'] = FileUploadService(root=root)
371
+ logger.debug("Initialized FileUploadService")
372
+ return self._services['file_upload']
373
+
374
+ # ==========================================================================
375
+ # Developer Experience Services
376
+ # ==========================================================================
377
+
378
+ @property
379
+ def logging(self) -> 'LoggingService':
380
+ """Logging service for q:log"""
381
+ if 'logging' not in self._services:
382
+ from quantum.core.features.logging.src import LoggingService
383
+ self._services['logging'] = LoggingService()
384
+ logger.debug("Initialized LoggingService")
385
+ return self._services['logging']
386
+
387
+ @property
388
+ def dump(self) -> 'DumpService':
389
+ """Dump service for q:dump"""
390
+ if 'dump' not in self._services:
391
+ from quantum.core.features.dump.src import DumpService
392
+ self._services['dump'] = DumpService()
393
+ logger.debug("Initialized DumpService")
394
+ return self._services['dump']
395
+
396
+ # ==========================================================================
397
+ # Cache Services
398
+ # ==========================================================================
399
+
400
+ @property
401
+ def expression_cache(self):
402
+ """Expression cache for performance"""
403
+ if 'expression_cache' not in self._services:
404
+ from quantum.runtime.expression_cache import get_expression_cache
405
+ self._services['expression_cache'] = get_expression_cache()
406
+ logger.debug("Initialized ExpressionCache")
407
+ return self._services['expression_cache']
408
+
409
+ # ==========================================================================
410
+ # Utility Methods
411
+ # ==========================================================================
412
+
413
+ def get(self, name: str, default: Any = None) -> Any:
414
+ """
415
+ Get a service by name.
416
+
417
+ Args:
418
+ name: Service name (e.g., 'database', 'llm')
419
+ default: Default value if not found
420
+
421
+ Returns:
422
+ Service instance or default
423
+ """
424
+ # Try property access first
425
+ if hasattr(self, name):
426
+ return getattr(self, name)
427
+ return self._services.get(name, default)
428
+
429
+ def register(self, name: str, service: Any) -> 'ServiceContainer':
430
+ """
431
+ Register a custom service.
432
+
433
+ Args:
434
+ name: Service name
435
+ service: Service instance
436
+
437
+ Returns:
438
+ Self for chaining
439
+ """
440
+ self._services[name] = service
441
+ return self
442
+
443
+ def is_initialized(self, name: str) -> bool:
444
+ """Check if a service has been initialized"""
445
+ return name in self._services
446
+
447
+ @property
448
+ def initialized_services(self) -> list:
449
+ """Get list of initialized service names"""
450
+ return list(self._services.keys())
451
+
452
+ def shutdown(self):
453
+ """
454
+ Shutdown all services that support it.
455
+
456
+ Call this when the runtime is being destroyed.
457
+ """
458
+ for name, service in self._services.items():
459
+ if hasattr(service, 'shutdown'):
460
+ try:
461
+ service.shutdown()
462
+ logger.debug(f"Shutdown {name}")
463
+ except Exception as e:
464
+ logger.warning(f"Error shutting down {name}: {e}")
465
+
466
+ self._services.clear()
467
+
468
+ def __repr__(self) -> str:
469
+ initialized = list(self._services.keys())
470
+ return f"ServiceContainer(initialized={initialized})"
@@ -0,0 +1,163 @@
1
+ """An action's params from a table's schema (M17, UI-10).
2
+
3
+ `<q:action name="criar" table="posts" datasource="db">` validates what it
4
+ receives by the table it writes to: a NOT NULL column without a default is
5
+ required, `CHECK (col IN (...))` is an enum, `VARCHAR(n)` a maxlength, the
6
+ column type the param type, and a foreign key must name an existing row. A
7
+ `q:param` written in the action wins over the one from the schema.
8
+
9
+ The schema is read from the database (quantum/runtime/db_schema.py) the first
10
+ time, and again when the database file changes. A form that posts to such an
11
+ action and has no fields of its own draws one for each param (UI-10).
12
+ """
13
+
14
+ import os
15
+ import re
16
+ import threading
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional, Tuple
19
+
20
+ from quantum.core.ast_nodes import QuantumParam
21
+
22
+ _CACHE: Dict[tuple, Tuple[float, list]] = {}
23
+ _TRAVA = threading.Lock()
24
+
25
+
26
+ class TableParamsError(Exception):
27
+ """The table cannot be read (missing, unsupported driver)."""
28
+
29
+
30
+ def _tipo(coluna) -> str:
31
+ t = (coluna.type or '').upper()
32
+ if 'BOOL' in t:
33
+ return 'boolean'
34
+ if 'INT' in t:
35
+ return 'integer'
36
+ if any(x in t for x in ('REAL', 'FLOA', 'DOUB', 'NUMERIC', 'DECIMAL')):
37
+ return 'decimal'
38
+ if t.startswith('DATE') and 'TIME' not in t:
39
+ return 'date'
40
+ return 'string'
41
+
42
+
43
+ def _datasource(config: dict, nome: str) -> dict:
44
+ fontes = (config or {}).get('datasources') or {}
45
+ if nome not in fontes:
46
+ raise TableParamsError(f'datasource {nome!r} is not declared in quantum.config.yaml')
47
+ return fontes[nome]
48
+
49
+
50
+ def _connect(config: dict, nome: str):
51
+ from quantum.runtime.db_schema import SchemaError, connect_readonly
52
+ try:
53
+ return connect_readonly(_datasource(config, nome), Path.cwd())
54
+ except SchemaError as exc:
55
+ raise TableParamsError(f'datasource {nome!r}: {exc}') from exc
56
+
57
+
58
+ def _read_columns(config: dict, nome: str, tabela: str):
59
+ from quantum.runtime.db_schema import read_schema
60
+ conexao = _connect(config, nome)
61
+ try:
62
+ esquema = read_schema(conexao)
63
+ finally:
64
+ conexao.close()
65
+ if tabela not in esquema:
66
+ raise TableParamsError(f'table {tabela!r} does not exist in datasource {nome!r} '
67
+ f'(tables: {", ".join(esquema) or "none"})')
68
+ return list(esquema[tabela].columns.values())
69
+
70
+
71
+ def _mtime(config: dict, nome: str) -> float:
72
+ database = str(_datasource(config, nome).get('database') or '')
73
+ try:
74
+ return os.path.getmtime(Path(database) if Path(database).is_absolute() else Path.cwd() / database)
75
+ except OSError:
76
+ return 0.0
77
+
78
+
79
+ def columns_of(config: dict, nome: str, tabela: str) -> list:
80
+ chave = (str(Path.cwd()), nome, tabela)
81
+ mtime = _mtime(config, nome)
82
+ with _TRAVA:
83
+ guardado = _CACHE.get(chave)
84
+ if guardado and guardado[0] == mtime:
85
+ return guardado[1]
86
+ colunas = _read_columns(config, nome, tabela)
87
+ with _TRAVA:
88
+ _CACHE[chave] = (mtime, colunas)
89
+ return colunas
90
+
91
+
92
+ def param_from_column(coluna) -> QuantumParam:
93
+ tipo = _tipo(coluna)
94
+ param = QuantumParam(
95
+ name=coluna.name, type=tipo,
96
+ required=bool(coluna.not_null and coluna.default is None and tipo != 'boolean'),
97
+ enum=','.join(coluna.options) if coluna.options else None,
98
+ )
99
+ tamanho = re.search(r'\((\d+)\)', coluna.type or '')
100
+ if tamanho and tipo == 'string':
101
+ param.maxlength = int(tamanho.group(1))
102
+ if tipo == 'boolean':
103
+ param.default = 'false'
104
+ param.references = coluna.references # (table, column) of a foreign key
105
+ # A column that takes NULL: left blank, the action gets None (and writes NULL).
106
+ param.nullable = not coluna.not_null
107
+ return param
108
+
109
+
110
+ def params_of(action, config: dict) -> List[QuantumParam]:
111
+ """The params an action validates: its own q:params, plus its table's (UI-10)."""
112
+ proprios = list(getattr(action, 'params', None) or [])
113
+ tabela = getattr(action, 'table', None)
114
+ if not tabela:
115
+ return proprios
116
+ escritos = {p.name for p in proprios}
117
+ permitidas = getattr(action, 'table_columns', None)
118
+ do_esquema = []
119
+ for coluna in columns_of(config, action.table_datasource, tabela):
120
+ if coluna.primary_key or coluna.name in escritos:
121
+ continue
122
+ if permitidas is not None and coluna.name not in permitidas:
123
+ continue
124
+ do_esquema.append(param_from_column(coluna))
125
+ if permitidas is not None:
126
+ faltando = [c for c in permitidas if c not in {p.name for p in do_esquema} | escritos]
127
+ if faltando:
128
+ raise TableParamsError(f'<q:action name="{action.name}" columns=...>: table {tabela!r} has no '
129
+ f'column {", ".join(faltando)}')
130
+ ordem = {nome: i for i, nome in enumerate(permitidas)}
131
+ do_esquema.sort(key=lambda p: ordem.get(p.name, len(ordem)))
132
+ return do_esquema + proprios
133
+
134
+
135
+ def reference_options(config: dict, datasource: str, referencia: Tuple[str, str]) -> List[Tuple[str, str]]:
136
+ """(value, label) of the rows a foreign key may point to — label is the first text column."""
137
+ tabela, coluna = referencia
138
+ colunas = columns_of(config, datasource, tabela)
139
+ preferidas = ('nome', 'name', 'titulo', 'title', 'label', 'descricao', 'description')
140
+ textos = [c.name for c in colunas if _tipo(c) == 'string']
141
+ rotulo = next((p for p in preferidas if p in textos), textos[0] if textos else coluna)
142
+ conexao = _connect(config, datasource)
143
+ try:
144
+ linhas = conexao.execute(
145
+ f'SELECT "{coluna}", "{rotulo}" FROM "{tabela}" ORDER BY "{rotulo}" LIMIT 1000').fetchall()
146
+ finally:
147
+ conexao.close()
148
+ return [(str(v), str(r)) for v, r in linhas]
149
+
150
+
151
+ def reference_exists(config: dict, datasource: str, referencia: Tuple[str, str], valor) -> bool:
152
+ tabela, coluna = referencia
153
+ conexao = _connect(config, datasource)
154
+ try:
155
+ return conexao.execute(f'SELECT 1 FROM "{tabela}" WHERE "{coluna}" = ? LIMIT 1', (valor,)).fetchone() is not None
156
+ finally:
157
+ conexao.close()
158
+
159
+
160
+ def humanize(nome: str) -> str:
161
+ """'autor_id' -> 'Autor'; 'data_publicacao' -> 'Data publicacao'."""
162
+ base = re.sub(r'_id$', '', nome).replace('_', ' ').strip()
163
+ return base[:1].upper() + base[1:] if base else nome