quantum-framework 0.9.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 (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -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 +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,951 @@
1
+ """
2
+ Quantum Hot Reload System
3
+
4
+ Provides file watching and WebSocket-based live reload for development.
5
+
6
+ Components:
7
+ - HotReloadWatcher: File watcher using watchdog
8
+ - HotReloadServer: WebSocket server to push updates to browsers
9
+ - HotReloadManager: Coordinates file watching and WebSocket broadcasting
10
+
11
+ Usage:
12
+ from quantum.cli.hot_reload import HotReloadManager
13
+
14
+ manager = HotReloadManager(
15
+ watch_paths=[Path('./components')],
16
+ port=35729
17
+ )
18
+ manager.start()
19
+ """
20
+
21
+ import asyncio
22
+ import json
23
+ import threading
24
+ import time
25
+ import hashlib
26
+ from pathlib import Path
27
+ from typing import Callable, Dict, List, Optional, Set
28
+ from dataclasses import dataclass, field
29
+ from datetime import datetime
30
+ from enum import Enum
31
+
32
+
33
+ class ReloadType(Enum):
34
+ """Type of reload to perform."""
35
+ FULL = "full" # Full page reload (structure changes)
36
+ CSS = "css" # CSS-only reload (style changes)
37
+ COMPONENT = "component" # Partial component reload
38
+
39
+
40
+ @dataclass
41
+ class FileChange:
42
+ """Represents a file change event."""
43
+ path: Path
44
+ change_type: str # 'modified', 'created', 'deleted'
45
+ timestamp: float = field(default_factory=time.time)
46
+
47
+ @property
48
+ def extension(self) -> str:
49
+ return self.path.suffix.lower()
50
+
51
+ @property
52
+ def reload_type(self) -> ReloadType:
53
+ """Determine reload type based on file extension."""
54
+ if self.extension in ('.css', '.scss', '.sass', '.less'):
55
+ return ReloadType.CSS
56
+ return ReloadType.FULL
57
+
58
+
59
+ class HotReloadWatcher:
60
+ """
61
+ File watcher using watchdog library.
62
+
63
+ Watches specified directories for changes and triggers callbacks.
64
+ Includes debouncing to handle rapid file changes.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ paths: List[Path],
70
+ extensions: List[str] = None,
71
+ debounce_ms: int = 100,
72
+ on_change: Callable[[List[FileChange]], None] = None
73
+ ):
74
+ """
75
+ Initialize the file watcher.
76
+
77
+ Args:
78
+ paths: Directories or files to watch
79
+ extensions: File extensions to watch (e.g., ['.q', '.css'])
80
+ debounce_ms: Debounce delay in milliseconds
81
+ on_change: Callback when files change
82
+ """
83
+ self.paths = [Path(p).resolve() for p in paths]
84
+ self.extensions = extensions or ['.q', '.yaml', '.yml', '.css', '.js', '.html']
85
+ self.debounce_ms = debounce_ms
86
+ self.on_change = on_change
87
+
88
+ self._running = False
89
+ self._thread: Optional[threading.Thread] = None
90
+ self._observer = None
91
+ self._pending_changes: Dict[Path, FileChange] = {}
92
+ self._debounce_timer: Optional[threading.Timer] = None
93
+ self._lock = threading.Lock()
94
+
95
+ # File content hashes for detecting actual changes
96
+ self._file_hashes: Dict[Path, str] = {}
97
+
98
+ def start(self):
99
+ """Start watching files."""
100
+ if self._running:
101
+ return
102
+
103
+ self._running = True
104
+
105
+ try:
106
+ from watchdog.observers import Observer
107
+ from watchdog.events import FileSystemEventHandler
108
+
109
+ watcher = self
110
+
111
+ class ChangeHandler(FileSystemEventHandler):
112
+ def on_modified(self, event):
113
+ if not event.is_directory:
114
+ watcher._handle_change(Path(event.src_path), 'modified')
115
+
116
+ def on_created(self, event):
117
+ if not event.is_directory:
118
+ watcher._handle_change(Path(event.src_path), 'created')
119
+
120
+ def on_deleted(self, event):
121
+ if not event.is_directory:
122
+ watcher._handle_change(Path(event.src_path), 'deleted')
123
+
124
+ self._observer = Observer()
125
+ handler = ChangeHandler()
126
+
127
+ for path in self.paths:
128
+ if path.exists():
129
+ if path.is_dir():
130
+ self._observer.schedule(handler, str(path), recursive=True)
131
+ else:
132
+ self._observer.schedule(handler, str(path.parent), recursive=False)
133
+
134
+ self._observer.start()
135
+
136
+ except ImportError:
137
+ # Fallback to polling if watchdog not available
138
+ self._thread = threading.Thread(target=self._poll_loop, daemon=True)
139
+ self._thread.start()
140
+
141
+ def stop(self):
142
+ """Stop watching files."""
143
+ self._running = False
144
+
145
+ if self._observer:
146
+ self._observer.stop()
147
+ self._observer.join(timeout=2)
148
+ self._observer = None
149
+
150
+ if self._thread:
151
+ self._thread.join(timeout=2)
152
+ self._thread = None
153
+
154
+ if self._debounce_timer:
155
+ self._debounce_timer.cancel()
156
+ self._debounce_timer = None
157
+
158
+ def _handle_change(self, path: Path, change_type: str):
159
+ """Handle a file change event."""
160
+ path = path.resolve()
161
+
162
+ # Check extension filter
163
+ if self.extensions and path.suffix.lower() not in self.extensions:
164
+ return
165
+
166
+ # Check if content actually changed (for modified events)
167
+ if change_type == 'modified':
168
+ try:
169
+ current_hash = self._compute_hash(path)
170
+ if path in self._file_hashes and self._file_hashes[path] == current_hash:
171
+ return # Content unchanged, skip
172
+ self._file_hashes[path] = current_hash
173
+ except (IOError, OSError):
174
+ pass # File might be locked or deleted
175
+
176
+ # Add to pending changes
177
+ with self._lock:
178
+ self._pending_changes[path] = FileChange(path, change_type)
179
+
180
+ # Reset debounce timer
181
+ if self._debounce_timer:
182
+ self._debounce_timer.cancel()
183
+
184
+ self._debounce_timer = threading.Timer(
185
+ self.debounce_ms / 1000.0,
186
+ self._flush_changes
187
+ )
188
+ self._debounce_timer.start()
189
+
190
+ def _flush_changes(self):
191
+ """Flush pending changes to callback."""
192
+ with self._lock:
193
+ if not self._pending_changes:
194
+ return
195
+
196
+ changes = list(self._pending_changes.values())
197
+ self._pending_changes.clear()
198
+
199
+ if self.on_change and changes:
200
+ try:
201
+ self.on_change(changes)
202
+ except Exception as e:
203
+ print(f"[Hot Reload] Error in change callback: {e}")
204
+
205
+ def _compute_hash(self, path: Path) -> str:
206
+ """Compute MD5 hash of file content."""
207
+ try:
208
+ content = path.read_bytes()
209
+ return hashlib.md5(content).hexdigest()
210
+ except (IOError, OSError):
211
+ return ""
212
+
213
+ def _poll_loop(self):
214
+ """Polling fallback when watchdog is not available."""
215
+ last_mtimes: Dict[Path, float] = {}
216
+
217
+ while self._running:
218
+ try:
219
+ current_files: Set[Path] = set()
220
+
221
+ for base_path in self.paths:
222
+ if not base_path.exists():
223
+ continue
224
+
225
+ if base_path.is_file():
226
+ files = [base_path]
227
+ else:
228
+ files = []
229
+ for ext in self.extensions:
230
+ files.extend(base_path.rglob(f'*{ext}'))
231
+
232
+ for file_path in files:
233
+ file_path = file_path.resolve()
234
+ current_files.add(file_path)
235
+
236
+ try:
237
+ mtime = file_path.stat().st_mtime
238
+
239
+ if file_path in last_mtimes:
240
+ if mtime > last_mtimes[file_path]:
241
+ self._handle_change(file_path, 'modified')
242
+
243
+ last_mtimes[file_path] = mtime
244
+ except (IOError, OSError):
245
+ pass
246
+
247
+ # Check for deleted files
248
+ for old_file in list(last_mtimes.keys()):
249
+ if old_file not in current_files:
250
+ self._handle_change(old_file, 'deleted')
251
+ del last_mtimes[old_file]
252
+
253
+ except Exception:
254
+ pass
255
+
256
+ time.sleep(0.5)
257
+
258
+
259
+ class HotReloadServer:
260
+ """
261
+ WebSocket server for pushing reload events to browsers.
262
+
263
+ Uses asyncio and the websockets library.
264
+ """
265
+
266
+ def __init__(self, host: str = 'localhost', port: int = 35729):
267
+ """
268
+ Initialize the WebSocket server.
269
+
270
+ Args:
271
+ host: Host to bind to
272
+ port: Port for WebSocket server
273
+ """
274
+ self.host = host
275
+ self.port = port
276
+ self._clients: Set = set()
277
+ self._server = None
278
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
279
+ self._thread: Optional[threading.Thread] = None
280
+ self._running = False
281
+
282
+ def start(self):
283
+ """Start the WebSocket server in a background thread."""
284
+ if self._running:
285
+ return
286
+
287
+ self._running = True
288
+ self._thread = threading.Thread(target=self._run_server, daemon=True)
289
+ self._thread.start()
290
+
291
+ # Wait a bit for server to start
292
+ time.sleep(0.2)
293
+
294
+ def stop(self):
295
+ """Stop the WebSocket server."""
296
+ self._running = False
297
+
298
+ if self._loop:
299
+ self._loop.call_soon_threadsafe(self._loop.stop)
300
+
301
+ if self._thread:
302
+ self._thread.join(timeout=2)
303
+ self._thread = None
304
+
305
+ def _run_server(self):
306
+ """Run the WebSocket server (in background thread)."""
307
+ try:
308
+ import websockets
309
+ import websockets.server
310
+ except ImportError:
311
+ print("[Hot Reload] websockets library not found. Using fallback polling.")
312
+ print("[Hot Reload] Install with: pip install websockets")
313
+ return
314
+
315
+ async def handler(websocket):
316
+ """Handle WebSocket connections."""
317
+ self._clients.add(websocket)
318
+ try:
319
+ # Send initial connection message
320
+ await websocket.send(json.dumps({
321
+ 'type': 'connected',
322
+ 'timestamp': time.time()
323
+ }))
324
+
325
+ # Keep connection alive
326
+ async for message in websocket:
327
+ # Handle ping/pong or other messages
328
+ try:
329
+ data = json.loads(message)
330
+ if data.get('type') == 'ping':
331
+ await websocket.send(json.dumps({'type': 'pong'}))
332
+ except json.JSONDecodeError:
333
+ pass
334
+
335
+ except websockets.exceptions.ConnectionClosed:
336
+ pass
337
+ finally:
338
+ self._clients.discard(websocket)
339
+
340
+ async def main():
341
+ async with websockets.server.serve(handler, self.host, self.port):
342
+ while self._running:
343
+ await asyncio.sleep(0.1)
344
+
345
+ self._loop = asyncio.new_event_loop()
346
+ asyncio.set_event_loop(self._loop)
347
+
348
+ try:
349
+ self._loop.run_until_complete(main())
350
+ except Exception:
351
+ pass
352
+ finally:
353
+ self._loop.close()
354
+
355
+ def broadcast(self, message: dict):
356
+ """
357
+ Broadcast a message to all connected clients.
358
+
359
+ Args:
360
+ message: Message dict to send as JSON
361
+ """
362
+ if not self._clients or not self._loop:
363
+ return
364
+
365
+ async def send_to_all():
366
+ json_msg = json.dumps(message)
367
+ disconnected = set()
368
+
369
+ for client in self._clients.copy():
370
+ try:
371
+ await client.send(json_msg)
372
+ except Exception:
373
+ disconnected.add(client)
374
+
375
+ self._clients -= disconnected
376
+
377
+ try:
378
+ asyncio.run_coroutine_threadsafe(send_to_all(), self._loop)
379
+ except Exception:
380
+ pass
381
+
382
+ @property
383
+ def client_count(self) -> int:
384
+ """Number of connected clients."""
385
+ return len(self._clients)
386
+
387
+
388
+ class HotReloadManager:
389
+ """
390
+ Coordinates file watching and WebSocket broadcasting.
391
+
392
+ Main interface for the hot reload system.
393
+ """
394
+
395
+ def __init__(
396
+ self,
397
+ watch_paths: List[Path] = None,
398
+ extensions: List[str] = None,
399
+ ws_host: str = 'localhost',
400
+ ws_port: int = 35729,
401
+ debounce_ms: int = 100,
402
+ on_reload: Callable[[List[FileChange], ReloadType], None] = None,
403
+ console = None
404
+ ):
405
+ """
406
+ Initialize the hot reload manager.
407
+
408
+ Args:
409
+ watch_paths: Paths to watch for changes
410
+ extensions: File extensions to watch
411
+ ws_host: WebSocket server host
412
+ ws_port: WebSocket server port
413
+ debounce_ms: Debounce delay in milliseconds
414
+ on_reload: Optional callback when reload is triggered
415
+ console: Optional console for logging (from cli.utils)
416
+ """
417
+ self.watch_paths = watch_paths or [Path('.')]
418
+ self.extensions = extensions or ['.q', '.yaml', '.yml', '.css', '.js', '.html']
419
+ self.ws_host = ws_host
420
+ self.ws_port = ws_port
421
+ self.debounce_ms = debounce_ms
422
+ self.on_reload = on_reload
423
+ self.console = console
424
+
425
+ self._watcher: Optional[HotReloadWatcher] = None
426
+ self._server: Optional[HotReloadServer] = None
427
+ self._running = False
428
+ self._reload_count = 0
429
+
430
+ # Error tracking for overlay
431
+ self._last_error: Optional[dict] = None
432
+
433
+ def start(self):
434
+ """Start the hot reload system."""
435
+ if self._running:
436
+ return
437
+
438
+ self._running = True
439
+
440
+ # Start WebSocket server
441
+ self._server = HotReloadServer(self.ws_host, self.ws_port)
442
+ self._server.start()
443
+
444
+ # Start file watcher
445
+ self._watcher = HotReloadWatcher(
446
+ paths=self.watch_paths,
447
+ extensions=self.extensions,
448
+ debounce_ms=self.debounce_ms,
449
+ on_change=self._handle_changes
450
+ )
451
+ self._watcher.start()
452
+
453
+ self._log(f"Hot reload started on ws://{self.ws_host}:{self.ws_port}")
454
+
455
+ def stop(self):
456
+ """Stop the hot reload system."""
457
+ self._running = False
458
+
459
+ if self._watcher:
460
+ self._watcher.stop()
461
+ self._watcher = None
462
+
463
+ if self._server:
464
+ self._server.stop()
465
+ self._server = None
466
+
467
+ self._log("Hot reload stopped")
468
+
469
+ def _handle_changes(self, changes: List[FileChange]):
470
+ """Handle file changes from watcher."""
471
+ if not changes:
472
+ return
473
+
474
+ self._reload_count += 1
475
+
476
+ # Determine reload type
477
+ reload_type = ReloadType.FULL
478
+ css_only = all(c.reload_type == ReloadType.CSS for c in changes)
479
+ if css_only:
480
+ reload_type = ReloadType.CSS
481
+
482
+ # Log changes
483
+ for change in changes:
484
+ rel_path = change.path
485
+ try:
486
+ rel_path = change.path.relative_to(Path.cwd())
487
+ except ValueError:
488
+ pass
489
+ self._log(f"[{datetime.now().strftime('%H:%M:%S')}] {change.change_type}: {rel_path}")
490
+
491
+ # Clear any previous error
492
+ self._last_error = None
493
+
494
+ # Try to parse changed .q files to detect errors
495
+ parse_error = None
496
+ for change in changes:
497
+ if change.extension == '.q' and change.change_type != 'deleted':
498
+ error = self._check_parse_error(change.path)
499
+ if error:
500
+ parse_error = error
501
+ break
502
+
503
+ if parse_error:
504
+ self._last_error = parse_error
505
+ self._broadcast_error(parse_error)
506
+ return
507
+
508
+ # Broadcast reload to clients
509
+ self._broadcast_reload(changes, reload_type)
510
+
511
+ # Call optional callback
512
+ if self.on_reload:
513
+ try:
514
+ self.on_reload(changes, reload_type)
515
+ except Exception as e:
516
+ self._log(f"Error in reload callback: {e}")
517
+
518
+ def _check_parse_error(self, path: Path) -> Optional[dict]:
519
+ """Check if a .q file has parse errors."""
520
+ try:
521
+ import sys
522
+ from quantum.core.parser import QuantumParser, QuantumParseError
523
+
524
+ parser = QuantumParser()
525
+ parser.parse_file(str(path))
526
+ return None
527
+
528
+ except QuantumParseError as e:
529
+ return {
530
+ 'type': 'parse_error',
531
+ 'file': str(path),
532
+ 'message': str(e),
533
+ 'line': getattr(e, 'line', None),
534
+ 'column': getattr(e, 'column', None)
535
+ }
536
+ except Exception as e:
537
+ return {
538
+ 'type': 'error',
539
+ 'file': str(path),
540
+ 'message': str(e)
541
+ }
542
+
543
+ def _broadcast_reload(self, changes: List[FileChange], reload_type: ReloadType):
544
+ """Broadcast reload event to connected browsers."""
545
+ if not self._server:
546
+ return
547
+
548
+ message = {
549
+ 'type': 'reload',
550
+ 'reloadType': reload_type.value,
551
+ 'files': [
552
+ {
553
+ 'path': str(c.path),
554
+ 'changeType': c.change_type,
555
+ 'extension': c.extension
556
+ }
557
+ for c in changes
558
+ ],
559
+ 'timestamp': time.time(),
560
+ 'reloadCount': self._reload_count
561
+ }
562
+
563
+ self._server.broadcast(message)
564
+ self._log(f"Reloading... ({reload_type.value})")
565
+
566
+ def _broadcast_error(self, error: dict):
567
+ """Broadcast error event to connected browsers."""
568
+ if not self._server:
569
+ return
570
+
571
+ message = {
572
+ 'type': 'error',
573
+ 'error': error,
574
+ 'timestamp': time.time()
575
+ }
576
+
577
+ self._server.broadcast(message)
578
+ self._log(f"Parse error in {error.get('file', 'unknown')}: {error.get('message', 'Unknown error')}")
579
+
580
+ def clear_error(self):
581
+ """Clear the current error and notify clients."""
582
+ self._last_error = None
583
+ if self._server:
584
+ self._server.broadcast({
585
+ 'type': 'clear_error',
586
+ 'timestamp': time.time()
587
+ })
588
+
589
+ def force_reload(self):
590
+ """Force a full page reload on all clients."""
591
+ if self._server:
592
+ self._reload_count += 1
593
+ self._server.broadcast({
594
+ 'type': 'reload',
595
+ 'reloadType': 'full',
596
+ 'files': [],
597
+ 'timestamp': time.time(),
598
+ 'reloadCount': self._reload_count,
599
+ 'forced': True
600
+ })
601
+ self._log("Forced reload")
602
+
603
+ def _log(self, message: str):
604
+ """Log a message."""
605
+ if self.console:
606
+ self.console.info(message)
607
+ else:
608
+ print(f"[Hot Reload] {message}")
609
+
610
+ @property
611
+ def ws_url(self) -> str:
612
+ """WebSocket URL for client connection."""
613
+ return f"ws://{self.ws_host}:{self.ws_port}"
614
+
615
+ @property
616
+ def client_count(self) -> int:
617
+ """Number of connected browser clients."""
618
+ return self._server.client_count if self._server else 0
619
+
620
+
621
+ def get_hot_reload_client_script(ws_port: int = 35729, ws_host: str = 'localhost') -> str:
622
+ """
623
+ Get the hot reload client JavaScript code.
624
+
625
+ Args:
626
+ ws_port: WebSocket server port
627
+ ws_host: WebSocket server host
628
+
629
+ Returns:
630
+ JavaScript code to inject into HTML pages
631
+ """
632
+ return f"""
633
+ <!-- Quantum Hot Reload Client -->
634
+ <script>
635
+ (function() {{
636
+ 'use strict';
637
+
638
+ var WS_URL = 'ws://{ws_host}:{ws_port}';
639
+ var RECONNECT_INTERVAL = 1000;
640
+ var MAX_RECONNECT_ATTEMPTS = 30;
641
+
642
+ var socket = null;
643
+ var reconnectAttempts = 0;
644
+ var overlay = null;
645
+ var isConnected = false;
646
+
647
+ // State preservation
648
+ var preservedState = {{}};
649
+
650
+ function log(msg) {{
651
+ console.log('[Hot Reload] ' + msg);
652
+ }}
653
+
654
+ function connect() {{
655
+ if (socket && socket.readyState === WebSocket.OPEN) {{
656
+ return;
657
+ }}
658
+
659
+ try {{
660
+ socket = new WebSocket(WS_URL);
661
+
662
+ socket.onopen = function() {{
663
+ log('Connected to dev server');
664
+ reconnectAttempts = 0;
665
+ isConnected = true;
666
+ hideOverlay();
667
+ showToast('Hot reload connected', 'success');
668
+ }};
669
+
670
+ socket.onclose = function() {{
671
+ isConnected = false;
672
+ log('Disconnected from dev server');
673
+ scheduleReconnect();
674
+ }};
675
+
676
+ socket.onerror = function(err) {{
677
+ log('WebSocket error');
678
+ }};
679
+
680
+ socket.onmessage = function(event) {{
681
+ try {{
682
+ var data = JSON.parse(event.data);
683
+ handleMessage(data);
684
+ }} catch (e) {{
685
+ log('Error parsing message: ' + e);
686
+ }}
687
+ }};
688
+
689
+ }} catch (e) {{
690
+ log('Failed to connect: ' + e);
691
+ scheduleReconnect();
692
+ }}
693
+ }}
694
+
695
+ function scheduleReconnect() {{
696
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {{
697
+ log('Max reconnect attempts reached');
698
+ showOverlay({{
699
+ type: 'connection_lost',
700
+ message: 'Lost connection to dev server. Please restart the server.'
701
+ }});
702
+ return;
703
+ }}
704
+
705
+ reconnectAttempts++;
706
+ setTimeout(connect, RECONNECT_INTERVAL);
707
+ }}
708
+
709
+ function handleMessage(data) {{
710
+ switch (data.type) {{
711
+ case 'connected':
712
+ log('Server acknowledged connection');
713
+ break;
714
+
715
+ case 'reload':
716
+ handleReload(data);
717
+ break;
718
+
719
+ case 'error':
720
+ handleError(data.error);
721
+ break;
722
+
723
+ case 'clear_error':
724
+ hideOverlay();
725
+ break;
726
+
727
+ case 'pong':
728
+ // Heartbeat response
729
+ break;
730
+ }}
731
+ }}
732
+
733
+ function handleReload(data) {{
734
+ log('Reload triggered: ' + data.reloadType);
735
+
736
+ if (data.reloadType === 'css') {{
737
+ reloadCSS();
738
+ showToast('Styles updated', 'info');
739
+ }} else {{
740
+ // Preserve state before reload
741
+ preserveState();
742
+
743
+ // Full page reload
744
+ showToast('Reloading...', 'info');
745
+ setTimeout(function() {{
746
+ location.reload();
747
+ }}, 100);
748
+ }}
749
+ }}
750
+
751
+ function handleError(error) {{
752
+ log('Error received: ' + error.message);
753
+ showOverlay(error);
754
+ }}
755
+
756
+ function reloadCSS() {{
757
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
758
+ var timestamp = Date.now();
759
+
760
+ links.forEach(function(link) {{
761
+ var href = link.href.split('?')[0];
762
+ link.href = href + '?_hr=' + timestamp;
763
+ }});
764
+
765
+ log('CSS reloaded');
766
+ }}
767
+
768
+ function preserveState() {{
769
+ // Save form values
770
+ var forms = document.querySelectorAll('form');
771
+ forms.forEach(function(form, i) {{
772
+ var formId = form.id || 'form_' + i;
773
+ preservedState[formId] = {{}};
774
+
775
+ var inputs = form.querySelectorAll('input, textarea, select');
776
+ inputs.forEach(function(input) {{
777
+ if (input.name) {{
778
+ if (input.type === 'checkbox' || input.type === 'radio') {{
779
+ preservedState[formId][input.name] = input.checked;
780
+ }} else {{
781
+ preservedState[formId][input.name] = input.value;
782
+ }}
783
+ }}
784
+ }});
785
+ }});
786
+
787
+ // Save scroll position
788
+ preservedState._scroll = {{
789
+ x: window.scrollX,
790
+ y: window.scrollY
791
+ }};
792
+
793
+ // Store in sessionStorage for restoration after reload
794
+ try {{
795
+ sessionStorage.setItem('__quantum_hr_state', JSON.stringify(preservedState));
796
+ }} catch (e) {{
797
+ // Storage might be full or unavailable
798
+ }}
799
+ }}
800
+
801
+ function restoreState() {{
802
+ try {{
803
+ var saved = sessionStorage.getItem('__quantum_hr_state');
804
+ if (!saved) return;
805
+
806
+ var state = JSON.parse(saved);
807
+ sessionStorage.removeItem('__quantum_hr_state');
808
+
809
+ // Restore form values
810
+ Object.keys(state).forEach(function(formId) {{
811
+ if (formId.startsWith('_')) return;
812
+
813
+ var form = document.getElementById(formId) || document.forms[formId.replace('form_', '')];
814
+ if (!form) return;
815
+
816
+ var formState = state[formId];
817
+ Object.keys(formState).forEach(function(name) {{
818
+ var input = form.querySelector('[name="' + name + '"]');
819
+ if (input) {{
820
+ if (input.type === 'checkbox' || input.type === 'radio') {{
821
+ input.checked = formState[name];
822
+ }} else {{
823
+ input.value = formState[name];
824
+ }}
825
+ }}
826
+ }});
827
+ }});
828
+
829
+ // Restore scroll position
830
+ if (state._scroll) {{
831
+ setTimeout(function() {{
832
+ window.scrollTo(state._scroll.x, state._scroll.y);
833
+ }}, 100);
834
+ }}
835
+
836
+ log('State restored');
837
+
838
+ }} catch (e) {{
839
+ log('Error restoring state: ' + e);
840
+ }}
841
+ }}
842
+
843
+ function showOverlay(error) {{
844
+ hideOverlay();
845
+
846
+ overlay = document.createElement('div');
847
+ overlay.id = '__quantum_hr_overlay';
848
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:999999;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,sans-serif;';
849
+
850
+ var content = document.createElement('div');
851
+ content.style.cssText = 'max-width:800px;padding:40px;background:#1e1e1e;border-radius:8px;color:#fff;box-shadow:0 4px 24px rgba(0,0,0,0.5);';
852
+
853
+ var title = error.type === 'connection_lost' ? 'Connection Lost' : 'Parse Error';
854
+ var titleColor = error.type === 'connection_lost' ? '#ffc107' : '#f44336';
855
+
856
+ content.innerHTML = '<h2 style="color:' + titleColor + ';margin:0 0 16px 0;display:flex;align-items:center;gap:8px;">' +
857
+ '<span style="font-size:24px;">' + (error.type === 'connection_lost' ? '&#x26A0;' : '&#x2717;') + '</span>' +
858
+ title +
859
+ '</h2>' +
860
+ (error.file ? '<p style="color:#888;margin:0 0 12px 0;font-size:14px;">File: ' + error.file + '</p>' : '') +
861
+ '<pre style="background:#2d2d2d;padding:16px;border-radius:4px;overflow:auto;max-height:400px;margin:0;font-size:14px;line-height:1.5;color:#ff6b6b;">' +
862
+ escapeHtml(error.message) +
863
+ '</pre>' +
864
+ '<p style="color:#888;margin:16px 0 0 0;font-size:13px;">Fix the error and save the file to reload automatically.</p>';
865
+
866
+ overlay.appendChild(content);
867
+ document.body.appendChild(overlay);
868
+
869
+ // Click outside to dismiss (but error will reappear on next change)
870
+ overlay.addEventListener('click', function(e) {{
871
+ if (e.target === overlay) {{
872
+ hideOverlay();
873
+ }}
874
+ }});
875
+ }}
876
+
877
+ function hideOverlay() {{
878
+ if (overlay && overlay.parentNode) {{
879
+ overlay.parentNode.removeChild(overlay);
880
+ }}
881
+ overlay = null;
882
+ }}
883
+
884
+ function showToast(message, type) {{
885
+ var existing = document.getElementById('__quantum_hr_toast');
886
+ if (existing) {{
887
+ existing.parentNode.removeChild(existing);
888
+ }}
889
+
890
+ var toast = document.createElement('div');
891
+ toast.id = '__quantum_hr_toast';
892
+
893
+ var bgColor = type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3';
894
+
895
+ toast.style.cssText = 'position:fixed;bottom:20px;right:20px;padding:12px 20px;background:' + bgColor + ';color:#fff;border-radius:4px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:14px;z-index:999998;box-shadow:0 2px 8px rgba(0,0,0,0.2);animation:__qhr_fadein 0.3s ease;';
896
+ toast.textContent = message;
897
+
898
+ // Add animation keyframes if not present
899
+ if (!document.getElementById('__quantum_hr_styles')) {{
900
+ var style = document.createElement('style');
901
+ style.id = '__quantum_hr_styles';
902
+ style.textContent = '@keyframes __qhr_fadein {{ from {{ opacity:0;transform:translateY(10px); }} to {{ opacity:1;transform:translateY(0); }} }}';
903
+ document.head.appendChild(style);
904
+ }}
905
+
906
+ document.body.appendChild(toast);
907
+
908
+ setTimeout(function() {{
909
+ if (toast.parentNode) {{
910
+ toast.style.opacity = '0';
911
+ toast.style.transition = 'opacity 0.3s ease';
912
+ setTimeout(function() {{
913
+ if (toast.parentNode) {{
914
+ toast.parentNode.removeChild(toast);
915
+ }}
916
+ }}, 300);
917
+ }}
918
+ }}, 2000);
919
+ }}
920
+
921
+ function escapeHtml(text) {{
922
+ var div = document.createElement('div');
923
+ div.textContent = text;
924
+ return div.innerHTML;
925
+ }}
926
+
927
+ // Heartbeat to keep connection alive
928
+ setInterval(function() {{
929
+ if (socket && socket.readyState === WebSocket.OPEN) {{
930
+ socket.send(JSON.stringify({{ type: 'ping' }}));
931
+ }}
932
+ }}, 30000);
933
+
934
+ // Initialize
935
+ if (document.readyState === 'loading') {{
936
+ document.addEventListener('DOMContentLoaded', function() {{
937
+ restoreState();
938
+ connect();
939
+ }});
940
+ }} else {{
941
+ restoreState();
942
+ connect();
943
+ }}
944
+
945
+ }})();
946
+ </script>
947
+ """
948
+
949
+
950
+ # Export client script as a constant for easy access
951
+ HOT_RELOAD_CLIENT_SCRIPT = get_hot_reload_client_script()