Coverage for openhcs/pyqt_gui/services/async_service_bridge.py: 0.0%

104 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1""" 

2Async Service Bridge 

3 

4Bridges async OpenHCS services to PyQt6 threading model, 

5converting async/await patterns to Qt signals/slots. 

6""" 

7 

8import logging 

9from typing import Any, Callable, Optional 

10import asyncio 

11 

12from PyQt6.QtCore import QObject, QThread, pyqtSignal 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17class AsyncServiceBridge(QObject): 

18 """ 

19 Bridge for converting async service operations to Qt threading model. 

20  

21 Handles the conversion of async/await patterns used in OpenHCS services 

22 to Qt's signal/slot threading model. 

23 """ 

24 

25 # Signals for async operation results 

26 operation_completed = pyqtSignal(object) # result 

27 operation_failed = pyqtSignal(str) # error_message 

28 operation_progress = pyqtSignal(int) # progress_percentage 

29 

30 def __init__(self, service_adapter): 

31 """ 

32 Initialize async service bridge. 

33  

34 Args: 

35 service_adapter: PyQtServiceAdapter instance 

36 """ 

37 super().__init__() 

38 self.service_adapter = service_adapter 

39 self.active_threads = [] 

40 

41 def execute_async_operation(self, async_func: Callable, *args, **kwargs) -> None: 

42 """ 

43 Execute async operation in Qt thread. 

44  

45 Args: 

46 async_func: Async function to execute 

47 *args: Function arguments 

48 **kwargs: Function keyword arguments 

49 """ 

50 thread = AsyncOperationThread(async_func, *args, **kwargs) 

51 

52 # Connect thread signals 

53 thread.result_ready.connect(self.operation_completed.emit) 

54 thread.error_occurred.connect(self.operation_failed.emit) 

55 thread.finished.connect(lambda: self._cleanup_thread(thread)) 

56 

57 # Track active thread 

58 self.active_threads.append(thread) 

59 

60 # Start thread 

61 thread.start() 

62 

63 def _cleanup_thread(self, thread: QThread) -> None: 

64 """ 

65 Clean up completed thread. 

66  

67 Args: 

68 thread: Completed thread to clean up 

69 """ 

70 if thread in self.active_threads: 

71 self.active_threads.remove(thread) 

72 thread.deleteLater() 

73 

74 def wait_for_all_operations(self, timeout_ms: int = 30000) -> bool: 

75 """ 

76 Wait for all active async operations to complete. 

77  

78 Args: 

79 timeout_ms: Timeout in milliseconds 

80  

81 Returns: 

82 True if all operations completed, False if timeout 

83 """ 

84 for thread in self.active_threads[:]: # Copy list to avoid modification during iteration 

85 if not thread.wait(timeout_ms): 

86 logger.warning(f"Async operation timed out after {timeout_ms}ms") 

87 return False 

88 return True 

89 

90 

91class AsyncOperationThread(QThread): 

92 """ 

93 Thread for executing async operations. 

94  

95 Converts async/await patterns to Qt threading model. 

96 """ 

97 

98 result_ready = pyqtSignal(object) 

99 error_occurred = pyqtSignal(str) 

100 progress_updated = pyqtSignal(int) 

101 

102 def __init__(self, async_func: Callable, *args, **kwargs): 

103 super().__init__() 

104 self.async_func = async_func 

105 self.args = args 

106 self.kwargs = kwargs 

107 

108 def run(self): 

109 """Execute async function in thread with new event loop.""" 

110 try: 

111 # Create new event loop for this thread 

112 loop = asyncio.new_event_loop() 

113 asyncio.set_event_loop(loop) 

114 

115 try: 

116 # Execute async function 

117 result = loop.run_until_complete( 

118 self.async_func(*self.args, **self.kwargs) 

119 ) 

120 self.result_ready.emit(result) 

121 

122 finally: 

123 # Clean up event loop 

124 loop.close() 

125 

126 except Exception as e: 

127 logger.error(f"Async operation failed: {e}") 

128 self.error_occurred.emit(str(e)) 

129 

130 

131class PatternFileServiceBridge: 

132 """ 

133 Bridge for PatternFileService async operations. 

134  

135 Adapts PatternFileService to work with PyQt6 service adapter. 

136 """ 

137 

138 def __init__(self, service_adapter): 

139 """ 

140 Initialize pattern file service bridge. 

141  

142 Args: 

143 service_adapter: PyQtServiceAdapter instance 

144 """ 

145 self.service_adapter = service_adapter 

146 self.async_bridge = AsyncServiceBridge(service_adapter) 

147 

148 # Import and adapt the original service 

