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,178 @@
1
+ """The /_dev panel (M12, DEV-1): what the last requests did, while developing.
2
+
3
+ With `server.debug: true`, the server records each request — the component,
4
+ the action it ran, the queries (SQL, parameters, time, rows), the variables of
5
+ each scope, the redirect and the flash — and `/_dev` shows the last ones. With
6
+ `debug: false` nothing is recorded and `/_dev` does not exist.
7
+
8
+ Recording goes through a context variable, so a query executed deep in the
9
+ runtime finds the request it belongs to without anything being passed down,
10
+ and two requests in two threads never mix.
11
+ """
12
+
13
+ import contextvars
14
+ import html
15
+ import threading
16
+ import time
17
+ from collections import deque
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ _ATUAL: contextvars.ContextVar = contextvars.ContextVar('quantum_dev_request', default=None)
21
+
22
+ # A value longer than this is cut in the panel (a query of 10k rows, a blob).
23
+ _MAX_TEXTO = 300
24
+
25
+
26
+ def _curto(valor: Any) -> str:
27
+ texto = repr(valor) if not isinstance(valor, str) else valor
28
+ return texto if len(texto) <= _MAX_TEXTO else texto[:_MAX_TEXTO] + f'… ({len(texto)} chars)'
29
+
30
+
31
+ def _escopo(variaveis: Dict[str, Any]) -> Dict[str, str]:
32
+ return {nome: _curto(valor) for nome, valor in sorted((variaveis or {}).items())
33
+ if not str(nome).startswith('_')}
34
+
35
+
36
+ # -- what the runtime reports ------------------------------------------------
37
+
38
+ def note(**campos) -> None:
39
+ """Add facts to the request being recorded (no-op when nothing records)."""
40
+ registro = _ATUAL.get()
41
+ if registro is not None:
42
+ registro.update(campos)
43
+
44
+
45
+ def note_query(datasource: str, sql: str, params: Optional[Dict[str, Any]], ms: float,
46
+ rows: Optional[int], error: Optional[str] = None) -> None:
47
+ registro = _ATUAL.get()
48
+ if registro is not None:
49
+ registro['queries'].append({
50
+ 'datasource': datasource, 'sql': ' '.join(sql.split()),
51
+ 'params': {k: _curto(v) for k, v in (params or {}).items()},
52
+ 'ms': round(ms, 2), 'rows': rows, 'error': error,
53
+ })
54
+
55
+
56
+ def note_scopes(execution_context, label: str = 'page') -> None:
57
+ """The variables the page — or the action (`label='action'`) — ended with,
58
+ and the session and application scopes as they were then."""
59
+ registro = _ATUAL.get()
60
+ if registro is None or execution_context is None:
61
+ return
62
+ proprias = dict(getattr(execution_context, 'component_vars', {}) or {})
63
+ proprias.update(getattr(execution_context, 'local_vars', {}) or {})
64
+ escopos = registro['scopes'] or {}
65
+ escopos[label] = _escopo({k: v for k, v in proprias.items() if '.' not in str(k)})
66
+ escopos['session'] = _escopo(getattr(execution_context, 'session_vars', {}))
67
+ escopos['application'] = _escopo(getattr(execution_context, 'application_vars', {}))
68
+ # action, then page, then the scopes that outlive them
69
+ ordem = ['action', 'page', 'session', 'application']
70
+ registro['scopes'] = {k: escopos[k] for k in ordem if k in escopos}
71
+
72
+
73
+ # -- the recorder ------------------------------------------------------------
74
+
75
+ class DevPanel:
76
+ """The last requests of this server process, newest first."""
77
+
78
+ def __init__(self, keep: int = 30):
79
+ self.requests: deque = deque(maxlen=keep)
80
+ self._numero = 0
81
+ self._trava = threading.Lock()
82
+
83
+ def start(self, method: str, path: str):
84
+ with self._trava:
85
+ self._numero += 1
86
+ numero = self._numero
87
+ registro = {'n': numero, 'method': method, 'path': path, 'component': None,
88
+ 'action': None, 'queries': [], 'scopes': None, 'status': None,
89
+ 'redirect': None, 'flash': None, 'error': None, 't0': time.perf_counter()}
90
+ return _ATUAL.set(registro)
91
+
92
+ def finish(self, token, status: int, location: Optional[str], flash: Optional[dict]) -> None:
93
+ registro = _ATUAL.get()
94
+ _ATUAL.reset(token)
95
+ if registro is None:
96
+ return
97
+ registro['status'] = status
98
+ registro['redirect'] = location
99
+ registro['flash'] = flash
100
+ registro['ms'] = round((time.perf_counter() - registro.pop('t0')) * 1000, 1)
101
+ with self._trava:
102
+ self.requests.appendleft(registro)
103
+
104
+ def get(self, numero: int) -> Optional[dict]:
105
+ return next((r for r in list(self.requests) if r['n'] == numero), None)
106
+
107
+ # -- the page ------------------------------------------------------------
108
+
109
+ def render(self, numero: Optional[int] = None) -> str:
110
+ requisicoes = list(self.requests)
111
+ escolhida = self.get(numero) if numero is not None else (requisicoes[0] if requisicoes else None)
112
+ e = html.escape
113
+ linhas = []
114
+ for r in requisicoes:
115
+ atual = escolhida is not None and r['n'] == escolhida['n']
116
+ linhas.append(
117
+ f'<tr class="{"atual" if atual else ""}"><td><a href="/_dev/{r["n"]}">#{r["n"]}</a></td>'
118
+ f'<td>{e(r["method"])}</td><td>{e(r["path"])}</td><td>{r["status"]}</td>'
119
+ f'<td>{e(r["action"] or "")}</td><td>{len(r["queries"])}</td><td>{r["ms"]} ms</td></tr>')
120
+ detalhe = self._detalhe(escolhida) if escolhida else '<p>No requests yet. Open a page of the app.</p>'
121
+ return f'''<!DOCTYPE html>
122
+ <html lang="en"><head><meta charset="UTF-8"><title>Quantum /_dev</title>
123
+ <meta name="viewport" content="width=device-width, initial-scale=1">
124
+ <style>
125
+ body {{ font: 14px/1.45 system-ui, sans-serif; margin: 0; padding: 16px; color: #1f2328; background: #f6f8fa; }}
126
+ h1 {{ font-size: 18px; margin: 0 0 4px; }} h2 {{ font-size: 15px; margin: 20px 0 6px; }}
127
+ .nota {{ color: #57606a; margin: 0 0 12px; }}
128
+ table {{ border-collapse: collapse; width: 100%; background: #fff; }}
129
+ td, th {{ border: 1px solid #d0d7de; padding: 4px 8px; text-align: left; vertical-align: top; }}
130
+ tr.atual td {{ background: #ddf4ff; }}
131
+ code, pre {{ font: 12px/1.4 ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; margin: 0; }}
132
+ th code {{ white-space: nowrap; word-break: normal; }}
133
+ .erro {{ color: #cf222e; }}
134
+ .caixa {{ overflow-x: auto; }}
135
+ </style></head><body>
136
+ <h1>Quantum /_dev</h1>
137
+ <p class="nota">The last {len(requisicoes)} requests of this server process. Shown because
138
+ <code>server.debug</code> is true; with <code>debug: false</code> this page does not exist.</p>
139
+ <div class="caixa"><table><tr><th>#</th><th>Method</th><th>Path</th><th>Status</th><th>Action</th>
140
+ <th>Queries</th><th>Time</th></tr>{"".join(linhas)}</table></div>
141
+ {detalhe}
142
+ </body></html>'''
143
+
144
+ def _detalhe(self, r: dict) -> str:
145
+ e = html.escape
146
+ partes = [f'<h2>#{r["n"]} {e(r["method"])} {e(r["path"])} → {r["status"]} ({r["ms"]} ms)</h2>',
147
+ '<div class="caixa"><table>',
148
+ f'<tr><th>Component</th><td><code>{e(r["component"] or "—")}</code></td></tr>',
149
+ f'<tr><th>Action</th><td>{e(r["action"] or "—")}</td></tr>',
150
+ f'<tr><th>Redirect</th><td>{e(r["redirect"] or "—")}</td></tr>']
151
+ flash = r.get('flash')
152
+ partes.append('<tr><th>Flash</th><td>' + (
153
+ f'{e(str(flash.get("message", "")))} <em>({e(str(flash.get("type", "")))})</em>' if flash else '—')
154
+ + '</td></tr>')
155
+ if r.get('error'):
156
+ partes.append(f'<tr><th>Error</th><td class="erro"><pre>{e(r["error"])}</pre></td></tr>')
157
+ partes.append('</table></div>')
158
+ partes.append(f'<h2>Queries ({len(r["queries"])})</h2>')
159
+ if r['queries']:
160
+ partes.append('<div class="caixa"><table><tr><th>Datasource</th><th>SQL</th><th>Params</th>'
161
+ '<th>Rows</th><th>Time</th></tr>')
162
+ for q in r['queries']:
163
+ params = ', '.join(f'{e(k)}={e(v)}' for k, v in q['params'].items())
164
+ resultado = f'<span class="erro">{e(q["error"])}</span>' if q['error'] else (q['rows'] if q['rows'] is not None else '—')
165
+ partes.append(f'<tr><td>{e(q["datasource"])}</td><td><pre>{e(q["sql"])}</pre></td>'
166
+ f'<td><code>{params}</code></td><td>{resultado}</td><td>{q["ms"]} ms</td></tr>')
167
+ partes.append('</table></div>')
168
+ else:
169
+ partes.append('<p>None.</p>')
170
+ for escopo, variaveis in (r.get('scopes') or {}).items():
171
+ partes.append(f'<h2>{e(escopo)} ({len(variaveis)})</h2>')
172
+ if variaveis:
173
+ partes.append('<div class="caixa"><table>' + ''.join(
174
+ f'<tr><th><code>{e(k)}</code></th><td><code>{e(v)}</code></td></tr>'
175
+ for k, v in variaveis.items()) + '</table></div>')
176
+ else:
177
+ partes.append('<p>None.</p>')
178
+ return '\n'.join(partes)
@@ -0,0 +1,162 @@
1
+ """
2
+ Phase I: Email Service
3
+
4
+ ColdFusion cfmail-inspired email sending with SMTP support.
5
+ """
6
+
7
+ import smtplib
8
+ import os
9
+ from email.mime.text import MIMEText
10
+ from email.mime.multipart import MIMEMultipart
11
+ from email.mime.base import MIMEBase
12
+ from email import encoders
13
+ from typing import List, Dict, Any, Optional
14
+ from pathlib import Path
15
+
16
+
17
+ class EmailError(Exception):
18
+ """Raised when email sending fails"""
19
+ pass
20
+
21
+
22
+ class EmailService:
23
+ """Service for sending emails via SMTP"""
24
+
25
+ def __init__(self):
26
+ """Initialize email service with config from environment"""
27
+ self.smtp_host = os.getenv('SMTP_HOST', 'localhost')
28
+ self.smtp_port = int(os.getenv('SMTP_PORT', '587'))
29
+ self.smtp_user = os.getenv('SMTP_USER', '')
30
+ self.smtp_password = os.getenv('SMTP_PASSWORD', '')
31
+ self.smtp_use_tls = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true'
32
+ self.default_from = os.getenv('SMTP_FROM', 'noreply@quantum.dev')
33
+
34
+ def send_email(
35
+ self,
36
+ to: str,
37
+ subject: str,
38
+ body: str,
39
+ from_addr: Optional[str] = None,
40
+ cc: Optional[str] = None,
41
+ bcc: Optional[str] = None,
42
+ reply_to: Optional[str] = None,
43
+ email_type: str = "html",
44
+ attachments: Optional[List[str]] = None
45
+ ) -> Dict[str, Any]:
46
+ """
47
+ Send email via SMTP
48
+
49
+ Args:
50
+ to: Recipient email(s) (comma-separated)
51
+ subject: Email subject
52
+ body: Email body content
53
+ from_addr: Sender email (uses default if not provided)
54
+ cc: CC recipients (comma-separated)
55
+ bcc: BCC recipients (comma-separated)
56
+ reply_to: Reply-To address
57
+ email_type: 'html' or 'text'
58
+ attachments: List of file paths to attach
59
+
60
+ Returns:
61
+ Dict with send result
62
+
63
+ Raises:
64
+ EmailError: If sending fails
65
+ """
66
+ try:
67
+ # Create message
68
+ msg = MIMEMultipart()
69
+ msg['From'] = from_addr or self.default_from
70
+ msg['To'] = to
71
+ msg['Subject'] = subject
72
+
73
+ if cc:
74
+ msg['Cc'] = cc
75
+ if reply_to:
76
+ msg['Reply-To'] = reply_to
77
+
78
+ # Attach body
79
+ mime_type = 'html' if email_type == 'html' else 'plain'
80
+ msg.attach(MIMEText(body, mime_type, 'utf-8'))
81
+
82
+ # Attach files if provided
83
+ if attachments:
84
+ for file_path in attachments:
85
+ self._attach_file(msg, file_path)
86
+
87
+ # Prepare recipients
88
+ recipients = self._parse_recipients(to, cc, bcc)
89
+
90
+ # Send email
91
+ if os.getenv('EMAIL_MOCK', 'true').lower() == 'true':
92
+ # Mock mode for development (don't actually send)
93
+ print(f"[MOCK] Email sent to {to}")
94
+ print(f" Subject: {subject}")
95
+ print(f" From: {msg['From']}")
96
+ print(f" Type: {email_type}")
97
+ return {
98
+ 'success': True,
99
+ 'mock': True,
100
+ 'to': to,
101
+ 'subject': subject,
102
+ 'message': 'Email mock-sent successfully'
103
+ }
104
+ else:
105
+ # Real SMTP sending
106
+ with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
107
+ if self.smtp_use_tls:
108
+ server.starttls()
109
+
110
+ if self.smtp_user and self.smtp_password:
111
+ server.login(self.smtp_user, self.smtp_password)
112
+
113
+ server.sendmail(msg['From'], recipients, msg.as_string())
114
+
115
+ return {
116
+ 'success': True,
117
+ 'to': to,
118
+ 'subject': subject,
119
+ 'message': 'Email sent successfully'
120
+ }
121
+
122
+ except Exception as e:
123
+ raise EmailError(f"Failed to send email: {e}")
124
+
125
+ def _attach_file(self, msg: MIMEMultipart, file_path: str):
126
+ """Attach a file to email message"""
127
+ try:
128
+ path = Path(file_path)
129
+ if not path.exists():
130
+ raise EmailError(f"Attachment file not found: {file_path}")
131
+
132
+ with open(file_path, 'rb') as f:
133
+ part = MIMEBase('application', 'octet-stream')
134
+ part.set_payload(f.read())
135
+
136
+ encoders.encode_base64(part)
137
+ part.add_header(
138
+ 'Content-Disposition',
139
+ f'attachment; filename= {path.name}'
140
+ )
141
+
142
+ msg.attach(part)
143
+ except Exception as e:
144
+ raise EmailError(f"Failed to attach file {file_path}: {e}")
145
+
146
+ def _parse_recipients(
147
+ self,
148
+ to: str,
149
+ cc: Optional[str] = None,
150
+ bcc: Optional[str] = None
151
+ ) -> List[str]:
152
+ """Parse comma-separated email addresses into list"""
153
+ recipients = []
154
+
155
+ if to:
156
+ recipients.extend([email.strip() for email in to.split(',')])
157
+ if cc:
158
+ recipients.extend([email.strip() for email in cc.split(',')])
159
+ if bcc:
160
+ recipients.extend([email.strip() for email in bcc.split(',')])
161
+
162
+ return recipients
@@ -0,0 +1,295 @@
1
+ """
2
+ Enhanced Error Handler - Phase C: Developer Experience
3
+
4
+ Provides better error messages with:
5
+ - Context (line numbers, code snippets)
6
+ - Suggestions (did you mean...?)
7
+ - Helpful hints
8
+ - Color-coded output
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Optional, List
14
+ import difflib
15
+
16
+ class QuantumError(Exception):
17
+ """Base class for Quantum errors with enhanced context"""
18
+
19
+ def __init__(self, message: str, file_path: Optional[str] = None,
20
+ line: Optional[int] = None, column: Optional[int] = None,
21
+ suggestion: Optional[str] = None, hint: Optional[str] = None):
22
+ self.message = message
23
+ self.file_path = file_path
24
+ self.line = line
25
+ self.column = column
26
+ self.suggestion = suggestion
27
+ self.hint = hint
28
+ super().__init__(self.format_error())
29
+
30
+ def format_error(self) -> str:
31
+ """Format error message with context"""
32
+ parts = []
33
+
34
+ # Header
35
+ parts.append("=" * 60)
36
+ parts.append("❌ QUANTUM ERROR")
37
+ parts.append("=" * 60)
38
+
39
+ # Location
40
+ if self.file_path:
41
+ location = f"📂 File: {self.file_path}"
42
+ if self.line:
43
+ location += f":{self.line}"
44
+ if self.column:
45
+ location += f":{self.column}"
46
+ parts.append(location)
47
+
48
+ # Error message
49
+ parts.append(f"\n💥 {self.message}")
50
+
51
+ # Code context
52
+ if self.file_path and self.line:
53
+ context = self._get_code_context()
54
+ if context:
55
+ parts.append("\n📝 Code:")
56
+ parts.append(context)
57
+
58
+ # Suggestion
59
+ if self.suggestion:
60
+ parts.append(f"\n💡 Suggestion: {self.suggestion}")
61
+
62
+ # Hint
63
+ if self.hint:
64
+ parts.append(f"\n🔍 Hint: {self.hint}")
65
+
66
+ parts.append("=" * 60)
67
+
68
+ return "\n".join(parts)
69
+
70
+ def _get_code_context(self, context_lines: int = 3) -> Optional[str]:
71
+ """Get code context around error line"""
72
+ try:
73
+ file_path = Path(self.file_path)
74
+ if not file_path.exists():
75
+ return None
76
+
77
+ lines = file_path.read_text(encoding='utf-8').splitlines()
78
+
79
+ start = max(0, self.line - context_lines - 1)
80
+ end = min(len(lines), self.line + context_lines)
81
+
82
+ context_parts = []
83
+ for i in range(start, end):
84
+ line_num = i + 1
85
+ line_content = lines[i]
86
+
87
+ # Mark error line with arrow
88
+ if line_num == self.line:
89
+ context_parts.append(f" → {line_num:4d} | {line_content}")
90
+ # Add column pointer if available
91
+ if self.column:
92
+ pointer = " " * (self.column + 10) + "^"
93
+ context_parts.append(pointer)
94
+ else:
95
+ context_parts.append(f" {line_num:4d} | {line_content}")
96
+
97
+ return "\n".join(context_parts)
98
+
99
+ except Exception:
100
+ return None
101
+
102
+
103
+ class ComponentNotFoundError(QuantumError):
104
+ """Component import not found"""
105
+
106
+ def __init__(self, component_name: str, file_path: str, line: int,
107
+ available_components: List[str] = None):
108
+ # Find similar component names
109
+ suggestion = None
110
+ if available_components:
111
+ matches = difflib.get_close_matches(component_name, available_components, n=3, cutoff=0.6)
112
+ if matches:
113
+ if len(matches) == 1:
114
+ suggestion = f"Did you mean '{matches[0]}'?"
115
+ else:
116
+ suggestion = f"Did you mean one of: {', '.join(matches)}?"
117
+
118
+ hint = f"Add: <q:import component=\"{component_name}\" />"
119
+
120
+ super().__init__(
121
+ f"Component '{component_name}' not found",
122
+ file_path=file_path,
123
+ line=line,
124
+ suggestion=suggestion,
125
+ hint=hint
126
+ )
127
+
128
+
129
+ class ValidationErrorWithContext(QuantumError):
130
+ """Validation error with context"""
131
+
132
+ def __init__(self, field_name: str, validation_type: str,
133
+ value: any, file_path: str = None, line: int = None):
134
+ message = f"Validation failed for field '{field_name}' ({validation_type})"
135
+
136
+ if validation_type == "required":
137
+ suggestion = f"Field '{field_name}' is required but was not provided"
138
+ elif validation_type == "type":
139
+ suggestion = f"Expected type differs from provided value: {value}"
140
+ elif validation_type == "minlength":
141
+ suggestion = f"Value '{value}' is too short"
142
+ elif validation_type == "maxlength":
143
+ suggestion = f"Value '{value}' is too long"
144
+ elif validation_type == "pattern":
145
+ suggestion = f"Value '{value}' doesn't match required pattern"
146
+ else:
147
+ suggestion = f"Validation '{validation_type}' failed for value: {value}"
148
+
149
+ super().__init__(
150
+ message,
151
+ file_path=file_path,
152
+ line=line,
153
+ suggestion=suggestion
154
+ )
155
+
156
+
157
+ class QueryErrorWithContext(QuantumError):
158
+ """Query error with SQL context"""
159
+
160
+ def __init__(self, query_name: str, sql: str, error: str,
161
+ file_path: str = None, line: int = None):
162
+ message = f"Query '{query_name}' failed"
163
+
164
+ # Provide SQL-specific suggestions
165
+ suggestion = None
166
+ if "syntax error" in error.lower():
167
+ suggestion = "Check your SQL syntax"
168
+ elif "table" in error.lower() and "doesn't exist" in error.lower():
169
+ suggestion = "Table doesn't exist - check your database schema"
170
+ elif "column" in error.lower():
171
+ suggestion = "Column doesn't exist - check your table structure"
172
+ elif "duplicate" in error.lower():
173
+ suggestion = "Duplicate entry - check for unique constraints"
174
+
175
+ hint = f"SQL: {sql[:100]}..." if len(sql) > 100 else f"SQL: {sql}"
176
+
177
+ super().__init__(
178
+ f"{message}: {error}",
179
+ file_path=file_path,
180
+ line=line,
181
+ suggestion=suggestion,
182
+ hint=hint
183
+ )
184
+
185
+
186
+ class TransactionErrorWithContext(QuantumError):
187
+ """Transaction error with rollback info"""
188
+
189
+ def __init__(self, error: str, statements_executed: int,
190
+ file_path: str = None, line: int = None):
191
+ message = "Transaction failed and was rolled back"
192
+
193
+ suggestion = f"{statements_executed} statement(s) were executed before failure"
194
+ hint = "All changes have been rolled back. Check the error above and fix the issue."
195
+
196
+ super().__init__(
197
+ f"{message}: {error}",
198
+ file_path=file_path,
199
+ line=line,
200
+ suggestion=suggestion,
201
+ hint=hint
202
+ )
203
+
204
+
205
+ class FileUploadErrorWithContext(QuantumError):
206
+ """File upload error with details"""
207
+
208
+ def __init__(self, error: str, filename: str = None,
209
+ file_size: int = None, max_size: int = None,
210
+ file_path: str = None, line: int = None):
211
+ message = f"File upload failed"
212
+
213
+ suggestion = None
214
+ if "too large" in error.lower():
215
+ if file_size and max_size:
216
+ suggestion = f"File is {file_size} bytes but maximum is {max_size} bytes"
217
+ elif "type" in error.lower() or "extension" in error.lower():
218
+ suggestion = "File type not allowed - check allowed extensions"
219
+
220
+ hint = None
221
+ if filename:
222
+ hint = f"Filename: {filename}"
223
+
224
+ super().__init__(
225
+ f"{message}: {error}",
226
+ file_path=file_path,
227
+ line=line,
228
+ suggestion=suggestion,
229
+ hint=hint
230
+ )
231
+
232
+
233
+ class ErrorHandler:
234
+ """Central error handler for better error reporting"""
235
+
236
+ _logger = None
237
+
238
+ @classmethod
239
+ def _get_logger(cls):
240
+ if cls._logger is None:
241
+ import logging
242
+ cls._logger = logging.getLogger('quantum.errors')
243
+ return cls._logger
244
+
245
+ @staticmethod
246
+ def handle_parse_error(error: Exception, file_path: str) -> QuantumError:
247
+ """Convert parse errors to enhanced errors"""
248
+ error_msg = str(error)
249
+
250
+ # Extract line and column from XML parse errors
251
+ line = None
252
+ column = None
253
+ if "line" in error_msg:
254
+ import re
255
+ match = re.search(r'line (\d+)', error_msg)
256
+ if match:
257
+ line = int(match.group(1))
258
+ match = re.search(r'column (\d+)', error_msg)
259
+ if match:
260
+ column = int(match.group(1))
261
+
262
+ return QuantumError(
263
+ f"Parse error: {error_msg}",
264
+ file_path=file_path,
265
+ line=line,
266
+ column=column,
267
+ suggestion="Check your XML syntax",
268
+ hint="Make sure all tags are properly closed and attributes are quoted"
269
+ )
270
+
271
+ @staticmethod
272
+ def handle_runtime_error(error: Exception, component_name: str,
273
+ file_path: str = None) -> QuantumError:
274
+ """Convert runtime errors to enhanced errors"""
275
+ return QuantumError(
276
+ f"Runtime error in component '{component_name}': {error}",
277
+ file_path=file_path,
278
+ suggestion="Check your component logic and variable names",
279
+ hint="Run the component with `quantum run <file>.q` to see the error without the server"
280
+ )
281
+
282
+ @classmethod
283
+ def print_warning(cls, message: str):
284
+ """Log formatted warning"""
285
+ cls._get_logger().warning(message)
286
+
287
+ @classmethod
288
+ def print_info(cls, message: str):
289
+ """Log formatted info"""
290
+ cls._get_logger().info(message)
291
+
292
+ @classmethod
293
+ def print_success(cls, message: str):
294
+ """Log formatted success"""
295
+ cls._get_logger().info(message)