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.
@@ -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")
@@ -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,,