pomera-ai-commander 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +680 -0
- package/bin/pomera-ai-commander.js +62 -0
- package/core/__init__.py +66 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/app_context.py +482 -0
- package/core/async_text_processor.py +422 -0
- package/core/backup_manager.py +656 -0
- package/core/backup_recovery_manager.py +1034 -0
- package/core/content_hash_cache.py +509 -0
- package/core/context_menu.py +313 -0
- package/core/data_validator.py +1067 -0
- package/core/database_connection_manager.py +745 -0
- package/core/database_curl_settings_manager.py +609 -0
- package/core/database_promera_ai_settings_manager.py +447 -0
- package/core/database_schema.py +412 -0
- package/core/database_schema_manager.py +396 -0
- package/core/database_settings_manager.py +1508 -0
- package/core/database_settings_manager_interface.py +457 -0
- package/core/dialog_manager.py +735 -0
- package/core/efficient_line_numbers.py +511 -0
- package/core/error_handler.py +747 -0
- package/core/error_service.py +431 -0
- package/core/event_consolidator.py +512 -0
- package/core/mcp/__init__.py +43 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/core/mcp/protocol.py +288 -0
- package/core/mcp/schema.py +251 -0
- package/core/mcp/server_stdio.py +299 -0
- package/core/mcp/tool_registry.py +2345 -0
- package/core/memory_efficient_text_widget.py +712 -0
- package/core/migration_manager.py +915 -0
- package/core/migration_test_suite.py +1086 -0
- package/core/migration_validator.py +1144 -0
- package/core/optimized_find_replace.py +715 -0
- package/core/optimized_pattern_engine.py +424 -0
- package/core/optimized_search_highlighter.py +553 -0
- package/core/performance_monitor.py +675 -0
- package/core/persistence_manager.py +713 -0
- package/core/progressive_stats_calculator.py +632 -0
- package/core/regex_pattern_cache.py +530 -0
- package/core/regex_pattern_library.py +351 -0
- package/core/search_operation_manager.py +435 -0
- package/core/settings_defaults_registry.py +1087 -0
- package/core/settings_integrity_validator.py +1112 -0
- package/core/settings_serializer.py +558 -0
- package/core/settings_validator.py +1824 -0
- package/core/smart_stats_calculator.py +710 -0
- package/core/statistics_update_manager.py +619 -0
- package/core/stats_config_manager.py +858 -0
- package/core/streaming_text_handler.py +723 -0
- package/core/task_scheduler.py +596 -0
- package/core/update_pattern_library.py +169 -0
- package/core/visibility_monitor.py +596 -0
- package/core/widget_cache.py +498 -0
- package/mcp.json +61 -0
- package/package.json +57 -0
- package/pomera.py +7483 -0
- package/pomera_mcp_server.py +144 -0
- package/tools/__init__.py +5 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
- package/tools/ai_tools.py +2892 -0
- package/tools/ascii_art_generator.py +353 -0
- package/tools/base64_tools.py +184 -0
- package/tools/base_tool.py +511 -0
- package/tools/case_tool.py +309 -0
- package/tools/column_tools.py +396 -0
- package/tools/cron_tool.py +885 -0
- package/tools/curl_history.py +601 -0
- package/tools/curl_processor.py +1208 -0
- package/tools/curl_settings.py +503 -0
- package/tools/curl_tool.py +5467 -0
- package/tools/diff_viewer.py +1072 -0
- package/tools/email_extraction_tool.py +249 -0
- package/tools/email_header_analyzer.py +426 -0
- package/tools/extraction_tools.py +250 -0
- package/tools/find_replace.py +1751 -0
- package/tools/folder_file_reporter.py +1463 -0
- package/tools/folder_file_reporter_adapter.py +480 -0
- package/tools/generator_tools.py +1217 -0
- package/tools/hash_generator.py +256 -0
- package/tools/html_tool.py +657 -0
- package/tools/huggingface_helper.py +449 -0
- package/tools/jsonxml_tool.py +730 -0
- package/tools/line_tools.py +419 -0
- package/tools/list_comparator.py +720 -0
- package/tools/markdown_tools.py +562 -0
- package/tools/mcp_widget.py +1417 -0
- package/tools/notes_widget.py +973 -0
- package/tools/number_base_converter.py +373 -0
- package/tools/regex_extractor.py +572 -0
- package/tools/slug_generator.py +311 -0
- package/tools/sorter_tools.py +459 -0
- package/tools/string_escape_tool.py +393 -0
- package/tools/text_statistics_tool.py +366 -0
- package/tools/text_wrapper.py +431 -0
- package/tools/timestamp_converter.py +422 -0
- package/tools/tool_loader.py +710 -0
- package/tools/translator_tools.py +523 -0
- package/tools/url_link_extractor.py +262 -0
- package/tools/url_parser.py +205 -0
- package/tools/whitespace_tools.py +356 -0
- package/tools/word_frequency_counter.py +147 -0
|
@@ -0,0 +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()
|
|
435
|
+
_global_operation_manager = None
|