pysfi 0.1.12__py3-none-any.whl → 0.1.14__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.
Files changed (42) hide show
  1. {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/METADATA +1 -1
  2. pysfi-0.1.14.dist-info/RECORD +68 -0
  3. {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/entry_points.txt +3 -0
  4. sfi/__init__.py +19 -2
  5. sfi/alarmclock/__init__.py +3 -0
  6. sfi/alarmclock/alarmclock.py +23 -40
  7. sfi/bumpversion/__init__.py +3 -1
  8. sfi/bumpversion/bumpversion.py +64 -15
  9. sfi/cleanbuild/__init__.py +3 -0
  10. sfi/cleanbuild/cleanbuild.py +5 -1
  11. sfi/cli.py +25 -4
  12. sfi/condasetup/__init__.py +1 -0
  13. sfi/condasetup/condasetup.py +91 -76
  14. sfi/docdiff/__init__.py +1 -0
  15. sfi/docdiff/docdiff.py +3 -2
  16. sfi/docscan/__init__.py +1 -1
  17. sfi/docscan/docscan.py +78 -23
  18. sfi/docscan/docscan_gui.py +152 -48
  19. sfi/filedate/filedate.py +12 -5
  20. sfi/img2pdf/img2pdf.py +453 -0
  21. sfi/llmclient/llmclient.py +31 -8
  22. sfi/llmquantize/llmquantize.py +76 -37
  23. sfi/llmserver/__init__.py +1 -0
  24. sfi/llmserver/llmserver.py +63 -13
  25. sfi/makepython/makepython.py +1145 -201
  26. sfi/pdfsplit/pdfsplit.py +45 -12
  27. sfi/pyarchive/__init__.py +1 -0
  28. sfi/pyarchive/pyarchive.py +908 -278
  29. sfi/pyembedinstall/pyembedinstall.py +88 -89
  30. sfi/pylibpack/pylibpack.py +561 -463
  31. sfi/pyloadergen/pyloadergen.py +372 -218
  32. sfi/pypack/pypack.py +510 -959
  33. sfi/pyprojectparse/pyprojectparse.py +337 -40
  34. sfi/pysourcepack/__init__.py +1 -0
  35. sfi/pysourcepack/pysourcepack.py +210 -131
  36. sfi/quizbase/quizbase_gui.py +2 -2
  37. sfi/taskkill/taskkill.py +168 -59
  38. sfi/which/which.py +11 -3
  39. pysfi-0.1.12.dist-info/RECORD +0 -62
  40. sfi/workflowengine/workflowengine.py +0 -444
  41. {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/WHEEL +0 -0
  42. /sfi/{workflowengine → img2pdf}/__init__.py +0 -0
@@ -8,8 +8,8 @@ import logging
8
8
  import pathlib
9
9
  import subprocess
10
10
  import sys
11
+ from dataclasses import dataclass
11
12
  from pathlib import Path
12
- from types import SimpleNamespace
13
13
 
14
14
  from PySide2.QtCore import QThread, Signal, Slot
15
15
  from PySide2.QtGui import QMoveEvent, QResizeEvent
@@ -29,37 +29,65 @@ from PySide2.QtWidgets import (
29
29
  QWidget,
30
30
  )
31
31
 
32
- CONFIG_FILE = Path.home() / ".sfi" / "llmquantize.json"
32
+ CONFIG_FILE = Path.home() / ".pysfi" / "llmquantize.json"
33
33
 
34
- logging.basicConfig(level=logging.INFO)
34
+ logging.basicConfig(
35
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
36
+ )
35
37
  logger = logging.getLogger(__name__)
36
38
 
39
+ __version__ = "1.0.0"
40
+ __build__ = "20260204"
41
+
37
42
 
38
- class QuantizerConfig(SimpleNamespace):
43
+ @dataclass
44
+ class QuantizerConfig:
39
45
  """GGUF量化转换工具配置."""
40
46
 
41
47
  TITLE: str = "GGUF量化转换工具"
42
- WIN_SIZE: list[int] = [600, 500] # noqa: RUF012
43
- WIN_POS: list[int] = [100, 100] # noqa: RUF012
48
+ WIN_SIZE: list[int] = None
49
+ WIN_POS: list[int] = None
44
50
  LAST_INPUT_FILE: str = ""
45
- SELECTED_QUANTS: list[str] = ["Q4_K_M", "Q5_K_M"] # noqa: RUF012
51
+ SELECTED_QUANTS: list[str] = None
52
+
53
+ def __post_init__(self) -> None:
54
+ """初始化默认值并加载配置文件."""
55
+ # 初始化默认值
56
+ if self.WIN_SIZE is None:
57
+ self.WIN_SIZE = [600, 500]
58
+ if self.WIN_POS is None:
59
+ self.WIN_POS = [100, 100]
60
+ if self.SELECTED_QUANTS is None:
61
+ self.SELECTED_QUANTS = ["Q4_K_M", "Q5_K_M"]
46
62
 
47
- def __init__(self) -> None:
48
63
  if CONFIG_FILE.exists():
49
64
  logger.info("Loading configuration from %s", CONFIG_FILE)
50
65
  try:
51
- # 直接更新,忽略无效字段
52
- self.__dict__.update(json.loads(CONFIG_FILE.read_text()))
53
- except (json.JSONDecodeError, TypeError) as e:
66
+ config_data = json.loads(CONFIG_FILE.read_text())
67
+ # 更新实例属性,只更新存在的属性
68
+ for key, value in config_data.items():
69
+ if hasattr(self, key):
70
+ setattr(self, key, value)
71
+ except (json.JSONDecodeError, TypeError, AttributeError) as e:
54
72
  logger.warning("Failed to load configuration: %s", e)
55
73
  logger.info("Using default configuration")
56
74
  else:
57
75
  logger.info("Using default configuration")
58
76
 
59
77
  def save(self) -> None:
60
- """保存配置."""
78
+ """保存配置到文件."""
61
79
  CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
62
- CONFIG_FILE.write_text(json.dumps(vars(self), indent=4))
80
+ # 将数据类转换为字典进行JSON序列化
81
+ config_dict = {}
82
+ for attr_name in dir(self):
83
+ if not attr_name.startswith("_") and attr_name not in ["TITLE"]:
84
+ try:
85
+ attr_value = getattr(self, attr_name)
86
+ if not callable(attr_value):
87
+ config_dict[attr_name] = attr_value
88
+ except AttributeError:
89
+ continue
90
+ CONFIG_FILE.write_text(json.dumps(config_dict, indent=4))
63
91
 
64
92
 
65
93
  conf = QuantizerConfig()
@@ -69,8 +97,11 @@ atexit.register(conf.save)
69
97
  def _process_gguf_stem(filename: str) -> str:
70
98
  """处理文件名, 移除可能的F16后缀.
71
99
 
100
+ Args:
101
+ filename: 输入的文件名(不含扩展名)
102
+
72
103
  Returns:
73
- str: 处理后的文件名
104
+ str: 处理后的文件名, 移除了F16后缀(如果存在)
74
105
  """
75
106
  if filename.upper().endswith("-F16"):
76
107
  filename = filename[:-4] # 移除-F16后缀
@@ -78,7 +109,13 @@ def _process_gguf_stem(filename: str) -> str:
78
109
 
79
110
 
80
111
  class QuantizationWorker(QThread):
81
- """量化执行线程Worker."""
112
+ """量化执行线程Worker.
113
+
114
+ Attributes:
115
+ progress_msg_updated: 进度消息更新信号
116
+ progress_count_updated: 进度数值更新信号
117
+ is_finished: 完成信号
118
+ """
82
119
 
83
120
  progress_msg_updated = Signal(str)
84
121
  progress_count_updated = Signal(int)
@@ -89,6 +126,12 @@ class QuantizationWorker(QThread):
89
126
  input_file: pathlib.Path,
90
127
  quant_types: list[str],
91
128
  ) -> None:
129
+ """初始化量化Worker.
130
+
131
+ Args:
132
+ input_file: 输入的F16 GGUF文件路径
133
+ quant_types: 需要转换的量化类型列表
134
+ """
92
135
  super().__init__()
93
136
 
94
137
  self.input_file = input_file
@@ -111,7 +154,7 @@ class QuantizationWorker(QThread):
111
154
  f"正在转换到 {quant_type} 格式...",
112
155
  )
113
156
 
114
- # 构建命令行参数(确保所有参数都是字符串)
157
+ # 构建命令行参数
115
158
  cmd = [
116
159
  "llama-quantize",
117
160
  str(self.input_file.name),
@@ -119,7 +162,7 @@ class QuantizationWorker(QThread):
119
162
  quant_type,
120
163
  ]
121
164
 
122
- # 执行转换命令(使用 cwd 参数避免全局目录变更)
165
+ # 执行转换命令
123
166
  try:
124
167
  process = subprocess.Popen(
125
168
  cmd,
@@ -284,7 +327,7 @@ class GGUFQuantizerGUI(QMainWindow):
284
327
  self.setCentralWidget(main_widget)
285
328
 
286
329
  def select_file(self) -> None:
287
- """选择文件."""
330
+ """选择F16 GGUF文件."""
288
331
  # 使用上次选择的目录作为初始目录
289
332
  initial_dir = ""
290
333
  if conf.LAST_INPUT_FILE and pathlib.Path(conf.LAST_INPUT_FILE).exists():
@@ -333,21 +376,25 @@ class GGUFQuantizerGUI(QMainWindow):
333
376
  filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
334
377
  expected_file = dir_path / filename
335
378
  if expected_file.exists():
336
- # 文件已存在,标记但不禁用,允许用户选择重新生成
379
+ # 文件已存在,标记并禁用,防止重复生成
337
380
  self.quant_checks[quant_type].setText(
338
381
  f"{self.quant_types[quant_type]} (已存在)",
339
382
  )
340
383
  self.quant_checks[quant_type].setStyleSheet("color: orange;")
384
+ self.quant_checks[quant_type].setChecked(True)
385
+ self.quant_checks[quant_type].setEnabled(False)
341
386
  else:
342
387
  self.quant_checks[quant_type].setText(
343
388
  self.quant_types[quant_type],
344
389
  )
345
390
  self.quant_checks[quant_type].setStyleSheet("")
391
+ self.quant_checks[quant_type].setEnabled(True)
346
392
 
347
393
  def _scroll_to_bottom(self) -> None:
348
394
  """滚动输出框到底部."""
349
395
  scrollbar = self.output_text.verticalScrollBar()
350
- scrollbar.setValue(scrollbar.maximum())
396
+ if scrollbar:
397
+ scrollbar.setValue(scrollbar.maximum())
351
398
 
352
399
  def on_quant_type_changed(self, _state: int) -> None:
353
400
  """量化类型变更时保存配置."""
@@ -369,7 +416,7 @@ class GGUFQuantizerGUI(QMainWindow):
369
416
  return super().resizeEvent(event)
370
417
 
371
418
  def start_conversion(self) -> None:
372
- """开始转换."""
419
+ """开始转换量化任务."""
373
420
  # 检查是否已有任务在运行
374
421
  if self.worker and self.worker.isRunning():
375
422
  self.output_text.append("已有转换任务正在进行, 请等待完成")
@@ -377,7 +424,9 @@ class GGUFQuantizerGUI(QMainWindow):
377
424
  return
378
425
 
379
426
  selected_quants: list[str] = [
380
- q for q, check in self.quant_checks.items() if check.isChecked()
427
+ q
428
+ for q, check in self.quant_checks.items()
429
+ if check.isChecked() and check.isEnabled()
381
430
  ]
382
431
 
383
432
  if not selected_quants:
@@ -390,19 +439,8 @@ class GGUFQuantizerGUI(QMainWindow):
390
439
  self._scroll_to_bottom()
391
440
  return
392
441
 
393
- # 检查是否有已存在的文件将被覆盖
394
- existing_files = []
395
- for quant_type in selected_quants:
396
- filename = f"{_process_gguf_stem(self.input_file.stem)}-{quant_type}.gguf"
397
- expected_file = self.input_file.parent / filename
398
- if expected_file.exists():
399
- existing_files.append(filename)
400
-
401
- if existing_files:
402
- self.output_text.append("警告: 将覆盖以下已存在文件:")
403
- for existing_file in existing_files:
404
- self.output_text.append(f" - {existing_file}")
405
- self._scroll_to_bottom()
442
+ # 注意:由于selected_quants只包含启用的复选框,
443
+ # 所以不会包含已存在的禁用文件,无需额外检查覆盖
406
444
 
407
445
  self.convert_btn.setEnabled(False)
408
446
  self.progress_bar.setValue(0)
@@ -459,6 +497,7 @@ class GGUFQuantizerGUI(QMainWindow):
459
497
 
460
498
 
461
499
  def main() -> None:
500
+ """主程序入口."""
462
501
  app = QApplication(sys.argv)
463
502
 
464
503
  # 检查是否安装了llama.cpp
@@ -469,8 +508,8 @@ def main() -> None:
469
508
  check=False,
470
509
  )
471
510
  except FileNotFoundError:
472
- logger.exception("错误: 未找到llama.cpp/quantize工具")
473
- logger.exception(
511
+ logger.error("错误: 未找到llama.cpp/quantize工具")
512
+ logger.error(
474
513
  "请确保已编译llama.cpp并将quantize工具放在llama.cpp/目录下",
475
514
  )
476
515
  sys.exit(1)
@@ -0,0 +1 @@
1
+
@@ -6,8 +6,8 @@ import logging
6
6
  import os
7
7
  import pathlib
8
8
  import sys
9
+ from dataclasses import dataclass
9
10
  from pathlib import Path
10
- from types import SimpleNamespace
11
11
  from typing import ClassVar
12
12
 
13
13
  from PySide2.QtCore import QProcess, QTextStream, QUrl
@@ -36,36 +36,61 @@ from PySide2.QtWidgets import (
36
36
  QWidget,
37
37
  )
38
38
 
39
- CONFIG_FILE = Path.home() / ".sfi" / "llmserver.json"
39
+ CONFIG_FILE = Path.home() / ".pysfi" / "llmserver.json"
40
40
  logging.basicConfig(level="INFO", format="%(message)s")
41
41
  logger = logging.getLogger(__name__)
42
42
 
43
43
 
44
- class LLMServerConfig(SimpleNamespace):
44
+ @dataclass
45
+ class LLMServerConfig:
45
46
  """Llama local model server configuration."""
46
47
 
47
48
  TITLE: str = "Llama Local Model Server"
48
49
  WIN_SIZE: ClassVar[list[int]] = [800, 800]
49
50
  WIN_POS: ClassVar[list[int]] = [200, 200]
50
51
  MODEL_PATH: str = ""
51
-
52
52
  URL: str = "http://127.0.0.1"
53
53
  LISTEN_PORT: int = 8080
54
54
  LISTEN_PORT_RNG: ClassVar[list[int]] = [1024, 65535]
55
55
  THREAD_COUNT_RNG: ClassVar[list[int]] = [1, 24]
56
56
  THREAD_COUNT: int = 4
57
57
 
58
- def __init__(self) -> None:
58
+ _loaded_from_file: bool = False
59
+
60
+ def __post_init__(self) -> None:
59
61
  if CONFIG_FILE.exists():
60
62
  logger.info("Loading configuration from %s", CONFIG_FILE)
61
- self.__dict__.update(json.loads(CONFIG_FILE.read_text()))
63
+ try:
64
+ config_data = json.loads(CONFIG_FILE.read_text())
65
+ # Update instance attributes with loaded values
66
+ for key, value in config_data.items():
67
+ if hasattr(self, key):
68
+ setattr(self, key, value)
69
+ self._loaded_from_file = True
70
+ except (json.JSONDecodeError, TypeError, AttributeError) as e:
71
+ logger.error(f"Error loading config from {CONFIG_FILE}: {e}")
62
72
  else:
63
73
  logger.info("Using default configuration")
64
74
 
65
75
  def save(self) -> None:
66
76
  """Save configuration."""
67
- CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
68
- CONFIG_FILE.write_text(json.dumps(vars(self), indent=4))
77
+ try:
78
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
79
+ # Convert dataclass to dict for JSON serialization
80
+ config_dict = {}
81
+ for attr_name in dir(self):
82
+ if not attr_name.startswith("_"):
83
+ try:
84
+ attr_value = getattr(self, attr_name)
85
+ if not callable(attr_value):
86
+ config_dict[attr_name] = attr_value
87
+ except AttributeError:
88
+ continue
89
+ CONFIG_FILE.write_text(json.dumps(config_dict, indent=4))
90
+ logger.info(f"Configuration saved to {CONFIG_FILE}")
91
+ except Exception as e:
92
+ logger.error(f"Failed to save configuration: {e}")
93
+ raise
69
94
 
70
95
 
71
96
  conf = LLMServerConfig()
@@ -92,11 +117,8 @@ class LlamaServerGUI(QMainWindow):
92
117
  self.init_ui()
93
118
  self.setup_process()
94
119
 
95
- model_path = conf.MODEL_PATH
96
- if model_path:
97
- self.model_path_input.setText(str(model_path))
98
- else:
99
- self.model_path_input.setPlaceholderText("Choose model file...")
120
+ # Apply loaded configuration to UI controls
121
+ self.apply_config_to_ui()
100
122
 
101
123
  def init_ui(self) -> None:
102
124
  """Initialize user interface."""
@@ -191,6 +213,19 @@ class LlamaServerGUI(QMainWindow):
191
213
  self.process.readyReadStandardError.connect(self.handle_stderr) # type: ignore
192
214
  self.process.finished.connect(self.on_process_finished) # type: ignore
193
215
 
216
+ def apply_config_to_ui(self) -> None:
217
+ """Apply loaded configuration to UI controls."""
218
+ # Set model path
219
+ model_path = conf.MODEL_PATH
220
+ if model_path:
221
+ self.model_path_input.setText(str(model_path))
222
+ else:
223
+ self.model_path_input.setPlaceholderText("Choose model file...")
224
+
225
+ # Set port and thread values
226
+ self.port_spin.setValue(conf.LISTEN_PORT)
227
+ self.threads_spin.setValue(conf.THREAD_COUNT)
228
+
194
229
  def on_config_changed(self) -> None:
195
230
  """Configuration changed."""
196
231
  conf.MODEL_PATH = self.model_path_input.text().strip()
@@ -325,6 +360,21 @@ class LlamaServerGUI(QMainWindow):
325
360
  conf.WIN_SIZE = [geometry.width(), geometry.height()]
326
361
  return super().resizeEvent(event)
327
362
 
363
+ def closeEvent(self, event) -> None: # noqa: N802
364
+ """Handle window close event to ensure configuration is saved."""
365
+ try:
366
+ # Save current configuration
367
+ conf.save()
368
+ logger.info("Configuration saved successfully on exit")
369
+ except Exception as e:
370
+ logger.error(f"Failed to save configuration on exit: {e}")
371
+ finally:
372
+ # Also stop server if running
373
+ if hasattr(self, "process") and self.process.state() == QProcess.Running:
374
+ self.process.terminate()
375
+ self.process.waitForFinished(2000) # Wait up to 2 seconds
376
+ event.accept()
377
+
328
378
 
329
379
  def main() -> None:
330
380
  """Main entry point."""