pysfi 0.1.12__py3-none-any.whl → 0.1.13__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.12.dist-info → pysfi-0.1.13.dist-info}/METADATA +1 -1
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/RECORD +35 -27
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/entry_points.txt +2 -0
- sfi/__init__.py +5 -3
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +5 -3
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cli.py +12 -2
- sfi/condasetup/__init__.py +1 -0
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +1 -1
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan_gui.py +150 -46
- sfi/img2pdf/__init__.py +0 -0
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +39 -11
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +507 -124
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +571 -465
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +494 -965
- sfi/pyprojectparse/pyprojectparse.py +328 -28
- sfi/pysourcepack/__init__.py +1 -0
- sfi/pysourcepack/pysourcepack.py +210 -131
- sfi/quizbase/quizbase_gui.py +2 -2
- sfi/taskkill/taskkill.py +168 -59
- sfi/which/which.py +11 -3
- sfi/workflowengine/workflowengine.py +225 -122
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/WHEEL +0 -0
sfi/taskkill/taskkill.py
CHANGED
|
@@ -6,40 +6,66 @@ import logging
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from typing import Final
|
|
9
10
|
|
|
11
|
+
# Configure logging
|
|
10
12
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
15
|
+
# Constants
|
|
16
|
+
DEFAULT_MATCH_THRESHOLD: Final[int] = 10
|
|
17
|
+
FORCE_MATCH_THRESHOLD: Final[int] = 1000
|
|
18
|
+
PROCESS_LIST_TIMEOUT: Final[int] = 10
|
|
19
|
+
ENCODING_ATTEMPTS: Final[tuple[str, ...]] = ("gbk", "utf8")
|
|
20
|
+
|
|
21
|
+
# Global state (consider refactoring to class-based approach in future)
|
|
13
22
|
_cached_process_list: list[ProcessInfo] = []
|
|
14
23
|
_processes_cached: bool = False
|
|
15
|
-
_max_match_threshold: int =
|
|
24
|
+
_max_match_threshold: int = DEFAULT_MATCH_THRESHOLD
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
@dataclass
|
|
19
28
|
class ProcessInfo:
|
|
20
|
-
"""
|
|
29
|
+
"""Represents information about a system process.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
name: The name of the process executable
|
|
33
|
+
pid: The process identifier as string
|
|
34
|
+
"""
|
|
21
35
|
|
|
22
36
|
name: str
|
|
23
37
|
pid: str
|
|
24
38
|
|
|
25
39
|
|
|
26
40
|
def get_matched_process(process_name: str) -> list[ProcessInfo]:
|
|
27
|
-
"""Get
|
|
41
|
+
"""Get processes matching the given pattern.
|
|
42
|
+
|
|
43
|
+
Performs case-insensitive wildcard matching against process names.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
process_name: Pattern to match against process names (supports wildcards)
|
|
28
47
|
|
|
29
48
|
Returns:
|
|
30
|
-
|
|
49
|
+
List of ProcessInfo objects that match the pattern
|
|
31
50
|
"""
|
|
32
|
-
|
|
51
|
+
pattern = process_name.lower()
|
|
52
|
+
return [p for p in _cached_process_list if fnmatch.fnmatch(p.name.lower(), pattern)]
|
|
33
53
|
|
|
34
54
|
|
|
35
55
|
def get_process_list(force_refresh: bool = False) -> None:
|
|
36
|
-
"""
|
|
56
|
+
"""Retrieve the current list of system processes.
|
|
57
|
+
|
|
58
|
+
Caches the process list to avoid repeated system calls. Use force_refresh=True
|
|
59
|
+
to bypass the cache when fresh data is required.
|
|
37
60
|
|
|
38
61
|
Args:
|
|
39
|
-
force_refresh:
|
|
62
|
+
force_refresh: If True, forces refreshing the process list even if cached.
|
|
63
|
+
Defaults to False.
|
|
40
64
|
"""
|
|
41
|
-
global _processes_cached
|
|
65
|
+
global _processes_cached, _cached_process_list
|
|
66
|
+
|
|
42
67
|
if _processes_cached and not force_refresh:
|
|
68
|
+
logger.debug("Using cached process list")
|
|
43
69
|
return
|
|
44
70
|
|
|
45
71
|
_cached_process_list.clear()
|
|
@@ -48,9 +74,18 @@ def get_process_list(force_refresh: bool = False) -> None:
|
|
|
48
74
|
else:
|
|
49
75
|
_get_process_list_unix()
|
|
50
76
|
_processes_cached = True
|
|
77
|
+
logger.debug(f"Retrieved {len(_cached_process_list)} processes")
|
|
51
78
|
|
|
52
79
|
|
|
53
80
|
def _get_process_list_windows(encoding: str = "gbk") -> None:
|
|
81
|
+
"""Retrieve process list on Windows using tasklist command.
|
|
82
|
+
|
|
83
|
+
Attempts to decode output with specified encoding, falling back to alternatives
|
|
84
|
+
if decoding fails.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
encoding: Text encoding to try first. Defaults to "gbk".
|
|
88
|
+
"""
|
|
54
89
|
try:
|
|
55
90
|
result = subprocess.run(
|
|
56
91
|
["tasklist", "/fo", "csv", "/nh"],
|
|
@@ -58,7 +93,7 @@ def _get_process_list_windows(encoding: str = "gbk") -> None:
|
|
|
58
93
|
text=True,
|
|
59
94
|
encoding=encoding,
|
|
60
95
|
check=True,
|
|
61
|
-
timeout=
|
|
96
|
+
timeout=PROCESS_LIST_TIMEOUT,
|
|
62
97
|
)
|
|
63
98
|
logger.debug(f"Decoded using {encoding}")
|
|
64
99
|
for line in result.stdout.strip().split("\n"):
|
|
@@ -69,37 +104,46 @@ def _get_process_list_windows(encoding: str = "gbk") -> None:
|
|
|
69
104
|
pid = parts[1].strip('"')
|
|
70
105
|
_cached_process_list.append(ProcessInfo(name, pid))
|
|
71
106
|
except UnicodeDecodeError:
|
|
72
|
-
logger.warning(
|
|
73
|
-
|
|
107
|
+
logger.warning(
|
|
108
|
+
f"Failed to decode using {encoding} encoding, trying other encoding"
|
|
109
|
+
)
|
|
110
|
+
if encoding == ENCODING_ATTEMPTS[-1]:
|
|
74
111
|
logger.error("All encoding attempts failed, unable to get process list")
|
|
75
112
|
return
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
# Try next encoding in sequence
|
|
114
|
+
next_encoding_idx = ENCODING_ATTEMPTS.index(encoding) + 1
|
|
115
|
+
if next_encoding_idx < len(ENCODING_ATTEMPTS):
|
|
116
|
+
next_encoding = ENCODING_ATTEMPTS[next_encoding_idx]
|
|
117
|
+
try:
|
|
118
|
+
_get_process_list_windows(encoding=next_encoding)
|
|
119
|
+
logger.debug(f"Decoded using {next_encoding}")
|
|
120
|
+
except (
|
|
121
|
+
subprocess.SubprocessError,
|
|
122
|
+
OSError,
|
|
123
|
+
ValueError,
|
|
124
|
+
UnicodeDecodeError,
|
|
125
|
+
):
|
|
126
|
+
logger.error("Failed to get process list after encoding fallback")
|
|
127
|
+
return
|
|
88
128
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
|
89
129
|
logger.error(f"Failed to execute tasklist command: {e.__class__.__name__}")
|
|
90
130
|
return
|
|
91
131
|
except Exception as e:
|
|
92
|
-
logger.error(
|
|
132
|
+
logger.error(
|
|
133
|
+
f"Unknown error occurred while getting process list: {e.__class__.__name__}"
|
|
134
|
+
)
|
|
93
135
|
return
|
|
94
136
|
|
|
95
137
|
|
|
96
138
|
def _get_process_list_unix() -> None:
|
|
139
|
+
"""Retrieve process list on Unix-like systems using ps command."""
|
|
97
140
|
try:
|
|
98
141
|
result = subprocess.run(
|
|
99
142
|
["ps", "-eo", "pid,comm", "--no-headers"],
|
|
100
143
|
capture_output=True,
|
|
101
144
|
text=True,
|
|
102
145
|
check=True,
|
|
146
|
+
timeout=PROCESS_LIST_TIMEOUT,
|
|
103
147
|
)
|
|
104
148
|
for line in result.stdout.strip().split("\n"):
|
|
105
149
|
if line:
|
|
@@ -108,17 +152,26 @@ def _get_process_list_unix() -> None:
|
|
|
108
152
|
pid = parts[0]
|
|
109
153
|
name = parts[1]
|
|
110
154
|
_cached_process_list.append(ProcessInfo(name, pid))
|
|
111
|
-
except (
|
|
112
|
-
|
|
155
|
+
except (
|
|
156
|
+
subprocess.SubprocessError,
|
|
157
|
+
OSError,
|
|
158
|
+
ValueError,
|
|
159
|
+
subprocess.TimeoutExpired,
|
|
160
|
+
) as e:
|
|
161
|
+
logger.error(f"Failed to get process list: {e.__class__.__name__}")
|
|
113
162
|
return
|
|
114
163
|
|
|
115
164
|
|
|
116
165
|
def kill_process(process_name: str, force_refresh: bool = True) -> None:
|
|
117
|
-
"""Terminate
|
|
166
|
+
"""Terminate processes matching the given name or pattern.
|
|
167
|
+
|
|
168
|
+
Implements safety mechanisms including match threshold limits to prevent
|
|
169
|
+
accidental mass termination of processes.
|
|
118
170
|
|
|
119
171
|
Args:
|
|
120
|
-
process_name: Process name
|
|
121
|
-
force_refresh:
|
|
172
|
+
process_name: Process name or pattern to match (supports wildcards)
|
|
173
|
+
force_refresh: Whether to refresh the process list before matching.
|
|
174
|
+
Defaults to True.
|
|
122
175
|
"""
|
|
123
176
|
get_process_list(force_refresh=force_refresh)
|
|
124
177
|
matched_processes = get_matched_process(process_name)
|
|
@@ -142,19 +195,27 @@ def kill_process(process_name: str, force_refresh: bool = True) -> None:
|
|
|
142
195
|
logger.info(
|
|
143
196
|
f"Found {match_count} process(es) matching '{process_name}': {[m.name for m in matched_processes]}",
|
|
144
197
|
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
|
|
199
|
+
success_count = 0
|
|
200
|
+
failure_count = 0
|
|
201
|
+
|
|
202
|
+
for process in matched_processes:
|
|
203
|
+
if _kill_process_by_pid(process.pid):
|
|
204
|
+
logger.info(
|
|
205
|
+
f"Successfully terminated process {process.name} (PID: {process.pid})"
|
|
206
|
+
)
|
|
207
|
+
success_count += 1
|
|
208
|
+
else:
|
|
209
|
+
logger.info(
|
|
210
|
+
f"Failed to terminate process {process.name} (PID: {process.pid})"
|
|
211
|
+
)
|
|
212
|
+
failure_count += 1
|
|
213
|
+
|
|
214
|
+
logger.info(
|
|
215
|
+
f"Successfully terminated {success_count} process(es) matching '{process_name}'"
|
|
216
|
+
)
|
|
217
|
+
if failure_count > 0:
|
|
218
|
+
logger.warning(f"Failed to terminate {failure_count} process(es)")
|
|
158
219
|
|
|
159
220
|
|
|
160
221
|
def _kill_process_by_pid(pid: str) -> bool:
|
|
@@ -165,70 +226,118 @@ def _kill_process_by_pid(pid: str) -> bool:
|
|
|
165
226
|
|
|
166
227
|
|
|
167
228
|
def _kill_process_by_pid_windows(pid: str) -> bool:
|
|
168
|
-
"""Terminate process by PID on Windows.
|
|
229
|
+
"""Terminate process by PID on Windows using taskkill command.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
pid: Process identifier as string
|
|
169
233
|
|
|
170
234
|
Returns:
|
|
171
|
-
|
|
235
|
+
True if process was successfully terminated, False otherwise
|
|
172
236
|
"""
|
|
173
237
|
try:
|
|
174
|
-
subprocess.run(
|
|
238
|
+
result = subprocess.run(
|
|
175
239
|
["taskkill", "/F", "/PID", pid],
|
|
176
240
|
capture_output=True,
|
|
177
241
|
text=True,
|
|
178
242
|
check=True,
|
|
243
|
+
timeout=PROCESS_LIST_TIMEOUT,
|
|
179
244
|
)
|
|
245
|
+
logger.debug(f"taskkill output: {result.stdout.strip()}")
|
|
180
246
|
return True
|
|
181
|
-
except subprocess.CalledProcessError:
|
|
182
|
-
logger.error(f"Failed to terminate process PID {pid}")
|
|
247
|
+
except subprocess.CalledProcessError as e:
|
|
248
|
+
logger.error(f"Failed to terminate process PID {pid}: {e}")
|
|
249
|
+
if e.stdout:
|
|
250
|
+
logger.debug(f"taskkill stdout: {e.stdout.strip()}")
|
|
251
|
+
if e.stderr:
|
|
252
|
+
logger.debug(f"taskkill stderr: {e.stderr.strip()}")
|
|
253
|
+
return False
|
|
254
|
+
except subprocess.TimeoutExpired:
|
|
255
|
+
logger.error(f"Timeout while terminating process PID {pid}")
|
|
183
256
|
return False
|
|
184
257
|
|
|
185
258
|
|
|
186
259
|
def _kill_process_by_pid_unix(pid: str) -> bool:
|
|
187
|
-
"""Terminate process by PID on Unix/Linux.
|
|
260
|
+
"""Terminate process by PID on Unix/Linux using kill command.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
pid: Process identifier as string
|
|
188
264
|
|
|
189
265
|
Returns:
|
|
190
|
-
|
|
266
|
+
True if process was successfully terminated, False otherwise
|
|
191
267
|
"""
|
|
192
268
|
try:
|
|
193
|
-
subprocess.run(
|
|
269
|
+
result = subprocess.run(
|
|
194
270
|
["kill", "-9", pid],
|
|
195
271
|
capture_output=True,
|
|
196
272
|
text=True,
|
|
197
273
|
check=True,
|
|
274
|
+
timeout=PROCESS_LIST_TIMEOUT,
|
|
198
275
|
)
|
|
276
|
+
logger.debug(f"kill output: {result.stdout.strip()}")
|
|
199
277
|
return True
|
|
200
|
-
except subprocess.CalledProcessError:
|
|
201
|
-
logger.error(f"Failed to terminate process PID {pid}")
|
|
278
|
+
except subprocess.CalledProcessError as e:
|
|
279
|
+
logger.error(f"Failed to terminate process PID {pid}: {e}")
|
|
280
|
+
if e.stdout:
|
|
281
|
+
logger.debug(f"kill stdout: {e.stdout.strip()}")
|
|
282
|
+
if e.stderr:
|
|
283
|
+
logger.debug(f"kill stderr: {e.stderr.strip()}")
|
|
284
|
+
return False
|
|
285
|
+
except subprocess.TimeoutExpired:
|
|
286
|
+
logger.error(f"Timeout while terminating process PID {pid}")
|
|
202
287
|
return False
|
|
203
288
|
|
|
204
289
|
|
|
205
290
|
def main() -> None:
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
291
|
+
"""Main entry point for the taskkill command-line interface.
|
|
292
|
+
|
|
293
|
+
Terminates processes by name or pattern with cross-platform support.
|
|
294
|
+
"""
|
|
295
|
+
parser = argparse.ArgumentParser(
|
|
296
|
+
description="Cross-platform process termination utility",
|
|
297
|
+
epilog="Examples:\n taskk notepad.exe\n taskk python*\n taskk chrome.exe --debug\n taskk temp* --force",
|
|
298
|
+
)
|
|
299
|
+
parser.add_argument(
|
|
300
|
+
"processes",
|
|
301
|
+
type=str,
|
|
302
|
+
nargs="+",
|
|
303
|
+
help="Process name or pattern to terminate (supports wildcards)",
|
|
304
|
+
)
|
|
305
|
+
parser.add_argument(
|
|
306
|
+
"--debug",
|
|
307
|
+
"-d",
|
|
308
|
+
action="store_true",
|
|
309
|
+
help="Enable debug logging for detailed output",
|
|
310
|
+
)
|
|
210
311
|
parser.add_argument(
|
|
211
312
|
"--force",
|
|
212
313
|
"-f",
|
|
213
314
|
action="store_true",
|
|
214
|
-
help="Force termination even if match count exceeds threshold
|
|
315
|
+
help="Force termination even if match count exceeds safety threshold",
|
|
215
316
|
)
|
|
216
317
|
|
|
217
318
|
args = parser.parse_args()
|
|
218
319
|
|
|
219
320
|
if args.debug:
|
|
220
321
|
logger.setLevel(logging.DEBUG)
|
|
322
|
+
logger.debug("Debug mode enabled")
|
|
221
323
|
|
|
222
324
|
# Temporarily increase threshold if force flag is set
|
|
223
325
|
global _max_match_threshold
|
|
224
326
|
original_threshold = _max_match_threshold
|
|
225
327
|
if args.force:
|
|
226
|
-
_max_match_threshold =
|
|
328
|
+
_max_match_threshold = FORCE_MATCH_THRESHOLD
|
|
329
|
+
logger.debug(
|
|
330
|
+
f"Force mode enabled, threshold increased to {_max_match_threshold}"
|
|
331
|
+
)
|
|
227
332
|
|
|
228
|
-
#
|
|
333
|
+
# Process each target
|
|
229
334
|
for i, process_name in enumerate(args.processes):
|
|
335
|
+
# Refresh process list only for the first process to avoid redundant calls
|
|
230
336
|
force_refresh = i == 0
|
|
337
|
+
logger.info(f"Terminating processes matching: {process_name}")
|
|
231
338
|
kill_process(process_name, force_refresh=force_refresh)
|
|
232
339
|
|
|
233
340
|
# Restore original threshold
|
|
234
341
|
_max_match_threshold = original_threshold
|
|
342
|
+
if args.force:
|
|
343
|
+
logger.debug("Restored original match threshold")
|
sfi/which/which.py
CHANGED
|
@@ -18,8 +18,12 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
def find_executable(name: str, *, fuzzy: bool) -> tuple[str, str | None]:
|
|
19
19
|
"""Find executable path for commands cross-platform.
|
|
20
20
|
|
|
21
|
+
Args:
|
|
22
|
+
name: Name of the command to find
|
|
23
|
+
fuzzy: Whether to use fuzzy matching
|
|
24
|
+
|
|
21
25
|
Returns:
|
|
22
|
-
Tuple
|
|
26
|
+
Tuple containing the command name and its path if found, None otherwise.
|
|
23
27
|
"""
|
|
24
28
|
try:
|
|
25
29
|
# Select command based on system
|
|
@@ -49,8 +53,12 @@ def find_executable(name: str, *, fuzzy: bool) -> tuple[str, str | None]:
|
|
|
49
53
|
def main() -> None:
|
|
50
54
|
parser = argparse.ArgumentParser(description="Find executable path for commands.")
|
|
51
55
|
parser.add_argument("commands", type=str, nargs="+", help="Commands to query")
|
|
52
|
-
parser.add_argument(
|
|
53
|
-
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"-f", "--fuzzy", action="store_true", help="Enable fuzzy matching"
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"-q", "--quiet", action="store_true", help="Only print paths, no status symbols"
|
|
61
|
+
)
|
|
54
62
|
|
|
55
63
|
args = parser.parse_args()
|
|
56
64
|
|