149 from openhcs.textual_tui.services.pattern_file_service import PatternFileService 

150 self.original_service = PatternFileService(service_adapter) 

151 

152 def load_pattern_from_file(self, file_path, callback: Callable = None): 

153 """ 

154 Load pattern from file using Qt threading. 

155  

156 Args: 

157 file_path: Path to pattern file 

158 callback: Optional callback for result 

159 """ 

160 if callback: 

161 self.async_bridge.operation_completed.connect(callback) 

162 self.async_bridge.operation_failed.connect( 

163 lambda error: self.service_adapter.show_error_dialog(f"Load failed: {error}") 

164 ) 

165 

166 self.async_bridge.execute_async_operation( 

167 self.original_service.load_pattern_from_file, 

168 file_path 

169 ) 

170 

171 def save_pattern_to_file(self, pattern, file_path, callback: Callable = None): 

172 """ 

173 Save pattern to file using Qt threading. 

174  

175 Args: 

176 pattern: Pattern to save 

177 file_path: Path to save to 

178 callback: Optional callback for completion 

179 """ 

180 if callback: 

181 self.async_bridge.operation_completed.connect(callback) 

182 self.async_bridge.operation_failed.connect( 

183 lambda error: self.service_adapter.show_error_dialog(f"Save failed: {error}") 

184 ) 

185 

186 self.async_bridge.execute_async_operation( 

187 self.original_service.save_pattern_to_file, 

188 pattern, 

189 file_path 

190 ) 

191 

192 

193class ExternalEditorServiceBridge: 

194 """ 

195 Bridge for ExternalEditorService with PyQt6 integration. 

196  

197 Replaces prompt_toolkit dependencies with Qt equivalents. 

198 """ 

199 

200 def __init__(self, service_adapter): 

201 """ 

202 Initialize external editor service bridge. 

203  

204 Args: 

205 service_adapter: PyQtServiceAdapter instance 

206 """ 

207 self.service_adapter = service_adapter 

208 self.async_bridge = AsyncServiceBridge(service_adapter) 

209 

210 def edit_pattern_in_external_editor(self, initial_content: str, callback: Callable = None): 

211 """ 

212 Edit pattern in external editor using Qt process management. 

213  

214 Args: 

215 initial_content: Initial content for editor 

216 callback: Optional callback for result 

217 """ 

218 if callback: 

219 self.async_bridge.operation_completed.connect(callback) 

220 self.async_bridge.operation_failed.connect( 

221 lambda error: self.service_adapter.show_error_dialog(f"Editor failed: {error}") 

222 ) 

223 

224 # Use Qt-based external editor implementation 

225 self.async_bridge.execute_async_operation( 

226 self._qt_external_editor_operation, 

227 initial_content 

228 ) 

229 

230 async def _qt_external_editor_operation(self, initial_content: str): 

231 """ 

232 Qt-based external editor operation. 

233  

234 Args: 

235 initial_content: Initial content for editor 

236  

237 Returns: 

238 Tuple of (success, pattern, error_message) 

239 """ 

240 import tempfile 

241 import os 

242 from pathlib import Path 

243 

244 # Create temporary file 

245 with tempfile.NamedTemporaryFile(delete=False, mode='w+', suffix='.py', encoding='utf-8') as tmp_file: 

246 tmp_file.write(initial_content) 

247 tmp_file_path = Path(tmp_file.name) 

248 

249 try: 

250 # Get editor command 

251 editor = os.environ.get('EDITOR', 'vim') 

252 command = f"{editor} {tmp_file_path}" 

253 

254 # Use service adapter to run command 

255 success = self.service_adapter.run_system_command(command, wait_for_finish=True) 

256 

257 if not success: 

258 return False, None, "Editor command failed" 

259 

260 # Read modified content 

261 with open(tmp_file_path, "r", encoding="utf-8") as f: 

262 modified_content = f.read() 

263 

264 # Validate content (simplified validation) 

265 try: 

266 import ast 

267 tree = ast.parse(modified_content) 

268 

269 # Extract pattern assignment 

270 for node in tree.body: 

271 if isinstance(node, ast.Assign) and len(node.targets) == 1: 

272 target = node.targets[0] 

273 if isinstance(target, ast.Name) and target.id == 'pattern': 

274 pattern = ast.literal_eval(ast.unparse(node.value)) 

275 return True, pattern, None 

276 

277 return False, None, "No valid pattern assignment found" 

278 

279 except Exception as e: 

280 return False, None, f"Pattern validation failed: {e}" 

281 

282 finally: 

283 # Clean up temporary file 

284 if tmp_file_path.exists(): 

285 tmp_file_path.unlink()