pomera-ai-commander 0.1.0 → 1.2.1

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 (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,435 +1,435 @@
1
- #!/usr/bin/env python3
2
- """
3
- Search operation manager with cancellation, timeout handling, and resource management.
4
- """
5
-
6
- import tkinter as tk
7
- import threading
8
- import time
9
- import weakref
10
- from typing import Dict, List, Optional, Callable, Any, Set
11
- from dataclasses import dataclass, field
12
- from enum import Enum
13
- import uuid
14
-
15
- class CancellationReason(Enum):
16
- """Reasons for operation cancellation."""
17
- USER_REQUESTED = "user_requested"
18
- TIMEOUT = "timeout"
19
- RESOURCE_LIMIT = "resource_limit"
20
- WIDGET_DESTROYED = "widget_destroyed"
21
- SYSTEM_SHUTDOWN = "system_shutdown"
22
- ERROR = "error"
23
-
24
- class OperationStatus(Enum):
25
- """Status of search operations."""
26
- PENDING = "pending"
27
- RUNNING = "running"
28
- COMPLETED = "completed"
29
- CANCELLED = "cancelled"
30
- FAILED = "failed"
31
-
32
- @dataclass
33
- class OperationTimeout:
34
- """Timeout configuration for operations."""
35
- search_timeout: float = 30.0 # seconds
36
- highlight_timeout: float = 60.0 # seconds
37
- replace_timeout: float = 120.0 # seconds
38
- preview_timeout: float = 15.0 # seconds
39
-
40
- @dataclass
41
- class ResourceLimits:
42
- """Resource limits for operations."""
43
- max_concurrent_operations: int = 5
44
- max_operations_per_widget: int = 3
45
- max_memory_mb: float = 100.0
46
- max_matches_per_operation: int = 10000
47
-
48
-
49
-
50
- @dataclass
51
- class ManagedOperation:
52
- """Represents a managed search operation."""
53
- operation_id: str
54
- operation_type: str
55
- text_widget_ref: Optional[weakref.ref] = None
56
- status: OperationStatus = OperationStatus.PENDING
57
- cancellation_reason: Optional[CancellationReason] = None
58
-
59
- # Operation parameters
60
- pattern: str = ""
61
- replacement: str = ""
62
- case_sensitive: bool = True
63
- whole_words: bool = False
64
- use_regex: bool = False
65
-
66
- # Control
67
- cancel_event: threading.Event = field(default_factory=threading.Event)
68
- completion_event: threading.Event = field(default_factory=threading.Event)
69
-
70
- # Callbacks
71
- progress_callback: Optional[Callable] = None
72
- completion_callback: Optional[Callable] = None
73
- error_callback: Optional[Callable] = None
74
-
75
- # Results
76
- results: Dict[str, Any] = field(default_factory=dict)
77
- error_message: Optional[str] = None
78
-
79
- def is_cancelled(self) -> bool:
80
- """Check if operation is cancelled."""
81
- return self.cancel_event.is_set()
82
-
83
- def cancel(self, reason: CancellationReason):
84
- """Cancel the operation with given reason."""
85
- self.cancellation_reason = reason
86
- self.status = OperationStatus.CANCELLED
87
- self.cancel_event.set()
88
- self.completion_event.set()
89
-
90
- def complete(self, results: Optional[Dict[str, Any]] = None):
91
- """Mark operation as completed."""
92
- if results:
93
- self.results.update(results)
94
- self.status = OperationStatus.COMPLETED
95
- self.completion_event.set()
96
-
97
- def fail(self, error_message: str):
98
- """Mark operation as failed."""
99
- self.error_message = error_message
100
- self.status = OperationStatus.FAILED
101
- self.completion_event.set()
102
-
103
- def get_text_widget(self) -> Optional[tk.Text]:
104
- """Get the text widget if it still exists."""
105
- return self.text_widget_ref() if self.text_widget_ref else None
106
-
107
- class SearchOperationManager:
108
- """
109
- Manages search operations with cancellation, timeout handling,
110
- and resource management for optimal performance.
111
- """
112
-
113
- def __init__(self,
114
- timeouts: Optional[OperationTimeout] = None,
115
- limits: Optional[ResourceLimits] = None):
116
-
117
- self.timeouts = timeouts or OperationTimeout()
118
- self.limits = limits or ResourceLimits()
119
-
120
- # Operation tracking
121
- self.operations: Dict[str, ManagedOperation] = {}
122
- self.operations_lock = threading.RLock()
123
-
124
- # Widget-specific operation tracking
125
- self.widget_operations: Dict[int, List[str]] = {} # widget_id -> operation_ids
126
-
127
- # Timeout monitoring
128
- self.timeout_monitor_thread = None
129
- self.shutdown_event = threading.Event()
130
- self._start_timeout_monitor()
131
-
132
- def _start_timeout_monitor(self):
133
- """Start the timeout monitoring thread."""
134
- if self.timeout_monitor_thread is None or not self.timeout_monitor_thread.is_alive():
135
- self.timeout_monitor_thread = threading.Thread(
136
- target=self._timeout_monitor_loop,
137
- daemon=True,
138
- name="SearchOperationTimeout"
139
- )
140
- self.timeout_monitor_thread.start()
141
-
142
- def _timeout_monitor_loop(self):
143
- """Monitor operations for timeouts."""
144
- while not self.shutdown_event.is_set():
145
- try:
146
- current_time = time.time()
147
- operations_to_cancel = []
148
-
149
- with self.operations_lock:
150
- for op_id, operation in self.operations.items():
151
- if operation.status not in [OperationStatus.RUNNING, OperationStatus.PENDING]:
152
- continue
153
-
154
- # Check timeout based on operation type
155
- timeout = self._get_timeout_for_operation(operation.operation_type)
156
- if current_time - operation.metrics.start_time > timeout:
157
- operations_to_cancel.append((op_id, operation))
158
-
159
- # Cancel timed out operations
160
- for op_id, operation in operations_to_cancel:
161
- self._cancel_operation_internal(operation, CancellationReason.TIMEOUT)
162
-
163
- # Sleep for a short interval
164
- time.sleep(1.0)
165
-
166
- except Exception as e:
167
- print(f"Error in timeout monitor: {e}")
168
- time.sleep(1.0)
169
-
170
- def _get_timeout_for_operation(self, operation_type: str) -> float:
171
- """Get timeout value for operation type."""
172
- timeout_map = {
173
- 'search': self.timeouts.search_timeout,
174
- 'highlight': self.timeouts.highlight_timeout,
175
- 'replace': self.timeouts.replace_timeout,
176
- 'preview': self.timeouts.preview_timeout
177
- }
178
- return timeout_map.get(operation_type, self.timeouts.search_timeout)
179
-
180
- def create_operation(self,
181
- operation_type: str,
182
- text_widget: tk.Text,
183
- pattern: str,
184
- replacement: str = "",
185
- case_sensitive: bool = True,
186
- whole_words: bool = False,
187
- use_regex: bool = False,
188
- progress_callback: Optional[Callable] = None,
189
- completion_callback: Optional[Callable] = None,
190
- error_callback: Optional[Callable] = None) -> Optional[str]:
191
- """
192
- Create a new managed search operation.
193
-
194
- Returns:
195
- Operation ID if created successfully, None if rejected due to limits
196
- """
197
- with self.operations_lock:
198
- # Check resource limits
199
- if not self._can_create_operation(text_widget):
200
- return None
201
-
202
- # Generate unique operation ID
203
- operation_id = str(uuid.uuid4())
204
-
205
- # Create operation
206
- operation = ManagedOperation(
207
- operation_id=operation_id,
208
- operation_type=operation_type,
209
- text_widget_ref=weakref.ref(text_widget),
210
- pattern=pattern,
211
- replacement=replacement,
212
- case_sensitive=case_sensitive,
213
- whole_words=whole_words,
214
- use_regex=use_regex,
215
- progress_callback=progress_callback,
216
- completion_callback=completion_callback,
217
- error_callback=error_callback
218
- )
219
-
220
- # Track operation
221
- self.operations[operation_id] = operation
222
-
223
- # Track by widget
224
- widget_id = id(text_widget)
225
- if widget_id not in self.widget_operations:
226
- self.widget_operations[widget_id] = []
227
- self.widget_operations[widget_id].append(operation_id)
228
-
229
- return operation_id
230
-
231
- def _can_create_operation(self, text_widget: tk.Text) -> bool:
232
- """Check if a new operation can be created based on resource limits."""
233
- # Check total concurrent operations
234
- active_count = sum(1 for op in self.operations.values()
235
- if op.status in [OperationStatus.PENDING, OperationStatus.RUNNING])
236
-
237
- if active_count >= self.limits.max_concurrent_operations:
238
- return False
239
-
240
- # Check operations per widget
241
- widget_id = id(text_widget)
242
- if widget_id in self.widget_operations:
243
- widget_active_count = sum(1 for op_id in self.widget_operations[widget_id]
244
- if op_id in self.operations and
245
- self.operations[op_id].status in [OperationStatus.PENDING, OperationStatus.RUNNING])
246
-
247
- if widget_active_count >= self.limits.max_operations_per_widget:
248
- return False
249
-
250
- return True
251
-
252
- def start_operation(self, operation_id: str) -> bool:
253
- """Start a pending operation."""
254
- with self.operations_lock:
255
- if operation_id not in self.operations:
256
- return False
257
-
258
- operation = self.operations[operation_id]
259
- if operation.status != OperationStatus.PENDING:
260
- return False
261
-
262
- # Check if widget still exists
263
- if operation.get_text_widget() is None:
264
- self._cancel_operation_internal(operation, CancellationReason.WIDGET_DESTROYED)
265
- return False
266
-
267
- operation.status = OperationStatus.RUNNING
268
- return True
269
-
270
- def cancel_operation(self, operation_id: str, reason: CancellationReason = CancellationReason.USER_REQUESTED) -> bool:
271
- """Cancel a specific operation."""
272
- with self.operations_lock:
273
- if operation_id not in self.operations:
274
- return False
275
-
276
- operation = self.operations[operation_id]
277
- self._cancel_operation_internal(operation, reason)
278
- return True
279
-
280
- def _cancel_operation_internal(self, operation: ManagedOperation, reason: CancellationReason):
281
- """Internal method to cancel an operation."""
282
- if operation.status in [OperationStatus.COMPLETED, OperationStatus.CANCELLED, OperationStatus.FAILED]:
283
- return
284
-
285
- operation.cancel(reason)
286
-
287
- # Call error callback if provided
288
- if operation.error_callback:
289
- try:
290
- operation.error_callback(operation, f"Operation cancelled: {reason.value}")
291
- except Exception as e:
292
- print(f"Error in operation error callback: {e}")
293
-
294
- def cancel_widget_operations(self, text_widget: tk.Text, reason: CancellationReason = CancellationReason.USER_REQUESTED):
295
- """Cancel all operations for a specific widget."""
296
- widget_id = id(text_widget)
297
-
298
- with self.operations_lock:
299
- if widget_id not in self.widget_operations:
300
- return
301
-
302
- for operation_id in self.widget_operations[widget_id][:]: # Copy list to avoid modification during iteration
303
- if operation_id in self.operations:
304
- operation = self.operations[operation_id]
305
- self._cancel_operation_internal(operation, reason)
306
-
307
- def cancel_all_operations(self, reason: CancellationReason = CancellationReason.SYSTEM_SHUTDOWN):
308
- """Cancel all active operations."""
309
- with self.operations_lock:
310
- for operation in list(self.operations.values()):
311
- self._cancel_operation_internal(operation, reason)
312
-
313
- def complete_operation(self, operation_id: str, results: Optional[Dict[str, Any]] = None) -> bool:
314
- """Mark an operation as completed."""
315
- with self.operations_lock:
316
- if operation_id not in self.operations:
317
- return False
318
-
319
- operation = self.operations[operation_id]
320
- if operation.status != OperationStatus.RUNNING:
321
- return False
322
-
323
- operation.complete(results)
324
-
325
- # Call completion callback if provided
326
- if operation.completion_callback:
327
- try:
328
- operation.completion_callback(operation)
329
- except Exception as e:
330
- print(f"Error in operation completion callback: {e}")
331
-
332
- return True
333
-
334
- def fail_operation(self, operation_id: str, error_message: str) -> bool:
335
- """Mark an operation as failed."""
336
- with self.operations_lock:
337
- if operation_id not in self.operations:
338
- return False
339
-
340
- operation = self.operations[operation_id]
341
- if operation.status not in [OperationStatus.PENDING, OperationStatus.RUNNING]:
342
- return False
343
-
344
- operation.fail(error_message)
345
-
346
- # Call error callback if provided
347
- if operation.error_callback:
348
- try:
349
- operation.error_callback(operation, error_message)
350
- except Exception as e:
351
- print(f"Error in operation error callback: {e}")
352
-
353
- return True
354
-
355
- def get_operation(self, operation_id: str) -> Optional[ManagedOperation]:
356
- """Get operation by ID."""
357
- with self.operations_lock:
358
- return self.operations.get(operation_id)
359
-
360
- def get_widget_operations(self, text_widget: tk.Text) -> List[ManagedOperation]:
361
- """Get all operations for a specific widget."""
362
- widget_id = id(text_widget)
363
-
364
- with self.operations_lock:
365
- if widget_id not in self.widget_operations:
366
- return []
367
-
368
- operations = []
369
- for operation_id in self.widget_operations[widget_id]:
370
- if operation_id in self.operations:
371
- operations.append(self.operations[operation_id])
372
-
373
- return operations
374
-
375
- def get_active_operations(self) -> List[ManagedOperation]:
376
- """Get all active (pending or running) operations."""
377
- with self.operations_lock:
378
- return [op for op in self.operations.values()
379
- if op.status in [OperationStatus.PENDING, OperationStatus.RUNNING]]
380
-
381
- def cleanup_completed_operations(self, max_age_seconds: float = 300):
382
- """Clean up old completed operations."""
383
- current_time = time.time()
384
- operations_to_remove = []
385
-
386
- with self.operations_lock:
387
- for operation_id, operation in self.operations.items():
388
- if operation.status in [OperationStatus.COMPLETED, OperationStatus.CANCELLED, OperationStatus.FAILED]:
389
- operations_to_remove.append(operation_id)
390
-
391
- # Remove old operations
392
- for operation_id in operations_to_remove:
393
- operation = self.operations.pop(operation_id, None)
394
- if operation and operation.text_widget_ref:
395
- widget_id = id(operation.get_text_widget()) if operation.get_text_widget() else None
396
- if widget_id and widget_id in self.widget_operations:
397
- try:
398
- self.widget_operations[widget_id].remove(operation_id)
399
- if not self.widget_operations[widget_id]:
400
- del self.widget_operations[widget_id]
401
- except ValueError:
402
- pass
403
-
404
- def wait_for_operation(self, operation_id: str, timeout: Optional[float] = None) -> bool:
405
- """Wait for an operation to complete."""
406
- operation = self.get_operation(operation_id)
407
- if not operation:
408
- return False
409
-
410
- return operation.completion_event.wait(timeout)
411
-
412
- def shutdown(self):
413
- """Shutdown the operation manager."""
414
- self.cancel_all_operations(CancellationReason.SYSTEM_SHUTDOWN)
415
- self.shutdown_event.set()
416
-
417
- if self.timeout_monitor_thread and self.timeout_monitor_thread.is_alive():
418
- self.timeout_monitor_thread.join(timeout=2.0)
419
-
420
- # Global instance
421
- _global_operation_manager = None
422
-
423
- def get_operation_manager() -> SearchOperationManager:
424
- """Get the global search operation manager instance."""
425
- global _global_operation_manager
426
- if _global_operation_manager is None:
427
- _global_operation_manager = SearchOperationManager()
428
- return _global_operation_manager
429
-
430
- def shutdown_operation_manager():
431
- """Shutdown the global operation manager."""
432
- global _global_operation_manager
433
- if _global_operation_manager is not None:
434
- _global_operation_manager.shutdown()
1
+ #!/usr/bin/env python3
2
+ """
3
+ Search operation manager with cancellation, timeout handling, and resource management.
4
+ """
5
+
6
+ import tkinter as tk
7
+ import threading
8
+ import time
9
+ import weakref
10
+ from typing import Dict, List, Optional, Callable, Any, Set
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ import uuid
14
+
15
+ class CancellationReason(Enum):
16
+ """Reasons for operation cancellation."""
17
+ USER_REQUESTED = "user_requested"
18
+ TIMEOUT = "timeout"
19
+ RESOURCE_LIMIT = "resource_limit"
20
+ WIDGET_DESTROYED = "widget_destroyed"
21
+ SYSTEM_SHUTDOWN = "system_shutdown"
22
+ ERROR = "error"
23
+
24
+ class OperationStatus(Enum):
25
+ """Status of search operations."""
26
+ PENDING = "pending"
27
+ RUNNING = "running"
28
+ COMPLETED = "completed"
29
+ CANCELLED = "cancelled"
30
+ FAILED = "failed"
31
+
32
+ @dataclass
33
+ class OperationTimeout:
34
+ """Timeout configuration for operations."""
35
+ search_timeout: float = 30.0 # seconds
36
+ highlight_timeout: float = 60.0 # seconds
37
+ replace_timeout: float = 120.0 # seconds
38
+ preview_timeout: float = 15.0 # seconds
39
+
40
+ @dataclass
41
+ class ResourceLimits:
42
+ """Resource limits for operations."""
43
+ max_concurrent_operations: int = 5
44
+ max_operations_per_widget: int = 3
45
+ max_memory_mb: float = 100.0
46
+ max_matches_per_operation: int = 10000
47
+
48
+
49
+
50
+ @dataclass
51
+ class ManagedOperation:
52
+ """Represents a managed search operation."""
53
+ operation_id: str
54
+ operation_type: str
55
+ text_widget_ref: Optional[weakref.ref] = None
56
+ status: OperationStatus = OperationStatus.PENDING
57
+ cancellation_reason: Optional[CancellationReason] = None
58
+
59
+ # Operation parameters
60
+ pattern: str = ""
61
+ replacement: str = ""
62
+ case_sensitive: bool = True
63
+ whole_words: bool = False
64
+ use_regex: bool = False
65
+
66
+ # Control
67
+ cancel_event: threading.Event = field(default_factory=threading.Event)
68
+ completion_event: threading.Event = field(default_factory=threading.Event)
69
+
70
+ # Callbacks
71
+ progress_callback: Optional[Callable] = None
72
+ completion_callback: Optional[Callable] = None
73
+ error_callback: Optional[Callable] = None
74
+
75
+ # Results
76
+ results: Dict[str, Any] = field(default_factory=dict)
77
+ error_message: Optional[str] = None
78
+
79
+ def is_cancelled(self) -> bool:
80
+ """Check if operation is cancelled."""
81
+ return self.cancel_event.is_set()
82
+
83
+ def cancel(self, reason: CancellationReason):
84
+ """Cancel the operation with given reason."""
85
+ self.cancellation_reason = reason
86
+ self.status = OperationStatus.CANCELLED
87
+ self.cancel_event.set()
88
+ self.completion_event.set()
89
+
90
+ def complete(self, results: Optional[Dict[str, Any]] = None):
91
+ """Mark operation as completed."""
92
+ if results:
93
+ self.results.update(results)
94
+ self.status = OperationStatus.COMPLETED
95
+ self.completion_event.set()
96
+
97
+ def fail(self, error_message: str):
98
+ """Mark operation as failed."""
99
+ self.error_message = error_message
100
+ self.status = OperationStatus.FAILED
101
+ self.completion_event.set()
102
+
103
+ def get_text_widget(self) -> Optional[tk.Text]:
104
+ """Get the text widget if it still exists."""
105
+ return self.text_widget_ref() if self.text_widget_ref else None
106
+
107
+ class SearchOperationManager:
108
+ """
109
+ Manages search operations with cancellation, timeout handling,
110
+ and resource management for optimal performance.
111
+ """
112
+
113
+ def __init__(self,
114
+ timeouts: Optional[OperationTimeout] = None,
115
+ limits: Optional[ResourceLimits] = None):
116
+
117
+ self.timeouts = timeouts or OperationTimeout()
118
+ self.limits = limits or ResourceLimits()
119
+
120
+ # Operation tracking
121
+ self.operations: Dict[str, ManagedOperation] = {}
122
+ self.operations_lock = threading.RLock()
123
+
124
+ # Widget-specific operation tracking
125
+ self.widget_operations: Dict[int, List[str]] = {} # widget_id -> operation_ids
126
+
127
+ # Timeout monitoring
128
+ self.timeout_monitor_thread = None
129
+ self.shutdown_event = threading.Event()
130
+ self._start_timeout_monitor()
131
+
132
+ def _start_timeout_monitor(self):
133
+ """Start the timeout monitoring thread."""
134
+ if self.timeout_monitor_thread is None or not self.timeout_monitor_thread.is_alive():
135
+ self.timeout_monitor_thread = threading.Thread(
136
+ target=self._timeout_monitor_loop,
137
+ daemon=True,
138
+ name="SearchOperationTimeout"
139
+ )
140
+ self.timeout_monitor_thread.start()
141
+
142
+ def _timeout_monitor_loop(self):
143
+ """Monitor operations for timeouts."""
144
+ while not self.shutdown_event.is_set():
145
+ try:
146
+ current_time = time.time()
147
+ operations_to_cancel = []
148
+
149
+ with self.operations_lock:
150
+ for op_id, operation in self.operations.items():
151
+ if operation.status not in [OperationStatus.RUNNING, OperationStatus.PENDING]:
152
+ continue
153
+
154
+ # Check timeout based on operation type
155
+ timeout = self._get_timeout_for_operation(operation.operation_type)
156
+ if current_time - operation.metrics.start_time > timeout:
157
+ operations_to_cancel.append((op_id, operation))
158
+
159
+ # Cancel timed out operations
160
+ for op_id, operation in operations_to_cancel:
161
+ self._cancel_operation_internal(operation, CancellationReason.TIMEOUT)
162
+
163
+ # Sleep for a short interval
164
+ time.sleep(1.0)
165
+
166
+ except Exception as e:
167
+ print(f"Error in timeout monitor: {e}")
168
+ time.sleep(1.0)
169
+
170
+ def _get_timeout_for_operation(self, operation_type: str) -> float:
171
+ """Get timeout value for operation type."""
172
+ timeout_map = {
173
+ 'search': self.timeouts.search_timeout,
174
+ 'highlight': self.timeouts.highlight_timeout,
175
+ 'replace': self.timeouts.replace_timeout,
176
+ 'preview': self.timeouts.preview_timeout
177
+ }
178
+ return timeout_map.get(operation_type, self.timeouts.search_timeout)
179
+
180
+ def create_operation(self,
181
+ operation_type: str,
182
+ text_widget: tk.Text,
183
+ pattern: str,
184
+ replacement: str = "",
185
+ case_sensitive: bool = True,
186
+ whole_words: bool = False,
187
+ use_regex: bool = False,
188
+ progress_callback: Optional[Callable] = None,
189
+ completion_callback: Optional[Callable] = None,
190
+ error_callback: Optional[Callable] = None) -> Optional[str]:
191
+ """
192
+ Create a new managed search operation.
193
+
194
+ Returns:
195
+ Operation ID if created successfully, None if rejected due to limits
196
+ """
197
+ with self.operations_lock:
198
+ # Check resource limits
199
+ if not self._can_create_operation(text_widget):
200
+ return None
201
+
202
+ # Generate unique operation ID
203
+ operation_id = str(uuid.uuid4())
204
+
205
+ # Create operation
206
+ operation = ManagedOperation(
207
+ operation_id=operation_id,
208
+ operation_type=operation_type,
209
+ text_widget_ref=weakref.ref(text_widget),
210
+ pattern=pattern,
211
+ replacement=replacement,
212
+ case_sensitive=case_sensitive,
213
+ whole_words=whole_words,
214
+ use_regex=use_regex,
215
+ progress_callback=progress_callback,
216
+ completion_callback=completion_callback,
217
+ error_callback=error_callback
218
+ )
219
+
220
+ # Track operation
221
+ self.operations[operation_id] = operation
222
+
223
+ # Track by widget
224
+ widget_id = id(text_widget)
225
+ if widget_id not in self.widget_operations:
226
+ self.widget_operations[widget_id] = []
227
+ self.widget_operations[widget_id].append(operation_id)
228
+
229
+ return operation_id
230
+
231
+ def _can_create_operation(self, text_widget: tk.Text) -> bool:
232
+ """Check if a new operation can be created based on resource limits."""
233
+ # Check total concurrent operations
234
+ active_count = sum(1 for op in self.operations.values()
235
+ if op.status in [OperationStatus.PENDING, OperationStatus.RUNNING])
236
+
237
+ if active_count >= self.limits.max_concurrent_operations:
238
+ return False
239
+
240
+ # Check operations per widget
241
+ widget_id = id(text_widget)
242
+ if widget_id in self.widget_operations:
243
+ widget_active_count = sum(1 for op_id in self.widget_operations[widget_id]
244
+ if op_id in self.operations and
245
+ self.operations[op_id].status in [OperationStatus.PENDING, OperationStatus.RUNNING])
246
+
247
+ if widget_active_count >= self.limits.max_operations_per_widget:
248
+ return False
249
+
250
+ return True
251
+
252
+ def start_operation(self, operation_id: str) -> bool:
253
+ """Start a pending operation."""
254
+ with self.operations_lock:
255
+ if operation_id not in self.operations:
256
+ return False
257
+
258
+ operation = self.operations[operation_id]
259
+ if operation.status != OperationStatus.PENDING:
260
+ return False
261
+
262
+ # Check if widget still exists
263
+ if operation.get_text_widget() is None:
264
+ self._cancel_operation_internal(operation, CancellationReason.WIDGET_DESTROYED)
265
+ return False
266
+
267
+ operation.status = OperationStatus.RUNNING
268
+ return True
269
+
270
+ def cancel_operation(self, operation_id: str, reason: CancellationReason = CancellationReason.USER_REQUESTED) -> bool:
271
+ """Cancel a specific operation."""
272
+ with self.operations_lock:
273
+ if operation_id not in self.operations:
274
+ return False
275
+
276
+ operation = self.operations[operation_id]
277
+ self._cancel_operation_internal(operation, reason)
278
+ return True
279
+
280
+ def _cancel_operation_internal(self, operation: ManagedOperation, reason: CancellationReason):
281
+ """Internal method to cancel an operation."""
282
+ if operation.status in [OperationStatus.COMPLETED, OperationStatus.CANCELLED, OperationStatus.FAILED]:
283
+ return
284
+
285
+ operation.cancel(reason)
286
+
287
+ # Call error callback if provided
288
+ if operation.error_callback:
289
+ try:
290
+ operation.error_callback(operation, f"Operation cancelled: {reason.value}")
291
+ except Exception as e:
292
+ print(f"Error in operation error callback: {e}")
293
+
294
+ def cancel_widget_operations(self, text_widget: tk.Text, reason: CancellationReason = CancellationReason.USER_REQUESTED):
295
+ """Cancel all operations for a specific widget."""
296
+ widget_id = id(text_widget)
297
+
298
+ with self.operations_lock:
299
+ if widget_id not in self.widget_operations:
300
+ return
301
+
302
+ for operation_id in self.widget_operations[widget_id][:]: # Copy list to avoid modification during iteration
303
+ if operation_id in self.operations:
304
+ operation = self.operations[operation_id]
305
+ self._cancel_operation_internal(operation, reason)
306
+
307
+ def cancel_all_operations(self, reason: CancellationReason = CancellationReason.SYSTEM_SHUTDOWN):
308
+ """Cancel all active operations."""
309
+ with self.operations_lock:
310
+ for operation in list(self.operations.values()):
311
+ self._cancel_operation_internal(operation, reason)
312
+
313
+ def complete_operation(self, operation_id: str, results: Optional[Dict[str, Any]] = None) -> bool:
314
+ """Mark an operation as completed."""
315
+ with self.operations_lock:
316
+ if operation_id not in self.operations:
317
+ return False
318
+
319
+ operation = self.operations[operation_id]
320
+ if operation.status != OperationStatus.RUNNING:
321
+ return False
322
+
323
+ operation.complete(results)
324
+
325
+ # Call completion callback if provided
326
+ if operation.completion_callback:
327
+ try:
328
+ operation.completion_callback(operation)
329
+ except Exception as e:
330
+ print(f"Error in operation completion callback: {e}")
331
+
332
+ return True
333
+
334
+ def fail_operation(self, operation_id: str, error_message: str) -> bool:
335
+ """Mark an operation as failed."""
336
+ with self.operations_lock:
337
+ if operation_id not in self.operations:
338
+ return False
339
+
340
+ operation = self.operations[operation_id]
341
+ if operation.status not in [OperationStatus.PENDING, OperationStatus.RUNNING]:
342
+ return False
343
+
344
+ operation.fail(error_message)
345
+
346
+ # Call error callback if provided
347
+ if operation.error_callback:
348
+ try:
349
+ operation.error_callback(operation, error_message)
350
+ except Exception as e:
351
+ print(f"Error in operation error callback: {e}")
352
+
353
+ return True
354
+
355
+ def get_operation(self, operation_id: str) -> Optional[ManagedOperation]:
356
+ """Get operation by ID."""
357
+ with self.operations_lock:
358
+ return self.operations.get(operation_id)
359
+
360
+ def get_widget_operations(self, text_widget: tk.Text) -> List[ManagedOperation]:
361
+ """Get all operations for a specific widget."""
362
+ widget_id = id(text_widget)
363
+
364
+ with self.operations_lock:
365
+ if widget_id not in self.widget_operations:
366
+ return []
367
+
368
+ operations = []
369
+ for operation_id in self.widget_operations[widget_id]:
370
+ if operation_id in self.operations:
371
+ operations.append(self.operations[operation_id])
372
+
373
+ return operations
374
+
375
+ def get_active_operations(self) -> List[ManagedOperation]:
376
+ """Get all active (pending or running) operations."""
377
+ with self.operations_lock:
378
+ return [op for op in self.operations.values()
379
+ if op.status in [OperationStatus.PENDING, OperationStatus.RUNNING]]
380
+
381
+ def cleanup_completed_operations(self, max_age_seconds: float = 300):
382
+ """Clean up old completed operations."""
383
+ current_time = time.time()
384
+ operations_to_remove = []
385
+
386
+ with self.operations_lock:
387
+ for operation_id, operation in self.operations.items():
388
+ if operation.status in [OperationStatus.COMPLETED, OperationStatus.CANCELLED, OperationStatus.FAILED]:
389
+ operations_to_remove.append(operation_id)
390
+
391
+ # Remove old operations
392
+ for operation_id in operations_to_remove:
393
+ operation = self.operations.pop(operation_id, None)
394
+ if operation and operation.text_widget_ref:
395
+ widget_id = id(operation.get_text_widget()) if operation.get_text_widget() else None
396
+ if widget_id and widget_id in self.widget_operations:
397
+ try:
398
+ self.widget_operations[widget_id].remove(operation_id)
399
+ if not self.widget_operations[widget_id]:
400
+ del self.widget_operations[widget_id]
401
+ except ValueError:
402
+ pass
403
+
404
+ def wait_for_operation(self, operation_id: str, timeout: Optional[float] = None) -> bool:
405
+ """Wait for an operation to complete."""
406
+ operation = self.get_operation(operation_id)
407
+ if not operation:
408
+ return False
409
+
410
+ return operation.completion_event.wait(timeout)
411
+
412
+ def shutdown(self):
413
+ """Shutdown the operation manager."""
414
+ self.cancel_all_operations(CancellationReason.SYSTEM_SHUTDOWN)
415
+ self.shutdown_event.set()
416
+
417
+ if self.timeout_monitor_thread and self.timeout_monitor_thread.is_alive():
418
+ self.timeout_monitor_thread.join(timeout=2.0)
419
+
420
+ # Global instance
421
+ _global_operation_manager = None
422
+
423
+ def get_operation_manager() -> SearchOperationManager:
424
+ """Get the global search operation manager instance."""
425
+ global _global_operation_manager
426
+ if _global_operation_manager is None:
427
+ _global_operation_manager = SearchOperationManager()
428
+ return _global_operation_manager
429
+
430
+ def shutdown_operation_manager():
431
+ """Shutdown the global operation manager."""
432
+ global _global_operation_manager
433
+ if _global_operation_manager is not None:
434
+ _global_operation_manager.shutdown()
435
435
  _global_operation_manager = None