pysfi 0.1.4__py3-none-any.whl → 0.1.6__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.4.dist-info → pysfi-0.1.6.dist-info}/METADATA +5 -1
- pysfi-0.1.6.dist-info/RECORD +21 -0
- {pysfi-0.1.4.dist-info → pysfi-0.1.6.dist-info}/WHEEL +1 -1
- {pysfi-0.1.4.dist-info → pysfi-0.1.6.dist-info}/entry_points.txt +3 -0
- sfi/__init__.py +1 -1
- sfi/alarmclock/alarmclock.py +367 -367
- sfi/bumpversion/__init__.py +1 -1
- sfi/bumpversion/bumpversion.py +535 -535
- sfi/embedinstall/embedinstall.py +418 -418
- sfi/makepython/makepython.py +310 -310
- sfi/pdfsplit/pdfsplit.py +173 -0
- sfi/pyloadergen/__init__.py +0 -0
- sfi/pyloadergen/pyloadergen.py +995 -995
- sfi/taskkill/taskkill.py +236 -0
- sfi/which/which.py +74 -0
- pysfi-0.1.4.dist-info/RECORD +0 -17
sfi/taskkill/taskkill.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import fnmatch
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_cached_process_list: list[ProcessInfo] = []
|
|
14
|
+
_processes_cached: bool = False
|
|
15
|
+
_max_match_threshold: int = 10 # Maximum number of processes to match without confirmation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ProcessInfo:
|
|
20
|
+
"""Process information class."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
pid: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_matched_process(process_name: str) -> list[ProcessInfo]:
|
|
27
|
+
"""Get the list of matching processes.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
list[ProcessInfo]: List of matched processes
|
|
31
|
+
"""
|
|
32
|
+
return [p for p in _cached_process_list if fnmatch.fnmatch(p.name.lower(), process_name.lower())]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_process_list(force_refresh: bool = False) -> None:
|
|
36
|
+
"""Get the list of processes.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
force_refresh: Force refresh the process list even if cached.
|
|
40
|
+
"""
|
|
41
|
+
global _processes_cached
|
|
42
|
+
if _processes_cached and not force_refresh:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
_cached_process_list.clear()
|
|
46
|
+
if sys.platform == "win32":
|
|
47
|
+
_get_process_list_windows()
|
|
48
|
+
else:
|
|
49
|
+
_get_process_list_unix()
|
|
50
|
+
_processes_cached = True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_process_list_windows(encoding: str = "gbk") -> None:
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
["tasklist", "/fo", "csv", "/nh"],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
encoding=encoding,
|
|
60
|
+
check=True,
|
|
61
|
+
timeout=10, # Add timeout to prevent infinite wait
|
|
62
|
+
)
|
|
63
|
+
logger.debug(f"Decoded using {encoding}")
|
|
64
|
+
for line in result.stdout.strip().split("\n"):
|
|
65
|
+
if line:
|
|
66
|
+
parts = line.split('","')
|
|
67
|
+
if len(parts) >= 2:
|
|
68
|
+
name = parts[0].strip('"')
|
|
69
|
+
pid = parts[1].strip('"')
|
|
70
|
+
_cached_process_list.append(ProcessInfo(name, pid))
|
|
71
|
+
except UnicodeDecodeError:
|
|
72
|
+
logger.warning(f"Failed to decode using {encoding} encoding, trying other encoding")
|
|
73
|
+
if encoding == "utf8":
|
|
74
|
+
logger.error("All encoding attempts failed, unable to get process list")
|
|
75
|
+
return
|
|
76
|
+
# If GBK encoding fails, try UTF-8
|
|
77
|
+
try:
|
|
78
|
+
_get_process_list_windows(encoding="utf8")
|
|
79
|
+
logger.debug("Decoded using utf8")
|
|
80
|
+
except (
|
|
81
|
+
subprocess.SubprocessError,
|
|
82
|
+
OSError,
|
|
83
|
+
ValueError,
|
|
84
|
+
UnicodeDecodeError,
|
|
85
|
+
):
|
|
86
|
+
logger.error("Failed to get process list")
|
|
87
|
+
return
|
|
88
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
|
89
|
+
logger.error(f"Failed to execute tasklist command: {e.__class__.__name__}")
|
|
90
|
+
return
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"Unknown error occurred while getting process list: {e.__class__.__name__}")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_process_list_unix() -> None:
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["ps", "-eo", "pid,comm", "--no-headers"],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
check=True,
|
|
103
|
+
)
|
|
104
|
+
for line in result.stdout.strip().split("\n"):
|
|
105
|
+
if line:
|
|
106
|
+
parts = line.strip().split(maxsplit=1)
|
|
107
|
+
if len(parts) >= 2:
|
|
108
|
+
pid = parts[0]
|
|
109
|
+
name = parts[1]
|
|
110
|
+
_cached_process_list.append(ProcessInfo(name, pid))
|
|
111
|
+
except (subprocess.SubprocessError, OSError, ValueError):
|
|
112
|
+
logger.error("Failed to get process list")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def kill_process(process_name: str, force_refresh: bool = True) -> None:
|
|
117
|
+
"""Terminate process.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
process_name: Process name
|
|
121
|
+
force_refresh: Force refresh process list before killing (default True)
|
|
122
|
+
"""
|
|
123
|
+
get_process_list(force_refresh=force_refresh)
|
|
124
|
+
matched_processes = get_matched_process(process_name)
|
|
125
|
+
|
|
126
|
+
if not matched_processes:
|
|
127
|
+
logger.warning(f"Process `{process_name}` not found")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
match_count = len(matched_processes)
|
|
131
|
+
|
|
132
|
+
if match_count > _max_match_threshold:
|
|
133
|
+
logger.warning(
|
|
134
|
+
f"Found {match_count} processes matching '{process_name}' (exceeds threshold of {_max_match_threshold})",
|
|
135
|
+
)
|
|
136
|
+
logger.warning("Process names:")
|
|
137
|
+
for i, p in enumerate(matched_processes, 1):
|
|
138
|
+
logger.warning(f" {i}. {p.name} (PID: {p.pid})")
|
|
139
|
+
logger.warning("Use more specific process name to avoid accidental termination")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
logger.info(
|
|
143
|
+
f"Found {match_count} process(es) matching '{process_name}': {[m.name for m in matched_processes]}",
|
|
144
|
+
)
|
|
145
|
+
try:
|
|
146
|
+
success_count = 0
|
|
147
|
+
for process in matched_processes:
|
|
148
|
+
if _kill_process_by_pid(process.pid):
|
|
149
|
+
logger.info(f"Successfully terminated process {process.name} (PID: {process.pid})")
|
|
150
|
+
success_count += 1
|
|
151
|
+
else:
|
|
152
|
+
logger.info(f"Failed to terminate process {process.name} (PID: {process.pid})")
|
|
153
|
+
except (subprocess.SubprocessError, OSError, ValueError):
|
|
154
|
+
logger.error("Failed to terminate process")
|
|
155
|
+
return
|
|
156
|
+
else:
|
|
157
|
+
logger.info(f"Successfully terminated {success_count} process(es) matching '{process_name}'")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _kill_process_by_pid(pid: str) -> bool:
|
|
161
|
+
if sys.platform == "win32":
|
|
162
|
+
return _kill_process_by_pid_windows(pid)
|
|
163
|
+
else:
|
|
164
|
+
return _kill_process_by_pid_unix(pid)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _kill_process_by_pid_windows(pid: str) -> bool:
|
|
168
|
+
"""Terminate process by PID on Windows.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
bool: Whether the process was successfully terminated.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
subprocess.run(
|
|
175
|
+
["taskkill", "/F", "/PID", pid],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
text=True,
|
|
178
|
+
check=True,
|
|
179
|
+
)
|
|
180
|
+
return True
|
|
181
|
+
except subprocess.CalledProcessError:
|
|
182
|
+
logger.error(f"Failed to terminate process PID {pid}")
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _kill_process_by_pid_unix(pid: str) -> bool:
|
|
187
|
+
"""Terminate process by PID on Unix/Linux.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
bool: Whether the process was successfully terminated.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
subprocess.run(
|
|
194
|
+
["kill", "-9", pid],
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
check=True,
|
|
198
|
+
)
|
|
199
|
+
return True
|
|
200
|
+
except subprocess.CalledProcessError:
|
|
201
|
+
logger.error(f"Failed to terminate process PID {pid}")
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main() -> None:
|
|
206
|
+
"""Terminate process."""
|
|
207
|
+
parser = argparse.ArgumentParser()
|
|
208
|
+
parser.add_argument("processes", type=str, nargs="+", help="Process to terminate.")
|
|
209
|
+
parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode.")
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--force",
|
|
212
|
+
"-f",
|
|
213
|
+
action="store_true",
|
|
214
|
+
help="Force termination even if match count exceeds threshold.",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
args = parser.parse_args()
|
|
218
|
+
|
|
219
|
+
if args.debug:
|
|
220
|
+
logger.setLevel(logging.DEBUG)
|
|
221
|
+
|
|
222
|
+
# Temporarily increase threshold if force flag is set
|
|
223
|
+
global _max_match_threshold
|
|
224
|
+
original_threshold = _max_match_threshold
|
|
225
|
+
if args.force:
|
|
226
|
+
_max_match_threshold = 1000 # Large number to effectively disable the limit
|
|
227
|
+
|
|
228
|
+
# Fetch process list once for the first process, then reuse for remaining
|
|
229
|
+
for i, process_name in enumerate(args.processes):
|
|
230
|
+
force_refresh = i == 0
|
|
231
|
+
kill_process(process_name, force_refresh=force_refresh)
|
|
232
|
+
|
|
233
|
+
# Restore original threshold
|
|
234
|
+
_max_match_threshold = original_threshold
|
|
235
|
+
|
|
236
|
+
return
|
sfi/which/which.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Find executable path for commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
is_windows = platform.system() == "Windows"
|
|
12
|
+
ext = ".exe" if is_windows else ""
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_executable(name: str, *, fuzzy: bool) -> tuple[str, str | None]:
|
|
19
|
+
"""Find executable path for commands cross-platform.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Tuple[str, Optional[str]]: Command name and path.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
# Select command based on system
|
|
26
|
+
match_name = name if not fuzzy else f"*{name}*{ext}"
|
|
27
|
+
cmd = ["where" if is_windows else "which", match_name]
|
|
28
|
+
|
|
29
|
+
# Run command and capture output
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
cmd,
|
|
32
|
+
stdout=subprocess.PIPE,
|
|
33
|
+
stderr=subprocess.DEVNULL,
|
|
34
|
+
text=True,
|
|
35
|
+
check=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Handle Windows multiple results case
|
|
39
|
+
paths = result.stdout.strip().split("\n")
|
|
40
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
41
|
+
# Check direct executable path on UNIX systems
|
|
42
|
+
if not is_windows and os.access(f"/usr/bin/{name}", os.X_OK):
|
|
43
|
+
return name, f"/usr/bin/{name}"
|
|
44
|
+
return name, None
|
|
45
|
+
else:
|
|
46
|
+
return (name, paths[0]) if is_windows else (name, result.stdout.strip())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
parser = argparse.ArgumentParser(description="Find executable path for commands.")
|
|
51
|
+
parser.add_argument("commands", type=str, nargs="+", help="Commands to query")
|
|
52
|
+
parser.add_argument("-f", "--fuzzy", action="store_true", help="Enable fuzzy matching")
|
|
53
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="Only print paths, no status symbols")
|
|
54
|
+
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
|
|
57
|
+
found_count = 0
|
|
58
|
+
fuzzy = args.fuzzy
|
|
59
|
+
quiet = args.quiet
|
|
60
|
+
|
|
61
|
+
for command in args.commands:
|
|
62
|
+
name, path = find_executable(command, fuzzy=fuzzy)
|
|
63
|
+
if path:
|
|
64
|
+
found_count += 1
|
|
65
|
+
if quiet:
|
|
66
|
+
logger.info(path)
|
|
67
|
+
else:
|
|
68
|
+
logger.info(f"✓ {name} -> {path}")
|
|
69
|
+
else:
|
|
70
|
+
if not quiet:
|
|
71
|
+
logger.info(f"✗ {name} -> not found")
|
|
72
|
+
|
|
73
|
+
if not quiet and len(args.commands) > 1:
|
|
74
|
+
logger.info(f"\nFound {found_count}/{len(args.commands)} commands")
|
pysfi-0.1.4.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
sfi/__init__.py,sha256=xigBx2zkhJtzVxIy_WQ4O8Wu4c5h9Lh_TFl7Xven-qk,74
|
|
2
|
-
sfi/alarmclock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
sfi/alarmclock/alarmclock.py,sha256=N_UYS5WLTpELLX13cimGP_xUjDwOWQmpig5LRzdnW2M,12142
|
|
4
|
-
sfi/bumpversion/__init__.py,sha256=0EjY5RZ96HUo4PoYDaUMOO7ZX3FCx1QPHfqvBXxG2ao,85
|
|
5
|
-
sfi/bumpversion/bumpversion.py,sha256=4_rZmrmHwMVSlidlNFpyxot5XII6PKAOFsq88MrGWw4,20200
|
|
6
|
-
sfi/embedinstall/embedinstall.py,sha256=Trw9NGOw0jLwfbIgkjJZMC3AUS_-IrPYgIQZg9TmiRQ,14518
|
|
7
|
-
sfi/filedate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
sfi/filedate/filedate.py,sha256=DpVp26lumE_Lz_4TgqUEX8IxtK3Y6yHSEFV8qJyegyk,3645
|
|
9
|
-
sfi/makepython/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
sfi/makepython/makepython.py,sha256=-fMGWvPlbW5cDvUPt6Cdy77tt1rN6n4iBegNpwzvlyM,10232
|
|
11
|
-
sfi/projectparse/projectparse.py,sha256=Ojg-z4lZEtjEBpJYWyznTgL307N45AxlQKnRkEH0P70,5525
|
|
12
|
-
sfi/pyloadergen/pyloadergen.py,sha256=_a1VPqEfYSCxInOwozRLMvluKzTUHDyUV_RtNAehnlc,31428
|
|
13
|
-
sfi/pypacker/fspacker.py,sha256=3tlS7qiWoH_kOzsp9eSWsQ-SY7-bSTugwfB-HIL69iE,3238
|
|
14
|
-
pysfi-0.1.4.dist-info/METADATA,sha256=oO1UM4mMejBhr4IODoaSX7J1aOVlR9rn79o6Nbga4x0,2755
|
|
15
|
-
pysfi-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
-
pysfi-0.1.4.dist-info/entry_points.txt,sha256=6XalkBAzJXRaYaPydlk_Q6Sr6aPOAv8v_5ZBdvg63Lk,367
|
|
17
|
-
pysfi-0.1.4.dist-info/RECORD,,
|