pysfi 0.1.7__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.
- pysfi-0.1.7.dist-info/METADATA +134 -0
- pysfi-0.1.7.dist-info/RECORD +31 -0
- pysfi-0.1.7.dist-info/WHEEL +4 -0
- pysfi-0.1.7.dist-info/entry_points.txt +15 -0
- sfi/__init__.py +3 -0
- sfi/alarmclock/__init__.py +0 -0
- sfi/alarmclock/alarmclock.py +367 -0
- sfi/bumpversion/__init__.py +3 -0
- sfi/bumpversion/bumpversion.py +535 -0
- sfi/cli.py +11 -0
- sfi/docscan/__init__.py +3 -0
- sfi/docscan/docscan.py +841 -0
- sfi/docscan/docscan_gui.py +596 -0
- sfi/embedinstall/__init__.py +0 -0
- sfi/embedinstall/embedinstall.py +418 -0
- sfi/filedate/__init__.py +0 -0
- sfi/filedate/filedate.py +112 -0
- sfi/makepython/__init__.py +0 -0
- sfi/makepython/makepython.py +326 -0
- sfi/pdfsplit/__init__.py +0 -0
- sfi/pdfsplit/pdfsplit.py +173 -0
- sfi/projectparse/__init__.py +0 -0
- sfi/projectparse/projectparse.py +152 -0
- sfi/pyloadergen/__init__.py +0 -0
- sfi/pyloadergen/pyloadergen.py +995 -0
- sfi/pypacker/__init__.py +0 -0
- sfi/pypacker/fspacker.py +91 -0
- sfi/taskkill/__init__.py +0 -0
- sfi/taskkill/taskkill.py +236 -0
- sfi/which/__init__.py +0 -0
- sfi/which/which.py +74 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""PySide2 GUI version of docscan application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from PySide2.QtCore import QThread, Signal
|
|
13
|
+
from PySide2.QtWidgets import (
|
|
14
|
+
QApplication,
|
|
15
|
+
QCheckBox,
|
|
16
|
+
QFileDialog,
|
|
17
|
+
QGroupBox,
|
|
18
|
+
QHBoxLayout,
|
|
19
|
+
QLabel,
|
|
20
|
+
QLineEdit,
|
|
21
|
+
QMessageBox,
|
|
22
|
+
QProgressBar,
|
|
23
|
+
QPushButton,
|
|
24
|
+
QSpinBox,
|
|
25
|
+
QTableWidget,
|
|
26
|
+
QTableWidgetItem,
|
|
27
|
+
QTabWidget,
|
|
28
|
+
QTextEdit,
|
|
29
|
+
QVBoxLayout,
|
|
30
|
+
QWidget,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Import from docscan module
|
|
34
|
+
try:
|
|
35
|
+
from docscan import DocumentScanner, Rule
|
|
36
|
+
except ImportError:
|
|
37
|
+
from sfi.docscan.docscan import DocumentScanner, Rule
|
|
38
|
+
|
|
39
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ScanWorker(QThread):
|
|
44
|
+
"""Worker thread for running document scan in background."""
|
|
45
|
+
|
|
46
|
+
progress = Signal(str)
|
|
47
|
+
finished = Signal(dict)
|
|
48
|
+
error = Signal(str)
|
|
49
|
+
progress_update = Signal(int, int) # current, total
|
|
50
|
+
|
|
51
|
+
def __init__(self, scanner: DocumentScanner, threads: int):
|
|
52
|
+
"""Initialize worker thread.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
scanner: DocumentScanner instance
|
|
56
|
+
threads: Number of worker threads
|
|
57
|
+
"""
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.scanner = scanner
|
|
60
|
+
self.threads = threads
|
|
61
|
+
|
|
62
|
+
def run(self):
|
|
63
|
+
"""Run the document scan."""
|
|
64
|
+
try:
|
|
65
|
+
self.progress.emit("Starting scan...") # type: ignore
|
|
66
|
+
|
|
67
|
+
# Set up custom logger to capture messages
|
|
68
|
+
class ProgressHandler(logging.Handler):
|
|
69
|
+
def __init__(self, signal: Signal):
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.signal = signal
|
|
72
|
+
|
|
73
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
74
|
+
self.signal.emit(lambda: self.format(record)) # type: ignore
|
|
75
|
+
|
|
76
|
+
progress_handler = ProgressHandler(self.progress)
|
|
77
|
+
progress_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
78
|
+
logger.addHandler(progress_handler)
|
|
79
|
+
|
|
80
|
+
# Set progress callback
|
|
81
|
+
def progress_callback(current: int, total: int) -> None:
|
|
82
|
+
self.progress_update.emit(current, total) # pyright: ignore[reportAttributeAccessIssue]
|
|
83
|
+
|
|
84
|
+
self.scanner.set_progress_callback(progress_callback)
|
|
85
|
+
|
|
86
|
+
# Run scan
|
|
87
|
+
results = self.scanner.scan(threads=self.threads, show_progress=True)
|
|
88
|
+
|
|
89
|
+
logger.removeHandler(progress_handler)
|
|
90
|
+
self.progress.emit("Scan complete!") # pyright: ignore[reportAttributeAccessIssue]
|
|
91
|
+
self.finished.emit(results) # pyright: ignore[reportAttributeAccessIssue]
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.error.emit(str(e)) # pyright: ignore[reportAttributeAccessIssue]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DocScanGUI(QWidget):
|
|
97
|
+
"""Main GUI window for document scanner application."""
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
"""Initialize GUI components."""
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.scan_results = None
|
|
103
|
+
self.scan_worker = None
|
|
104
|
+
self.is_scanning = False
|
|
105
|
+
self.init_ui()
|
|
106
|
+
|
|
107
|
+
def init_ui(self):
|
|
108
|
+
"""Initialize user interface."""
|
|
109
|
+
self.setWindowTitle("Document Scanner GUI")
|
|
110
|
+
self.setMinimumSize(1000, 700)
|
|
111
|
+
|
|
112
|
+
# Main layout
|
|
113
|
+
main_layout = QVBoxLayout()
|
|
114
|
+
self.setLayout(main_layout)
|
|
115
|
+
|
|
116
|
+
# Create tab widget for options
|
|
117
|
+
tab_widget = QTabWidget()
|
|
118
|
+
self._create_input_tab(tab_widget)
|
|
119
|
+
self._create_options_tab(tab_widget)
|
|
120
|
+
main_layout.addWidget(tab_widget)
|
|
121
|
+
|
|
122
|
+
# Create other sections
|
|
123
|
+
self._create_actions_section(main_layout)
|
|
124
|
+
self._create_results_section(main_layout)
|
|
125
|
+
|
|
126
|
+
def _create_input_tab(self, tab_widget: QTabWidget) -> None:
|
|
127
|
+
"""Create input configuration tab.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
tab_widget: Tab widget to add tab to
|
|
131
|
+
"""
|
|
132
|
+
input_tab = QWidget()
|
|
133
|
+
input_layout = QVBoxLayout()
|
|
134
|
+
input_tab.setLayout(input_layout)
|
|
135
|
+
|
|
136
|
+
# Input directory
|
|
137
|
+
dir_layout = QHBoxLayout()
|
|
138
|
+
dir_label = QLabel("Input Directory:")
|
|
139
|
+
self.dir_edit = QLineEdit(str(Path.cwd()))
|
|
140
|
+
dir_browse_btn = QPushButton("Browse...")
|
|
141
|
+
dir_browse_btn.clicked.connect(self._browse_directory) # pyright: ignore[reportAttributeAccessIssue]
|
|
142
|
+
self.dir_edit.textChanged.connect(self._on_directory_changed) # pyright: ignore[reportAttributeAccessIssue]
|
|
143
|
+
dir_layout.addWidget(dir_label)
|
|
144
|
+
dir_layout.addWidget(self.dir_edit)
|
|
145
|
+
dir_layout.addWidget(dir_browse_btn)
|
|
146
|
+
input_layout.addLayout(dir_layout)
|
|
147
|
+
|
|
148
|
+
# Rules file
|
|
149
|
+
rules_layout = QHBoxLayout()
|
|
150
|
+
rules_label = QLabel("Rules File:")
|
|
151
|
+
self.rules_edit = QLineEdit("rules.json")
|
|
152
|
+
rules_browse_btn = QPushButton("Browse...")
|
|
153
|
+
rules_browse_btn.clicked.connect(self._browse_rules_file) # pyright: ignore[reportAttributeAccessIssue]
|
|
154
|
+
rules_layout.addWidget(rules_label)
|
|
155
|
+
rules_layout.addWidget(self.rules_edit)
|
|
156
|
+
rules_layout.addWidget(rules_browse_btn)
|
|
157
|
+
input_layout.addLayout(rules_layout)
|
|
158
|
+
|
|
159
|
+
# File types
|
|
160
|
+
types_layout = QVBoxLayout()
|
|
161
|
+
types_label = QLabel("File Types:")
|
|
162
|
+
self.types_edit = QLineEdit("pdf,docx,xlsx,pptx,txt,odt,rtf,epub,csv,xml,html,md")
|
|
163
|
+
types_layout.addWidget(types_label)
|
|
164
|
+
types_layout.addWidget(self.types_edit)
|
|
165
|
+
input_layout.addLayout(types_layout)
|
|
166
|
+
|
|
167
|
+
input_layout.addStretch()
|
|
168
|
+
|
|
169
|
+
tab_widget.addTab(input_tab, "Input Configuration")
|
|
170
|
+
|
|
171
|
+
def _create_options_tab(self, tab_widget: QTabWidget) -> None:
|
|
172
|
+
"""Create scan options tab.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
tab_widget: Tab widget to add tab to
|
|
176
|
+
"""
|
|
177
|
+
options_tab = QWidget()
|
|
178
|
+
options_layout = QVBoxLayout()
|
|
179
|
+
options_tab.setLayout(options_layout)
|
|
180
|
+
|
|
181
|
+
# Checkboxes
|
|
182
|
+
self.ocr_checkbox = QCheckBox("Use PDF OCR")
|
|
183
|
+
self.process_pool_checkbox = QCheckBox("Use Process Pool (CPU-intensive)")
|
|
184
|
+
options_layout.addWidget(self.ocr_checkbox)
|
|
185
|
+
options_layout.addWidget(self.process_pool_checkbox)
|
|
186
|
+
|
|
187
|
+
# Thread count
|
|
188
|
+
thread_layout = QHBoxLayout()
|
|
189
|
+
thread_label = QLabel("Threads:")
|
|
190
|
+
self.thread_spin = QSpinBox()
|
|
191
|
+
self.thread_spin.setMinimum(1)
|
|
192
|
+
self.thread_spin.setMaximum(16)
|
|
193
|
+
self.thread_spin.setValue(4)
|
|
194
|
+
thread_layout.addWidget(thread_label)
|
|
195
|
+
thread_layout.addWidget(self.thread_spin)
|
|
196
|
+
thread_layout.addStretch()
|
|
197
|
+
options_layout.addLayout(thread_layout)
|
|
198
|
+
|
|
199
|
+
# Batch size
|
|
200
|
+
batch_layout = QHBoxLayout()
|
|
201
|
+
batch_label = QLabel("Batch Size:")
|
|
202
|
+
self.batch_spin = QSpinBox()
|
|
203
|
+
self.batch_spin.setMinimum(1)
|
|
204
|
+
self.batch_spin.setMaximum(1000)
|
|
205
|
+
self.batch_spin.setValue(50)
|
|
206
|
+
batch_layout.addWidget(batch_label)
|
|
207
|
+
batch_layout.addWidget(self.batch_spin)
|
|
208
|
+
batch_layout.addStretch()
|
|
209
|
+
options_layout.addLayout(batch_layout)
|
|
210
|
+
|
|
211
|
+
options_layout.addStretch()
|
|
212
|
+
|
|
213
|
+
tab_widget.addTab(options_tab, "Scan Options")
|
|
214
|
+
|
|
215
|
+
def _create_actions_section(self, parent_layout: QVBoxLayout) -> None:
|
|
216
|
+
"""Create action buttons section.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
parent_layout: Parent layout to add this section to
|
|
220
|
+
"""
|
|
221
|
+
actions_layout = QHBoxLayout()
|
|
222
|
+
|
|
223
|
+
self.scan_btn = QPushButton("Start Scan")
|
|
224
|
+
self.scan_btn.clicked.connect(self._start_scan) # pyright: ignore[reportAttributeAccessIssue]
|
|
225
|
+
self.scan_btn.setMinimumHeight(40)
|
|
226
|
+
|
|
227
|
+
self.pause_btn = QPushButton("Pause")
|
|
228
|
+
self.pause_btn.clicked.connect(self._pause_scan) # pyright: ignore[reportAttributeAccessIssue]
|
|
229
|
+
self.pause_btn.setEnabled(False)
|
|
230
|
+
self.pause_btn.setMinimumHeight(40)
|
|
231
|
+
|
|
232
|
+
self.stop_btn = QPushButton("Stop")
|
|
233
|
+
self.stop_btn.clicked.connect(self._stop_scan) # pyright: ignore[reportAttributeAccessIssue]
|
|
234
|
+
self.stop_btn.setEnabled(False)
|
|
235
|
+
self.stop_btn.setMinimumHeight(40)
|
|
236
|
+
|
|
237
|
+
self.save_btn = QPushButton("Save Results")
|
|
238
|
+
self.save_btn.clicked.connect(self._save_results) # pyright: ignore[reportAttributeAccessIssue]
|
|
239
|
+
self.save_btn.setEnabled(False)
|
|
240
|
+
self.save_btn.setMinimumHeight(40)
|
|
241
|
+
|
|
242
|
+
self.clear_btn = QPushButton("Clear Results")
|
|
243
|
+
self.clear_btn.clicked.connect(self._clear_results) # pyright: ignore[reportAttributeAccessIssue]
|
|
244
|
+
self.clear_btn.setMinimumHeight(40)
|
|
245
|
+
|
|
246
|
+
actions_layout.addWidget(self.scan_btn)
|
|
247
|
+
actions_layout.addWidget(self.pause_btn)
|
|
248
|
+
actions_layout.addWidget(self.stop_btn)
|
|
249
|
+
actions_layout.addWidget(self.save_btn)
|
|
250
|
+
actions_layout.addWidget(self.clear_btn)
|
|
251
|
+
|
|
252
|
+
parent_layout.addLayout(actions_layout)
|
|
253
|
+
|
|
254
|
+
def _create_results_section(self, parent_layout: QVBoxLayout) -> None:
|
|
255
|
+
"""Create results display section.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
parent_layout: Parent layout to add this section to
|
|
259
|
+
"""
|
|
260
|
+
results_group = QGroupBox("Results")
|
|
261
|
+
results_layout = QVBoxLayout()
|
|
262
|
+
results_group.setLayout(results_layout)
|
|
263
|
+
|
|
264
|
+
# Summary labels
|
|
265
|
+
summary_layout = QHBoxLayout()
|
|
266
|
+
self.files_label = QLabel("Files Scanned: 0")
|
|
267
|
+
self.matches_label = QLabel("Files with Matches: 0")
|
|
268
|
+
summary_layout.addWidget(self.files_label)
|
|
269
|
+
summary_layout.addWidget(self.matches_label)
|
|
270
|
+
results_layout.addLayout(summary_layout)
|
|
271
|
+
|
|
272
|
+
# Progress bar
|
|
273
|
+
self.progress_bar = QProgressBar()
|
|
274
|
+
self.progress_bar.setMinimum(0)
|
|
275
|
+
self.progress_bar.setMaximum(100)
|
|
276
|
+
self.progress_bar.setValue(0)
|
|
277
|
+
results_layout.addWidget(self.progress_bar)
|
|
278
|
+
|
|
279
|
+
# Progress/Log text
|
|
280
|
+
self.log_text = QTextEdit()
|
|
281
|
+
self.log_text.setReadOnly(True)
|
|
282
|
+
self.log_text.setMaximumHeight(150)
|
|
283
|
+
results_layout.addWidget(QLabel("Progress Log:"))
|
|
284
|
+
results_layout.addWidget(self.log_text)
|
|
285
|
+
|
|
286
|
+
# Results table
|
|
287
|
+
self.results_table = QTableWidget()
|
|
288
|
+
self.results_table.setColumnCount(4)
|
|
289
|
+
self.results_table.setHorizontalHeaderLabels(["File", "Type", "Matches", "Time (s)"])
|
|
290
|
+
self.results_table.horizontalHeader().setStretchLastSection(True)
|
|
291
|
+
results_layout.addWidget(QLabel("Match Details:"))
|
|
292
|
+
results_layout.addWidget(self.results_table)
|
|
293
|
+
|
|
294
|
+
# Match details text
|
|
295
|
+
self.details_text = QTextEdit()
|
|
296
|
+
self.details_text.setReadOnly(True)
|
|
297
|
+
self.details_text.setMaximumHeight(200)
|
|
298
|
+
results_layout.addWidget(QLabel("Selected Match Context:"))
|
|
299
|
+
results_layout.addWidget(self.details_text)
|
|
300
|
+
|
|
301
|
+
# Connect table selection
|
|
302
|
+
self.results_table.itemSelectionChanged.connect(self._show_match_details) # pyright: ignore[reportAttributeAccessIssue]
|
|
303
|
+
|
|
304
|
+
parent_layout.addWidget(results_group)
|
|
305
|
+
|
|
306
|
+
def _browse_directory(self) -> None:
|
|
307
|
+
"""Open directory browser dialog."""
|
|
308
|
+
dir_path = QFileDialog.getExistingDirectory(self, "Select Input Directory")
|
|
309
|
+
if dir_path:
|
|
310
|
+
self.dir_edit.setText(str(Path(dir_path)))
|
|
311
|
+
|
|
312
|
+
def _on_directory_changed(self) -> None:
|
|
313
|
+
"""Handle directory text change - auto-search for rules.json."""
|
|
314
|
+
dir_text = self.dir_edit.text()
|
|
315
|
+
if not dir_text:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
input_dir = Path(dir_text)
|
|
320
|
+
if input_dir.exists() and input_dir.is_dir():
|
|
321
|
+
# Search for rules.json or rules*.json files
|
|
322
|
+
rule_files = list(input_dir.glob("rules.json")) + list(input_dir.glob("rules*.json"))
|
|
323
|
+
|
|
324
|
+
if rule_files:
|
|
325
|
+
# Use the first matching file, prefer exact "rules.json"
|
|
326
|
+
exact_match = next((f for f in rule_files if f.name == "rules.json"), None)
|
|
327
|
+
rules_file = exact_match if exact_match else rule_files[0]
|
|
328
|
+
self.rules_edit.setText(str(rules_file.resolve()))
|
|
329
|
+
except Exception:
|
|
330
|
+
# Ignore errors during directory change handling
|
|
331
|
+
pass
|
|
332
|
+
|
|
333
|
+
def _browse_rules_file(self) -> None:
|
|
334
|
+
"""Open file browser dialog for rules file."""
|
|
335
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Select Rules File", "", "JSON Files (*.json)")
|
|
336
|
+
if file_path:
|
|
337
|
+
self.rules_edit.setText(str(Path(file_path)))
|
|
338
|
+
|
|
339
|
+
def _load_rules(self) -> list[Rule]:
|
|
340
|
+
"""Load rules from JSON file.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
List of Rule objects
|
|
344
|
+
"""
|
|
345
|
+
rules_file = Path(self.rules_edit.text())
|
|
346
|
+
if not rules_file.exists():
|
|
347
|
+
# Try finding rules in input directory
|
|
348
|
+
input_dir = Path(self.dir_edit.text())
|
|
349
|
+
rule_files = list(input_dir.glob("rules*.json"))
|
|
350
|
+
if rule_files:
|
|
351
|
+
rules_file = rule_files[0]
|
|
352
|
+
self.rules_edit.setText(str(rules_file.resolve()))
|
|
353
|
+
else:
|
|
354
|
+
raise FileNotFoundError(f"Rules file not found: {rules_file}")
|
|
355
|
+
|
|
356
|
+
with open(rules_file, encoding="utf-8") as f:
|
|
357
|
+
rules_data = json.load(f)
|
|
358
|
+
|
|
359
|
+
rules = []
|
|
360
|
+
if isinstance(rules_data, list):
|
|
361
|
+
rules = [Rule(rule) for rule in rules_data]
|
|
362
|
+
elif isinstance(rules_data, dict) and "rules" in rules_data:
|
|
363
|
+
rules = [Rule(rule) for rule in rules_data["rules"]]
|
|
364
|
+
|
|
365
|
+
return rules
|
|
366
|
+
|
|
367
|
+
def _start_scan(self) -> None:
|
|
368
|
+
"""Start the document scan."""
|
|
369
|
+
# Validate inputs
|
|
370
|
+
input_dir = Path(self.dir_edit.text())
|
|
371
|
+
if not input_dir.exists() or not input_dir.is_dir():
|
|
372
|
+
QMessageBox.warning(self, "Error", "Invalid input directory")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
rules = self._load_rules()
|
|
377
|
+
if not rules:
|
|
378
|
+
QMessageBox.warning(self, "Error", "No valid rules found")
|
|
379
|
+
return
|
|
380
|
+
except Exception as e:
|
|
381
|
+
QMessageBox.warning(self, "Error", f"Failed to load rules: {e}")
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
# Parse file types
|
|
385
|
+
file_types = [ft.strip() for ft in self.types_edit.text().split(",")]
|
|
386
|
+
|
|
387
|
+
# Clear previous results
|
|
388
|
+
self._clear_results()
|
|
389
|
+
|
|
390
|
+
# Set scanning state
|
|
391
|
+
self.is_scanning = True
|
|
392
|
+
|
|
393
|
+
# Disable scan button during scan, enable pause and stop
|
|
394
|
+
self.scan_btn.setEnabled(False)
|
|
395
|
+
self.pause_btn.setEnabled(True)
|
|
396
|
+
self.stop_btn.setEnabled(True)
|
|
397
|
+
self.pause_btn.setText("Pause")
|
|
398
|
+
|
|
399
|
+
# Create scanner
|
|
400
|
+
scanner = DocumentScanner(
|
|
401
|
+
input_dir=input_dir,
|
|
402
|
+
rules=rules,
|
|
403
|
+
file_types=file_types,
|
|
404
|
+
use_pdf_ocr=self.ocr_checkbox.isChecked(),
|
|
405
|
+
use_process_pool=self.process_pool_checkbox.isChecked(),
|
|
406
|
+
batch_size=self.batch_spin.value(),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Create and start worker thread
|
|
410
|
+
self.scan_worker = ScanWorker(scanner, self.thread_spin.value())
|
|
411
|
+
self.scan_worker.progress.connect(self._log_message) # pyright: ignore[reportAttributeAccessIssue]
|
|
412
|
+
self.scan_worker.progress_update.connect(self._update_progress) # pyright: ignore[reportAttributeAccessIssue]
|
|
413
|
+
self.scan_worker.finished.connect(self._scan_finished) # pyright: ignore[reportAttributeAccessIssue]
|
|
414
|
+
self.scan_worker.error.connect(self._scan_error) # pyright: ignore[reportAttributeAccessIssue]
|
|
415
|
+
self.scan_worker.start()
|
|
416
|
+
|
|
417
|
+
def _scan_finished(self, results: dict[str, Any]) -> None:
|
|
418
|
+
"""Handle scan completion.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
results: Scan results dictionary
|
|
422
|
+
"""
|
|
423
|
+
self.scan_results = results
|
|
424
|
+
self.is_scanning = False
|
|
425
|
+
self.scan_btn.setEnabled(True)
|
|
426
|
+
self.pause_btn.setEnabled(False)
|
|
427
|
+
self.stop_btn.setEnabled(False)
|
|
428
|
+
self.save_btn.setEnabled(True)
|
|
429
|
+
|
|
430
|
+
# Update summary
|
|
431
|
+
scan_info = results.get("scan_info", {})
|
|
432
|
+
processed = scan_info.get("files_processed", scan_info.get("total_files", 0))
|
|
433
|
+
self.files_label.setText(f"Files Scanned: {processed}/{scan_info.get('total_files', 0)}")
|
|
434
|
+
self.matches_label.setText(f"Files with Matches: {scan_info.get('files_with_matches', 0)}")
|
|
435
|
+
|
|
436
|
+
# Update progress bar to 100%
|
|
437
|
+
self.progress_bar.setValue(100)
|
|
438
|
+
|
|
439
|
+
# Populate results table
|
|
440
|
+
matches = results.get("matches", [])
|
|
441
|
+
self.results_table.setRowCount(len(matches))
|
|
442
|
+
|
|
443
|
+
for row, match_data in enumerate(matches):
|
|
444
|
+
file_path = match_data.get("file_path", "")
|
|
445
|
+
file_type = match_data.get("file_type", "")
|
|
446
|
+
match_count = len(match_data.get("matches", []))
|
|
447
|
+
proc_time = match_data.get("metadata", {}).get("processing_time_seconds", 0)
|
|
448
|
+
|
|
449
|
+
self.results_table.setItem(row, 0, QTableWidgetItem(Path(file_path).name))
|
|
450
|
+
self.results_table.setItem(row, 1, QTableWidgetItem(file_type))
|
|
451
|
+
self.results_table.setItem(row, 2, QTableWidgetItem(str(match_count)))
|
|
452
|
+
self.results_table.setItem(row, 3, QTableWidgetItem(f"{proc_time:.3f}"))
|
|
453
|
+
|
|
454
|
+
status = "completed" if not results.get("stopped") else "stopped"
|
|
455
|
+
self._log_message(f"Scan {status}. Found matches in {len(matches)} files.")
|
|
456
|
+
|
|
457
|
+
def _scan_error(self, error_msg: str) -> None:
|
|
458
|
+
"""Handle scan error.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
error_msg: Error message
|
|
462
|
+
"""
|
|
463
|
+
self.is_scanning = False
|
|
464
|
+
self.scan_btn.setEnabled(True)
|
|
465
|
+
self.pause_btn.setEnabled(False)
|
|
466
|
+
self.stop_btn.setEnabled(False)
|
|
467
|
+
self._log_message(f"Error: {error_msg}")
|
|
468
|
+
QMessageBox.critical(self, "Error", f"Scan failed: {error_msg}")
|
|
469
|
+
|
|
470
|
+
def _pause_scan(self) -> None:
|
|
471
|
+
"""Pause or resume the document scan."""
|
|
472
|
+
if self.scan_worker and self.scan_worker.scanner:
|
|
473
|
+
scanner = self.scan_worker.scanner
|
|
474
|
+
if scanner.is_paused():
|
|
475
|
+
# Resume
|
|
476
|
+
scanner.resume()
|
|
477
|
+
self.pause_btn.setText("Pause")
|
|
478
|
+
self._log_message("Scan resumed")
|
|
479
|
+
else:
|
|
480
|
+
# Pause
|
|
481
|
+
scanner.pause()
|
|
482
|
+
self.pause_btn.setText("Resume")
|
|
483
|
+
self._log_message("Scan paused")
|
|
484
|
+
|
|
485
|
+
def _stop_scan(self) -> None:
|
|
486
|
+
"""Stop the document scan."""
|
|
487
|
+
if not self.is_scanning:
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
if self.scan_worker and self.scan_worker.scanner:
|
|
491
|
+
scanner = self.scan_worker.scanner
|
|
492
|
+
scanner.stop()
|
|
493
|
+
|
|
494
|
+
# Disable pause and stop buttons immediately
|
|
495
|
+
self.pause_btn.setEnabled(False)
|
|
496
|
+
self.stop_btn.setEnabled(False)
|
|
497
|
+
|
|
498
|
+
# Log the stop action
|
|
499
|
+
self._log_message("Stopping scan...")
|
|
500
|
+
|
|
501
|
+
# Force UI update
|
|
502
|
+
QApplication.processEvents()
|
|
503
|
+
|
|
504
|
+
def _update_progress(self, current: int, total: int) -> None:
|
|
505
|
+
"""Update progress bar and file count.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
current: Current number of files processed
|
|
509
|
+
total: Total number of files
|
|
510
|
+
"""
|
|
511
|
+
if total > 0:
|
|
512
|
+
percentage = int((current / total) * 100)
|
|
513
|
+
self.progress_bar.setValue(percentage)
|
|
514
|
+
self.files_label.setText(f"Files Scanned: {current}/{total}")
|
|
515
|
+
|
|
516
|
+
def _show_match_details(self) -> None:
|
|
517
|
+
"""Show details of selected match in the results table."""
|
|
518
|
+
selected_rows = self.results_table.selectionModel().selectedRows()
|
|
519
|
+
if not selected_rows or not self.scan_results:
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
row = selected_rows[0].row()
|
|
523
|
+
matches = self.scan_results.get("matches", [])
|
|
524
|
+
|
|
525
|
+
if row >= len(matches):
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
match_data = matches[row]
|
|
529
|
+
details = []
|
|
530
|
+
|
|
531
|
+
# File info
|
|
532
|
+
details.append(f"File: {match_data.get('file_path', '')}")
|
|
533
|
+
details.append(f"Type: {match_data.get('file_type', '')}")
|
|
534
|
+
details.append(f"Size: {match_data.get('file_size', 0)} bytes\n")
|
|
535
|
+
|
|
536
|
+
# Match info
|
|
537
|
+
for match in match_data.get("matches", []):
|
|
538
|
+
details.append(f"Rule: {match.get('rule_name', '')}")
|
|
539
|
+
details.append(f"Description: {match.get('rule_description', '')}")
|
|
540
|
+
details.append(f"Line {match.get('line_number', 0)}: {match.get('match', '')}")
|
|
541
|
+
details.append("\nContext:")
|
|
542
|
+
for ctx_line in match.get("context", []):
|
|
543
|
+
details.append(f" {ctx_line}")
|
|
544
|
+
details.append("-" * 50)
|
|
545
|
+
|
|
546
|
+
self.details_text.setText("\n".join(details))
|
|
547
|
+
|
|
548
|
+
def _save_results(self) -> None:
|
|
549
|
+
"""Save scan results to JSON file."""
|
|
550
|
+
if not self.scan_results:
|
|
551
|
+
QMessageBox.warning(self, "Warning", "No results to save")
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
default_name = f"scan_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
555
|
+
file_path, _ = QFileDialog.getSaveFileName(self, "Save Results", default_name, "JSON Files (*.json)")
|
|
556
|
+
|
|
557
|
+
if file_path:
|
|
558
|
+
try:
|
|
559
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
560
|
+
json.dump(self.scan_results, f, indent=2, ensure_ascii=False)
|
|
561
|
+
self._log_message(f"Results saved to: {file_path}")
|
|
562
|
+
QMessageBox.information(self, "Success", f"Results saved to:\n{file_path}")
|
|
563
|
+
except Exception as e:
|
|
564
|
+
QMessageBox.critical(self, "Error", f"Failed to save results: {e}")
|
|
565
|
+
|
|
566
|
+
def _clear_results(self) -> None:
|
|
567
|
+
"""Clear all results and logs."""
|
|
568
|
+
self.scan_results = None
|
|
569
|
+
self.log_text.clear()
|
|
570
|
+
self.results_table.setRowCount(0)
|
|
571
|
+
self.details_text.clear()
|
|
572
|
+
self.files_label.setText("Files Scanned: 0")
|
|
573
|
+
self.matches_label.setText("Files with Matches: 0")
|
|
574
|
+
self.progress_bar.setValue(0)
|
|
575
|
+
self.save_btn.setEnabled(False)
|
|
576
|
+
|
|
577
|
+
def _log_message(self, message: str) -> None:
|
|
578
|
+
"""Add message to log text area.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
message: Message to log
|
|
582
|
+
"""
|
|
583
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
584
|
+
self.log_text.append(f"[{timestamp}] {message}")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def main():
|
|
588
|
+
"""Main entry point for GUI application."""
|
|
589
|
+
app = QApplication(sys.argv)
|
|
590
|
+
window = DocScanGUI()
|
|
591
|
+
window.show()
|
|
592
|
+
sys.exit(app.exec_())
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
if __name__ == "__main__":
|
|
596
|
+
main()
|
|
File without changes
|