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,24 @@
1
+ """
2
+ WebSocket Feature Module
3
+
4
+ Provides real-time bidirectional communication via WebSockets.
5
+
6
+ Usage:
7
+ from quantum.core.features.websocket.src import (
8
+ WebSocketNode, WebSocketHandlerNode, WebSocketSendNode, WebSocketCloseNode
9
+ )
10
+ """
11
+
12
+ from .ast_node import (
13
+ WebSocketNode,
14
+ WebSocketHandlerNode,
15
+ WebSocketSendNode,
16
+ WebSocketCloseNode
17
+ )
18
+
19
+ __all__ = [
20
+ 'WebSocketNode',
21
+ 'WebSocketHandlerNode',
22
+ 'WebSocketSendNode',
23
+ 'WebSocketCloseNode'
24
+ ]
@@ -0,0 +1,247 @@
1
+ """
2
+ WebSocket AST Nodes
3
+
4
+ Defines the AST structure for WebSocket connections and messaging.
5
+
6
+ Classes:
7
+ WebSocketHandlerNode - Event handler (on-connect, on-message, etc.)
8
+ WebSocketSendNode - Send message action
9
+ WebSocketNode - Main WebSocket connection definition
10
+ """
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import List, Optional, Any
14
+
15
+ # Import base class
16
+ try:
17
+ from quantum.core.ast_nodes import QuantumNode
18
+ except ImportError:
19
+ from ...ast_nodes import QuantumNode
20
+
21
+
22
+ @dataclass
23
+ class WebSocketHandlerNode(QuantumNode):
24
+ """
25
+ Event handler for WebSocket events.
26
+
27
+ Attributes:
28
+ event: Event type (connect, message, error, close, open)
29
+ body: List of AST nodes to execute when event fires
30
+
31
+ Example:
32
+ <q:on-message>
33
+ <q:set name="lastMessage" value="{data}" />
34
+ <q:set name="messages" operation="append" value="{data}" />
35
+ </q:on-message>
36
+ """
37
+ event: str = "" # connect, message, error, close, open
38
+ body: List[QuantumNode] = field(default_factory=list)
39
+
40
+ def to_dict(self) -> dict:
41
+ """Convert to dictionary."""
42
+ return {
43
+ "event": self.event,
44
+ "body": [b.to_dict() if hasattr(b, 'to_dict') else str(b) for b in self.body]
45
+ }
46
+
47
+ def validate(self) -> List[str]:
48
+ """Validate the handler node."""
49
+ errors = []
50
+ valid_events = {"connect", "open", "message", "error", "close", "disconnect"}
51
+ if self.event not in valid_events:
52
+ errors.append(f"Invalid WebSocket event '{self.event}'. Valid: {valid_events}")
53
+ return errors
54
+
55
+
56
+ @dataclass
57
+ class WebSocketSendNode(QuantumNode):
58
+ """
59
+ Send a message through a WebSocket connection.
60
+
61
+ Attributes:
62
+ connection: Name of the WebSocket connection to use
63
+ message: Message content to send (supports databinding)
64
+ type: Message type (text, json, binary)
65
+
66
+ Example:
67
+ <q:websocket-send connection="chat" message="{userInput}" type="json" />
68
+ """
69
+ connection: str = ""
70
+ message: str = ""
71
+ type: str = "text" # text, json, binary
72
+
73
+ def to_dict(self) -> dict:
74
+ """Convert to dictionary."""
75
+ return {
76
+ "connection": self.connection,
77
+ "message": self.message,
78
+ "type": self.type
79
+ }
80
+
81
+ def validate(self) -> List[str]:
82
+ """Validate the send node."""
83
+ errors = []
84
+ if not self.connection:
85
+ errors.append("WebSocketSendNode requires 'connection' attribute")
86
+ if not self.message:
87
+ errors.append("WebSocketSendNode requires 'message' attribute")
88
+ valid_types = {"text", "json", "binary"}
89
+ if self.type not in valid_types:
90
+ errors.append(f"Invalid message type '{self.type}'. Valid: {valid_types}")
91
+ return errors
92
+
93
+
94
+ @dataclass
95
+ class WebSocketCloseNode(QuantumNode):
96
+ """
97
+ Close a WebSocket connection.
98
+
99
+ Attributes:
100
+ connection: Name of the WebSocket connection to close
101
+ code: Close code (optional, default 1000 = normal closure)
102
+ reason: Close reason message (optional)
103
+
104
+ Example:
105
+ <q:websocket-close connection="chat" code="1000" reason="User logged out" />
106
+ """
107
+ connection: str = ""
108
+ code: int = 1000
109
+ reason: str = ""
110
+
111
+ def to_dict(self) -> dict:
112
+ """Convert to dictionary."""
113
+ return {
114
+ "connection": self.connection,
115
+ "code": self.code,
116
+ "reason": self.reason
117
+ }
118
+
119
+ def validate(self) -> List[str]:
120
+ """Validate the close node."""
121
+ errors = []
122
+ if not self.connection:
123
+ errors.append("WebSocketCloseNode requires 'connection' attribute")
124
+ return errors
125
+
126
+
127
+ @dataclass
128
+ class WebSocketNode(QuantumNode):
129
+ """
130
+ WebSocket connection definition.
131
+
132
+ Creates a persistent WebSocket connection with event handlers
133
+ for real-time bidirectional communication.
134
+
135
+ Attributes:
136
+ name: Variable name for the connection (required)
137
+ url: WebSocket URL (ws:// or wss://)
138
+ auto_connect: Connect automatically on component mount (default: true)
139
+ reconnect: Auto-reconnect on disconnect (default: true)
140
+ reconnect_delay: Initial delay between reconnects in ms (default: 1000)
141
+ max_reconnects: Maximum reconnection attempts (default: 10)
142
+ heartbeat: Heartbeat interval in ms, 0 to disable (default: 30000)
143
+ protocols: WebSocket sub-protocols (optional, comma-separated)
144
+ handlers: Event handlers (on-connect, on-message, etc.)
145
+
146
+ Result Object:
147
+ {name}: Connection status object with:
148
+ - connected: bool
149
+ - url: str
150
+ - readyState: int (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)
151
+ - lastMessage: any
152
+ - error: str or None
153
+ - messageCount: int
154
+ - connectedAt: datetime or None
155
+
156
+ Example:
157
+ <q:websocket name="chat" url="wss://api.example.com/chat"
158
+ auto_connect="true" reconnect="true">
159
+
160
+ <q:on-connect>
161
+ <q:log level="info">Connected to chat server</q:log>
162
+ <q:set name="chatStatus" value="online" />
163
+ </q:on-connect>
164
+
165
+ <q:on-message>
166
+ <q:set name="messages" operation="append" value="{data}" />
167
+ </q:on-message>
168
+
169
+ <q:on-error>
170
+ <q:log level="error">WebSocket error: {error}</q:log>
171
+ </q:on-error>
172
+
173
+ <q:on-close>
174
+ <q:set name="chatStatus" value="offline" />
175
+ </q:on-close>
176
+
177
+ </q:websocket>
178
+
179
+ <!-- Send message -->
180
+ <q:action name="sendMessage">
181
+ <q:websocket-send connection="chat" message="{messageInput}" type="json" />
182
+ <q:set name="messageInput" value="" />
183
+ </q:action>
184
+ """
185
+ name: str = ""
186
+ url: str = ""
187
+ auto_connect: bool = True
188
+ reconnect: bool = True
189
+ reconnect_delay: int = 1000 # ms
190
+ max_reconnects: int = 10
191
+ heartbeat: int = 30000 # ms, 0 to disable
192
+ protocols: str = "" # comma-separated sub-protocols
193
+
194
+ # Event handlers
195
+ handlers: List[WebSocketHandlerNode] = field(default_factory=list)
196
+
197
+ def to_dict(self) -> dict:
198
+ """Convert to dictionary."""
199
+ return {
200
+ "name": self.name,
201
+ "url": self.url,
202
+ "auto_connect": self.auto_connect,
203
+ "reconnect": self.reconnect,
204
+ "reconnect_delay": self.reconnect_delay,
205
+ "max_reconnects": self.max_reconnects,
206
+ "heartbeat": self.heartbeat,
207
+ "protocols": self.protocols,
208
+ "handlers": [h.to_dict() for h in self.handlers]
209
+ }
210
+
211
+ def validate(self) -> List[str]:
212
+ """Validate the WebSocket node."""
213
+ errors = []
214
+
215
+ if not self.name:
216
+ errors.append("WebSocketNode requires 'name' attribute")
217
+
218
+ if not self.url:
219
+ errors.append("WebSocketNode requires 'url' attribute")
220
+ elif not self.url.startswith(("{", "ws://", "wss://")):
221
+ errors.append(f"WebSocket URL must start with ws:// or wss://, got: {self.url}")
222
+
223
+ if self.reconnect_delay < 0:
224
+ errors.append("reconnect_delay must be non-negative")
225
+
226
+ if self.max_reconnects < 0:
227
+ errors.append("max_reconnects must be non-negative")
228
+
229
+ if self.heartbeat < 0:
230
+ errors.append("heartbeat must be non-negative")
231
+
232
+ # Validate handlers
233
+ for handler in self.handlers:
234
+ errors.extend(handler.validate())
235
+
236
+ return errors
237
+
238
+ def get_handler(self, event: str) -> Optional[WebSocketHandlerNode]:
239
+ """Get handler for specific event."""
240
+ for handler in self.handlers:
241
+ if handler.event == event:
242
+ return handler
243
+ return None
244
+
245
+ def has_handler(self, event: str) -> bool:
246
+ """Check if handler exists for event."""
247
+ return self.get_handler(event) is not None
quantum/core/guards.py ADDED
@@ -0,0 +1,53 @@
1
+ """Page guards (AUTH-6): a q:if at the top of a page whose branch has a q:redirect.
2
+
3
+ <q:if condition="not session.authenticated">
4
+ <q:redirect url="/login" />
5
+ </q:if>
6
+
7
+ A q:action does not run the page's statements, so a guard written like this
8
+ used to protect only the GET — the POST ran the action without a session.
9
+ Guards now run before each action of the page as well as before the render.
10
+ """
11
+
12
+ from typing import List
13
+
14
+ from quantum.core.ast_nodes import RedirectNode
15
+ from quantum.core.features.conditionals.src.ast_node import IfNode
16
+
17
+
18
+ def _branches(node: IfNode) -> List:
19
+ corpo = list(node.if_body)
20
+ for bloco in node.elseif_blocks:
21
+ corpo += list(bloco.get('body', []))
22
+ return corpo + list(node.else_body)
23
+
24
+
25
+ def _has_redirect(nodes) -> bool:
26
+ return any(isinstance(n, RedirectNode) or (isinstance(n, IfNode) and _has_redirect(_branches(n)))
27
+ for n in nodes)
28
+
29
+
30
+ def page_guards(component) -> List[IfNode]:
31
+ """The top-level q:if statements that can redirect, in document order."""
32
+ return [s for s in getattr(component, 'statements', [])
33
+ if isinstance(s, IfNode) and _has_redirect(_branches(s))]
34
+
35
+
36
+ def guard_conditions(node: IfNode) -> List[str]:
37
+ """Every condition a guard evaluates, nested q:if included."""
38
+ condicoes = [node.condition] + [b.get('condition', '') for b in node.elseif_blocks]
39
+ for filho in _branches(node):
40
+ if isinstance(filho, IfNode):
41
+ condicoes += guard_conditions(filho)
42
+ return [c for c in condicoes if c]
43
+
44
+
45
+ def mark_guards(component) -> None:
46
+ """Flag each guard, nested q:if included, so its condition fails closed (AUTH-6)."""
47
+ def marcar(node):
48
+ node.is_guard = True
49
+ for filho in _branches(node):
50
+ if isinstance(filho, IfNode):
51
+ marcar(filho)
52
+ for guarda in page_guards(component):
53
+ marcar(guarda)
@@ -0,0 +1,299 @@
1
+ """
2
+ Accept the HTML people actually write.
3
+
4
+ A `.q` file is XML, but its body is HTML, and two things every HTML author
5
+ writes are invalid XML:
6
+
7
+ 1. **Boolean attributes** — `<input required autofocus />`. HTML5 allows a
8
+ bare attribute name; XML demands `required="required"`.
9
+ 2. **A bare ampersand in text** — `Forms & Actions`. Browsers tolerate it;
10
+ XML does not.
11
+
12
+ Ten of the forty-eight components SHIPPED WITH THE FRAMEWORK failed to parse
13
+ for exactly these two reasons, which meant they returned HTTP 400 — including
14
+ `login.q`, the login screen. The error message told the author to escape the
15
+ character, which is true and useless: the file was written the way HTML is
16
+ written.
17
+
18
+ This normalises both before the XML parser sees the source. It is deliberately
19
+ narrow:
20
+
21
+ - Only known boolean attribute names are expanded, and only inside a start
22
+ tag, never in text.
23
+ - Only an `&` that does not already begin a valid entity is escaped.
24
+ - `<script>`, `<style>`, comments and CDATA sections are left untouched —
25
+ their contents are not XML and rewriting them would corrupt real code.
26
+ """
27
+
28
+ import re
29
+
30
+ # HTML5 boolean attributes. A closed list: expanding an arbitrary bare word
31
+ # would turn a typo into a silently accepted attribute.
32
+ BOOLEAN_ATTRS = frozenset({
33
+ 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',
34
+ 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert',
35
+ 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate',
36
+ 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected',
37
+ 'shadowrootclonable', 'shadowrootdelegatesfocus', 'shadowrootserializable',
38
+ })
39
+
40
+ # HTML void elements. `<br>` and `<meta charset="utf-8">` are correct HTML and
41
+ # unclosed tags to an XML parser, which then reports the error far away — at
42
+ # the `</head>` or `</div>` that no longer matches. components/login.q and
43
+ # components/products.q both failed this way, and the reported line was
44
+ # nowhere near the real cause.
45
+ VOID_ELEMENTS = frozenset({
46
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
47
+ 'meta', 'param', 'source', 'track', 'wbr',
48
+ })
49
+
50
+ # Regions whose contents must not be rewritten.
51
+ _PROTECTED = re.compile(
52
+ r'(<!\[CDATA\[.*?\]\]>'
53
+ r'|<!--.*?-->'
54
+ r'|<script\b[^>]*>.*?</script\s*>'
55
+ r'|<style\b[^>]*>.*?</style\s*>)',
56
+ re.DOTALL | re.IGNORECASE,
57
+ )
58
+
59
+ # Tags whose body is Python source, not markup. HTML gives <script> exactly
60
+ # this treatment; XML gives it to nothing, so a perfectly ordinary line like
61
+ #
62
+ # pattern = re.compile(r'<q:agent\s+([^>]*?)/?>')
63
+ #
64
+ # ended the parse with "not well-formed (invalid token)" pointing at the
65
+ # regex. components/admin/agents.q and components/admin/components.q were
66
+ # both unparseable — HTTP 400 from the server — for that reason, and the
67
+ # author's only recourse was to know to write CDATA by hand.
68
+ #
69
+ # `script` e `style` estao aqui pela MESMA razao. O HTML trata o corpo dos
70
+ # dois como texto cru; o XML nao trata nada assim. Um template literal com
71
+ # markup dentro —
72
+ #
73
+ # lista.innerHTML = todos.map(t => `<div class="item">${t.task}</div>`)
74
+ #
75
+ # — nao e XML: `${t.completed ? 'checked' : ''}` dentro de uma tag e um
76
+ # token invalido. components/islands_demo.q, entregue no repositorio,
77
+ # devolvia HTTP 400 por isso, e nao havia o que o autor pudesse fazer a nao
78
+ # ser escrever CDATA a mao em volta do proprio JavaScript.
79
+ #
80
+ # Medido antes de mudar: dos 47 arquivos .q com <script>/<style>, 46
81
+ # renderizam byte a byte igual e 1 (islands_demo) deixa de ser erro de
82
+ # parse. Nenhum piora.
83
+ _RAW_TEXT_TAGS = ('q:python', 'q:class', 'q:pyclass', 'script', 'style')
84
+ _RAW_TEXT = re.compile(
85
+ r'(<(' + '|'.join(_RAW_TEXT_TAGS) + r')\b[^>]*>)(.*?)(</\2\s*>)',
86
+ re.DOTALL | re.IGNORECASE,
87
+ )
88
+
89
+
90
+ def wrap_raw_text_tags(source: str) -> str:
91
+ """Put the body of the scripting tags in CDATA when it needs it."""
92
+ def wrap(match):
93
+ open_tag, _name, body, close_tag = match.groups()
94
+ if '<' not in body and '&' not in body:
95
+ return match.group(0)
96
+ if '<![CDATA[' in body:
97
+ return match.group(0)
98
+
99
+ # Dentro de CDATA nada e decodificado, entao a entidade tem de ser
100
+ # desfeita AQUI. Sem isto, `if a &lt; 10:` chegava ao exec() com o
101
+ # `&lt;` literal — SyntaxError. Antes deste embrulho o parser XML
102
+ # decodificava; embrulhar tirou a decodificacao e quebrou 6 arquivos
103
+ # .q entregues no repositorio, que usavam a forma escapada porque era
104
+ # a unica que funcionava.
105
+ #
106
+ # `&amp;` por ultimo: desfazer antes transformaria `&amp;lt;` em `<`.
107
+ for entidade, caractere in (('&lt;', '<'), ('&gt;', '>'),
108
+ ('&quot;', '"'), ('&apos;', "'"),
109
+ ('&amp;', '&')):
110
+ body = body.replace(entidade, caractere)
111
+ # ]]> cannot appear inside a CDATA section; split it across two.
112
+ body = body.replace(']]>', ']]]]><![CDATA[>')
113
+ return f'{open_tag}<![CDATA[{body}]]>{close_tag}'
114
+
115
+ return _RAW_TEXT.sub(wrap, source)
116
+
117
+ # A start tag: <name ...attrs...> or <name ... />
118
+ #
119
+ # The attribute part must understand QUOTED VALUES, because `>` is legal inside
120
+ # one: `<q:validator expression="parseInt(value) >= 18" />`. A pattern of
121
+ # `[^<>]*` stopped at that `>` and mangled the tag — it silently turned
122
+ # `parseInt(value) >= 18` into `parseInt(value)>= 18`, which a test caught.
123
+ #
124
+ # The trailing `/` is NOT matched separately: a greedy attribute run would
125
+ # swallow it anyway (`<meta ... />` came back as `<meta ... / />`, breaking
126
+ # files that were previously fine). fix_tag strips it from the end instead,
127
+ # which leaves interior whitespace exactly as written.
128
+ _TAG = re.compile(
129
+ r'<([A-Za-z_][\w:.-]*)((?:\s(?:[^<>"\']|"[^"]*"|\'[^\']*\')*)?)>',
130
+ re.DOTALL,
131
+ )
132
+
133
+ # A bare attribute name sitting on its own inside a tag's attribute list.
134
+ _BARE_ATTR = re.compile(r'(^|\s)([A-Za-z_][\w:-]*)(?=\s|$)')
135
+
136
+ # `&` that already starts a well-formed entity.
137
+ _GOOD_ENTITY = re.compile(r'&(#\d+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*);')
138
+
139
+
140
+ def _expand_boolean_attrs(attrs: str) -> str:
141
+ """`required autofocus` -> `required="required" autofocus="autofocus"`."""
142
+ if not attrs.strip():
143
+ return attrs
144
+
145
+ # Split on quoted values so a bare word inside a value is never touched.
146
+ parts = re.split(r'("[^"]*"|\'[^\']*\')', attrs)
147
+ for i in range(0, len(parts), 2): # even indexes are outside quotes
148
+ segment = parts[i]
149
+
150
+ def repl(m):
151
+ lead, name = m.group(1), m.group(2)
152
+ # `foo=` is a normal attribute whose value follows; leave it.
153
+ after = segment[m.end():]
154
+ if after.lstrip().startswith('='):
155
+ return m.group(0)
156
+ if name.lower() in BOOLEAN_ATTRS:
157
+ return f'{lead}{name}="{name}"'
158
+ return m.group(0)
159
+
160
+ parts[i] = _BARE_ATTR.sub(repl, segment)
161
+ return ''.join(parts)
162
+
163
+
164
+ _QUOTED_VALUE = re.compile(r'"([^"]*)"|\'([^\']*)\'')
165
+
166
+
167
+ def _escape_bare_lt_in_values(attrs: str) -> str:
168
+ """Escapa `<` dentro do VALOR de um atributo.
169
+
170
+ XML proibe `<` num valor de atributo; HTML tolerante e as pessoas nao.
171
+ Estas duas linhas sao naturais de escrever e nao parseavam:
172
+
173
+ <q:if condition="n < 0">
174
+ <q:return value="<h1>Oi</h1>" />
175
+
176
+ Dez exemplos da documentacao — inclusive no quick-start — eram exatamente
177
+ isso. `&` cru e atributo booleano ja eram tolerados aqui; `<` seguia a
178
+ mesma logica e faltava.
179
+
180
+ Escapar preserva o valor: o parser XML desfaz `&lt;` na leitura, entao o
181
+ autor recebe de volta o `<` que escreveu. So o valor entre aspas e
182
+ tocado; o `<` que ABRE a tag esta fora deste trecho.
183
+ """
184
+ def troca(m):
185
+ aspas = '"' if m.group(1) is not None else "'"
186
+ valor = m.group(1) if m.group(1) is not None else m.group(2)
187
+ return f'{aspas}{valor.replace("<", "&lt;")}{aspas}'
188
+
189
+ return _QUOTED_VALUE.sub(troca, attrs)
190
+
191
+
192
+ # XML predefine CINCO entidades; HTML define mais de dois mil. `&nbsp;` — a
193
+ # mais usada de todas — nao existe em XML, entao um arquivo com ela dava
194
+ # "undefined entity" e HTTP 400. Este modulo existe para aceitar o HTML que as
195
+ # pessoas escrevem, e parava um passo antes da propria missao.
196
+ #
197
+ # A traducao e para referencia NUMERICA, que o XML aceita: `&nbsp;` -> `&#160;`.
198
+ # O caractere final e o mesmo; so a forma de escrever muda.
199
+ def _named_entities_to_numeric(text: str) -> str:
200
+ if '&' not in text:
201
+ return text
202
+
203
+ from html.entities import html5
204
+
205
+ def troca(m):
206
+ nome = m.group(1)
207
+ # As cinco do XML ficam como estao: sao validas e legiveis.
208
+ if nome in ('lt', 'gt', 'amp', 'quot', 'apos'):
209
+ return m.group(0)
210
+ caractere = html5.get(nome + ';')
211
+ if caractere is None:
212
+ return m.group(0)
213
+ return ''.join(f'&#{ord(c)};' for c in caractere)
214
+
215
+ return re.sub(r'&([A-Za-z][A-Za-z0-9]*);', troca, text)
216
+
217
+
218
+ def _escape_bare_ampersands(text: str) -> str:
219
+ """`Forms & Actions` -> `Forms &amp; Actions`, leaving real entities."""
220
+ if '&' not in text:
221
+ return text
222
+ out = []
223
+ i = 0
224
+ while i < len(text):
225
+ ch = text[i]
226
+ if ch != '&':
227
+ out.append(ch)
228
+ i += 1
229
+ continue
230
+ m = _GOOD_ENTITY.match(text, i)
231
+ if m:
232
+ out.append(m.group(0))
233
+ i = m.end()
234
+ else:
235
+ out.append('&amp;')
236
+ i += 1
237
+ return ''.join(out)
238
+
239
+
240
+ def normalise_html(source: str) -> str:
241
+ """Make ordinary HTML parseable as XML, without touching code regions."""
242
+ # First, because it CREATES protected regions: Python inside q:python is
243
+ # not markup, and wrapping it in CDATA both makes it parse and keeps the
244
+ # rewrites below away from it.
245
+ source = wrap_raw_text_tags(source)
246
+
247
+ pieces = _PROTECTED.split(source)
248
+ # split() with one capturing group alternates: text, protected, text, ...
249
+ for i in range(0, len(pieces), 2):
250
+ chunk = pieces[i]
251
+
252
+ # 1. boolean attributes, inside start tags only
253
+ def fix_tag(m):
254
+ name, attrs = m.group(1), m.group(2)
255
+
256
+ # Closing tags (</div>) and declarations are not start tags.
257
+ if name.startswith('/'):
258
+ return m.group(0)
259
+
260
+ # Peel a trailing self-closing slash off the attribute run, leaving
261
+ # every other character — including interior whitespace — alone.
262
+ self_closing = attrs.rstrip().endswith('/')
263
+ if self_closing:
264
+ stripped = attrs.rstrip()
265
+ attrs = stripped[:-1]
266
+
267
+ attrs = _expand_boolean_attrs(attrs)
268
+ # A bare & inside an attribute VALUE is invalid XML too, and query
269
+ # strings are full of them: href="/x?a=1&b=2".
270
+ attrs = _escape_bare_ampersands(attrs)
271
+ attrs = _named_entities_to_numeric(attrs)
272
+ # E um `<` cru dentro do valor tambem: `condition="n < 0"` e
273
+ # `value="<h1>Oi</h1>"` sao o que as pessoas escrevem — e o que a
274
+ # propria documentacao ensinava em dez exemplos que nao
275
+ # parseavam.
276
+ attrs = _escape_bare_lt_in_values(attrs)
277
+
278
+ # Close void elements the author left open (<br>, <meta ...>).
279
+ # `name.lower()` casava componente PascalCase com void
280
+ # element: <Link>, <Input>, <Source>, <Meta> viravam
281
+ # `<Link ... />conteudo</Link>` — auto-fechados, com a tag
282
+ # de fechamento orfa. Void element e conceito de HTML, e
283
+ # HTML se escreve em minusculas; chamada de componente e
284
+ # PascalCase por convencao e nao e HTML.
285
+ if self_closing or (name.islower() and name in VOID_ELEMENTS):
286
+ sep = '' if (not attrs or attrs.endswith(' ')) else ' '
287
+ return f'<{name}{attrs}{sep}/>'
288
+ return f'<{name}{attrs}>'
289
+
290
+ chunk = _TAG.sub(fix_tag, chunk)
291
+
292
+ # 2. bare ampersands, in the text between tags only
293
+ segments = re.split(r'(<[^>]*>)', chunk)
294
+ for j in range(0, len(segments), 2): # even indexes are text
295
+ segments[j] = _escape_bare_ampersands(segments[j])
296
+ segments[j] = _named_entities_to_numeric(segments[j])
297
+ pieces[i] = ''.join(segments)
298
+
299
+ return ''.join(pieces)