pysfi 0.1.10__py3-none-any.whl → 0.1.11__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 (47) hide show
  1. {pysfi-0.1.10.dist-info → pysfi-0.1.11.dist-info}/METADATA +7 -7
  2. pysfi-0.1.11.dist-info/RECORD +60 -0
  3. {pysfi-0.1.10.dist-info → pysfi-0.1.11.dist-info}/entry_points.txt +12 -2
  4. sfi/__init__.py +1 -1
  5. sfi/alarmclock/alarmclock.py +40 -40
  6. sfi/bumpversion/__init__.py +1 -1
  7. sfi/cleanbuild/cleanbuild.py +155 -0
  8. sfi/condasetup/condasetup.py +116 -0
  9. sfi/docscan/__init__.py +1 -1
  10. sfi/docscan/docscan_gui.py +1 -1
  11. sfi/docscan/lang/eng.py +152 -152
  12. sfi/docscan/lang/zhcn.py +170 -170
  13. sfi/filedate/filedate.py +185 -112
  14. sfi/gittool/__init__.py +2 -0
  15. sfi/gittool/gittool.py +401 -0
  16. sfi/llmclient/llmclient.py +592 -0
  17. sfi/llmquantize/llmquantize.py +480 -0
  18. sfi/llmserver/llmserver.py +335 -0
  19. sfi/makepython/makepython.py +2 -2
  20. sfi/pdfsplit/pdfsplit.py +4 -4
  21. sfi/pyarchive/pyarchive.py +418 -0
  22. sfi/pyembedinstall/pyembedinstall.py +629 -0
  23. sfi/pylibpack/pylibpack.py +813 -269
  24. sfi/pylibpack/rules/numpy.json +22 -0
  25. sfi/pylibpack/rules/pymupdf.json +10 -0
  26. sfi/pylibpack/rules/pyqt5.json +19 -0
  27. sfi/pylibpack/rules/pyside2.json +23 -0
  28. sfi/pylibpack/rules/scipy.json +23 -0
  29. sfi/pylibpack/rules/shiboken2.json +24 -0
  30. sfi/pyloadergen/pyloadergen.py +271 -572
  31. sfi/pypack/pypack.py +822 -471
  32. sfi/pyprojectparse/__init__.py +0 -0
  33. sfi/pyprojectparse/pyprojectparse.py +500 -0
  34. sfi/pysourcepack/pysourcepack.py +308 -369
  35. sfi/quizbase/__init__.py +0 -0
  36. sfi/quizbase/quizbase.py +828 -0
  37. sfi/quizbase/quizbase_gui.py +987 -0
  38. sfi/regexvalidate/__init__.py +0 -0
  39. sfi/regexvalidate/regex_help.html +284 -0
  40. sfi/regexvalidate/regexvalidate.py +468 -0
  41. sfi/taskkill/taskkill.py +0 -2
  42. pysfi-0.1.10.dist-info/RECORD +0 -39
  43. sfi/embedinstall/embedinstall.py +0 -478
  44. sfi/projectparse/projectparse.py +0 -152
  45. {pysfi-0.1.10.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
  46. /sfi/{embedinstall → llmquantize}/__init__.py +0 -0
  47. /sfi/{projectparse → pyembedinstall}/__init__.py +0 -0
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import json
5
+ import logging
6
+ import os
7
+ import pathlib
8
+ import sys
9
+ from pathlib import Path
10
+ from types import SimpleNamespace
11
+ from typing import ClassVar
12
+
13
+ from PySide2.QtCore import QProcess, QTextStream, QUrl
14
+ from PySide2.QtGui import (
15
+ QBrush,
16
+ QColor,
17
+ QDesktopServices,
18
+ QFont,
19
+ QMoveEvent,
20
+ QResizeEvent,
21
+ QTextCharFormat,
22
+ QTextCursor,
23
+ )
24
+ from PySide2.QtWidgets import (
25
+ QApplication,
26
+ QFileDialog,
27
+ QGroupBox,
28
+ QHBoxLayout,
29
+ QLabel,
30
+ QLineEdit,
31
+ QMainWindow,
32
+ QPushButton,
33
+ QSpinBox,
34
+ QTextEdit,
35
+ QVBoxLayout,
36
+ QWidget,
37
+ )
38
+
39
+ CONFIG_FILE = Path.home() / ".sfi" / "llmserver.json"
40
+ logging.basicConfig(level="INFO", format="%(message)s")
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class LLMServerConfig(SimpleNamespace):
45
+ """Llama local model server configuration."""
46
+
47
+ TITLE: str = "Llama Local Model Server"
48
+ WIN_SIZE: ClassVar[list[int]] = [800, 800]
49
+ WIN_POS: ClassVar[list[int]] = [200, 200]
50
+ MODEL_PATH: str = ""
51
+
52
+ URL: str = "http://127.0.0.1"
53
+ LISTEN_PORT: int = 8080
54
+ LISTEN_PORT_RNG: ClassVar[list[int]] = [1024, 65535]
55
+ THREAD_COUNT_RNG: ClassVar[list[int]] = [1, 24]
56
+ THREAD_COUNT: int = 4
57
+
58
+ def __init__(self) -> None:
59
+ if CONFIG_FILE.exists():
60
+ logger.info("Loading configuration from %s", CONFIG_FILE)
61
+ self.__dict__.update(json.loads(CONFIG_FILE.read_text()))
62
+ else:
63
+ logger.info("Using default configuration")
64
+
65
+ def save(self) -> None:
66
+ """Save configuration."""
67
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
68
+ CONFIG_FILE.write_text(json.dumps(vars(self), indent=4))
69
+
70
+
71
+ conf = LLMServerConfig()
72
+ atexit.register(conf.save)
73
+
74
+
75
+ class LlamaServerGUI(QMainWindow):
76
+ """Llama local model server GUI."""
77
+
78
+ # Color constants
79
+ COLOR_ERROR = QColor(255, 0, 0)
80
+ COLOR_WARNING = QColor(255, 165, 0)
81
+ COLOR_INFO = QColor(0, 0, 0)
82
+
83
+ # Process constants
84
+ PROCESS_TERMINATE_TIMEOUT_MS = 2000
85
+
86
+ def __init__(self) -> None:
87
+ super().__init__()
88
+ self.setWindowTitle(conf.TITLE)
89
+ self.setGeometry(*conf.WIN_POS, *conf.WIN_SIZE)
90
+
91
+ self.process: QProcess
92
+ self.init_ui()
93
+ self.setup_process()
94
+
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...")
100
+
101
+ def init_ui(self) -> None:
102
+ """Initialize user interface."""
103
+ # Main layout
104
+ main_widget = QWidget()
105
+ main_layout = QVBoxLayout() # type: ignore
106
+
107
+ # Configuration panel
108
+ config_group = QGroupBox("Server Configuration")
109
+ config_layout = QVBoxLayout(config_group)
110
+
111
+ # Model path selection
112
+ model_path_layout = QHBoxLayout() # type: ignore
113
+ model_path_layout.addWidget(QLabel("Model Path:"))
114
+ self.model_path_input = QLineEdit()
115
+
116
+ model_path_layout.addWidget(self.model_path_input)
117
+ self.load_model_btn = QPushButton("Browse...")
118
+ self.load_model_btn.clicked.connect(self.on_load_model) # type: ignore
119
+ model_path_layout.addWidget(self.load_model_btn)
120
+ config_layout.addLayout(model_path_layout)
121
+
122
+ # Server parameters
123
+ params_layout = QHBoxLayout() # type: ignore
124
+ params_layout.addStretch(1)
125
+ params_layout.addWidget(QLabel("Port:"))
126
+ self.port_spin = QSpinBox()
127
+ self.port_spin.setRange(*conf.LISTEN_PORT_RNG)
128
+ self.port_spin.setValue(conf.LISTEN_PORT)
129
+ params_layout.addWidget(self.port_spin)
130
+ self.port_spin.valueChanged.connect(self.on_config_changed) # type: ignore
131
+
132
+ params_layout.addWidget(QLabel("Threads:"))
133
+ self.threads_spin = QSpinBox()
134
+ self.threads_spin.setRange(*conf.THREAD_COUNT_RNG)
135
+ self.threads_spin.setValue(conf.THREAD_COUNT)
136
+ params_layout.addWidget(self.threads_spin)
137
+ config_layout.addLayout(params_layout)
138
+ self.threads_spin.valueChanged.connect(self.on_config_changed) # type: ignore
139
+
140
+ config_group.setLayout(config_layout)
141
+ main_layout.addWidget(config_group)
142
+
143
+ # Control buttons
144
+ control_layout = QHBoxLayout() # type: ignore
145
+ self.start_btn = QPushButton("Start Server")
146
+ self.start_btn.clicked.connect(self.toggle_server) # type: ignore
147
+ self.browser_btn = QPushButton("Start Browser")
148
+ self.browser_btn.setEnabled(False)
149
+ self.browser_btn.clicked.connect(self.on_start_browser) # type: ignore
150
+ control_layout.addWidget(self.start_btn)
151
+ control_layout.addWidget(self.browser_btn)
152
+ main_layout.addLayout(control_layout)
153
+
154
+ # Output display
155
+ output_group = QGroupBox("Server Output")
156
+ output_layout = QVBoxLayout(output_group)
157
+ self.output_area = QTextEdit("")
158
+ self.output_area.setReadOnly(True)
159
+ self.output_area.setLineWrapMode(QTextEdit.NoWrap) # type: ignore
160
+
161
+ # Set colors for different message types
162
+ self.error_format = self.create_text_format(self.COLOR_ERROR)
163
+ self.warning_format = self.create_text_format(self.COLOR_WARNING)
164
+ self.info_format = self.create_text_format(self.COLOR_INFO)
165
+
166
+ output_layout.addWidget(self.output_area)
167
+ output_group.setLayout(output_layout)
168
+ main_layout.addWidget(output_group)
169
+
170
+ main_widget.setLayout(main_layout)
171
+ self.setCentralWidget(main_widget)
172
+
173
+ @staticmethod
174
+ def create_text_format(color: QColor) -> QTextCharFormat:
175
+ """Create text format.
176
+
177
+ Args:
178
+ color: Text color.
179
+
180
+ Returns:
181
+ Text format.
182
+ """
183
+ text_format = QTextCharFormat()
184
+ text_format.setForeground(QBrush(color))
185
+ return text_format
186
+
187
+ def setup_process(self) -> None:
188
+ """Initialize process."""
189
+ self.process = QProcess(self)
190
+ self.process.readyReadStandardOutput.connect(self.handle_stdout) # type: ignore
191
+ self.process.readyReadStandardError.connect(self.handle_stderr) # type: ignore
192
+ self.process.finished.connect(self.on_process_finished) # type: ignore
193
+
194
+ def on_config_changed(self) -> None:
195
+ """Configuration changed."""
196
+ conf.MODEL_PATH = self.model_path_input.text().strip()
197
+ conf.LISTEN_PORT = self.port_spin.value()
198
+ conf.THREAD_COUNT = self.threads_spin.value()
199
+ # Configuration will be saved on exit via atexit.register
200
+
201
+ def on_load_model(self) -> None:
202
+ """Select model file."""
203
+ initial_dir = (
204
+ conf.MODEL_PATH if os.path.exists(conf.MODEL_PATH) else str(Path.home())
205
+ )
206
+ path, _ = QFileDialog.getOpenFileName(
207
+ self,
208
+ "Select Model File",
209
+ initial_dir,
210
+ "Model Files (*.bin *.gguf)",
211
+ )
212
+
213
+ if path:
214
+ conf.MODEL_PATH = path
215
+ self.model_path_input.setText(os.path.normpath(path))
216
+
217
+ def toggle_server(self) -> None:
218
+ """Start or stop server."""
219
+ if self.process.state() == QProcess.Running:
220
+ self.stop_server()
221
+ else:
222
+ self.start_server()
223
+
224
+ def start_server(self) -> None:
225
+ """Start server."""
226
+ model_path = pathlib.Path(self.model_path_input.text().strip())
227
+ if not model_path.exists():
228
+ self.append_output(
229
+ "Error: Invalid model file path\n",
230
+ self.error_format,
231
+ )
232
+ return
233
+
234
+ os.chdir(str(model_path.parent))
235
+ cmd = [
236
+ "llama-server",
237
+ "--model",
238
+ model_path.name,
239
+ "--port",
240
+ str(self.port_spin.value()),
241
+ "--threads",
242
+ str(self.threads_spin.value()),
243
+ ]
244
+
245
+ self.append_output(f"Start: {' '.join(cmd)}\n", self.info_format)
246
+
247
+ try:
248
+ self.process.start(cmd[0], cmd[1:])
249
+ self.update_ui_state(running=True)
250
+ except Exception as e:
251
+ self.append_output(f"Start failed: {e!s}\n", self.error_format)
252
+
253
+ def stop_server(self) -> None:
254
+ """Stop server."""
255
+ if self.process.state() == QProcess.Running:
256
+ self.append_output("Stopping server...\n", self.info_format)
257
+ self.process.terminate()
258
+ if not self.process.waitForFinished(self.PROCESS_TERMINATE_TIMEOUT_MS):
259
+ self.process.kill()
260
+
261
+ @staticmethod
262
+ def on_start_browser() -> None:
263
+ """Start browser."""
264
+ QDesktopServices.openUrl(QUrl(f"{conf.URL}:{conf.LISTEN_PORT}"))
265
+
266
+ def on_process_finished(self, exit_code: int, exit_status: int) -> None:
267
+ """Process finished."""
268
+ self.append_output(
269
+ f"\nServer stopped, Exit code: {exit_code}, Status: {exit_status}\n",
270
+ self.info_format,
271
+ )
272
+ self.update_ui_state(running=False)
273
+
274
+ def handle_stdout(self) -> None:
275
+ """Handle standard output."""
276
+ data = self.process.readAllStandardOutput()
277
+ text = QTextStream(data).readAll()
278
+ self.append_output(text, self.info_format)
279
+
280
+ def handle_stderr(self) -> None:
281
+ """Handle standard error."""
282
+ data = self.process.readAllStandardError()
283
+ text = QTextStream(data).readAll()
284
+ self.append_output(text, self.error_format)
285
+
286
+ def append_output(
287
+ self,
288
+ text: str,
289
+ text_format: QTextCharFormat | None = None,
290
+ ) -> None:
291
+ """Append output."""
292
+ cursor: QTextCursor = self.output_area.textCursor()
293
+ cursor.movePosition(QTextCursor.MoveOperation.End)
294
+
295
+ if text_format:
296
+ cursor.setCharFormat(text_format)
297
+
298
+ cursor.insertText(text)
299
+
300
+ self.output_area.setTextCursor(cursor)
301
+ self.output_area.ensureCursorVisible()
302
+
303
+ def update_ui_state(self, *, running: bool) -> None:
304
+ """Update UI state."""
305
+ self.model_path_input.setEnabled(not running)
306
+ self.load_model_btn.setEnabled(not running)
307
+ self.port_spin.setEnabled(not running)
308
+ self.threads_spin.setEnabled(not running)
309
+ self.browser_btn.setEnabled(running)
310
+
311
+ if running:
312
+ self.start_btn.setText("Stop Server")
313
+ else:
314
+ self.start_btn.setText("Start Server")
315
+
316
+ def moveEvent(self, event: QMoveEvent) -> None: # noqa: N802
317
+ """Handle window move event."""
318
+ top_left = self.geometry().topLeft()
319
+ conf.WIN_POS = [top_left.x(), top_left.y()]
320
+ return super().moveEvent(event)
321
+
322
+ def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
323
+ """Handle window resize event."""
324
+ geometry = self.geometry()
325
+ conf.WIN_SIZE = [geometry.width(), geometry.height()]
326
+ return super().resizeEvent(event)
327
+
328
+
329
+ def main() -> None:
330
+ """Main entry point."""
331
+ app = QApplication(sys.argv)
332
+ app.setFont(QFont("Consolas", 12))
333
+ window = LlamaServerGUI()
334
+ window.show()
335
+ sys.exit(app.exec_())
@@ -66,7 +66,7 @@ def _get_build_command_from_toml(directory: Path) -> str | None:
66
66
  return None
67
67
 
68
68
 
69
- def _get_build_command(directory: Path):
69
+ def _get_build_command(directory: Path) -> str | None:
70
70
  """Get build command from directory."""
71
71
  project_path = directory / "pyproject.toml"
72
72
  if project_path.is_file():
@@ -93,7 +93,7 @@ def _clean(root_dir: Path = cwd):
93
93
  _run_command(["rm", "-rf", "dist", "build", "*.egg-info"], root_dir)
94
94
 
95
95
 
96
- def main():
96
+ def main() -> None:
97
97
  # Get build command
98
98
  build_command = _get_build_command(cwd) or ""
99
99
  commands = [
sfi/pdfsplit/pdfsplit.py CHANGED
@@ -28,7 +28,7 @@ def parse_page_ranges(range_str: str, total_pages: int) -> list[int]:
28
28
  return pages
29
29
 
30
30
 
31
- def split_by_number(input_file: Path, output_file: Path, number: int):
31
+ def split_by_number(input_file: Path, output_file: Path, number: int) -> None:
32
32
  """Split PDF into specified number of parts evenly."""
33
33
  doc = fitz.open(input_file)
34
34
  total_pages = doc.page_count
@@ -65,7 +65,7 @@ def split_by_number(input_file: Path, output_file: Path, number: int):
65
65
  doc.close()
66
66
 
67
67
 
68
- def split_by_size(input_file: Path, output_file: Path, size: int):
68
+ def split_by_size(input_file: Path, output_file: Path, size: int) -> None:
69
69
  """Split PDF into parts with specified page size."""
70
70
  doc = fitz.open(input_file)
71
71
  total_pages = doc.page_count
@@ -93,7 +93,7 @@ def split_by_size(input_file: Path, output_file: Path, size: int):
93
93
  doc.close()
94
94
 
95
95
 
96
- def split_by_range(input_file: Path, output_file: Path, range_str: str):
96
+ def split_by_range(input_file: Path, output_file: Path, range_str: str) -> None:
97
97
  """Extract specific pages from PDF based on range string."""
98
98
  doc = fitz.open(input_file)
99
99
  total_pages = doc.page_count
@@ -121,7 +121,7 @@ def split_by_range(input_file: Path, output_file: Path, range_str: str):
121
121
  logger.info(f"Created output file: {output_file} ({len(pages)} pages)")
122
122
 
123
123
 
124
- def main():
124
+ def main() -> None:
125
125
  parser = argparse.ArgumentParser(description="Split PDF files")
126
126
  parser.add_argument("input", help="Input PDF file")
127
127
  parser.add_argument("output", nargs="?", help="Output PDF file (optional for -n and -s modes)")