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.
- {pysfi-0.1.10.dist-info → pysfi-0.1.11.dist-info}/METADATA +7 -7
- pysfi-0.1.11.dist-info/RECORD +60 -0
- {pysfi-0.1.10.dist-info → pysfi-0.1.11.dist-info}/entry_points.txt +12 -2
- sfi/__init__.py +1 -1
- sfi/alarmclock/alarmclock.py +40 -40
- sfi/bumpversion/__init__.py +1 -1
- sfi/cleanbuild/cleanbuild.py +155 -0
- sfi/condasetup/condasetup.py +116 -0
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan_gui.py +1 -1
- sfi/docscan/lang/eng.py +152 -152
- sfi/docscan/lang/zhcn.py +170 -170
- sfi/filedate/filedate.py +185 -112
- sfi/gittool/__init__.py +2 -0
- sfi/gittool/gittool.py +401 -0
- sfi/llmclient/llmclient.py +592 -0
- sfi/llmquantize/llmquantize.py +480 -0
- sfi/llmserver/llmserver.py +335 -0
- sfi/makepython/makepython.py +2 -2
- sfi/pdfsplit/pdfsplit.py +4 -4
- sfi/pyarchive/pyarchive.py +418 -0
- sfi/pyembedinstall/pyembedinstall.py +629 -0
- sfi/pylibpack/pylibpack.py +813 -269
- sfi/pylibpack/rules/numpy.json +22 -0
- sfi/pylibpack/rules/pymupdf.json +10 -0
- sfi/pylibpack/rules/pyqt5.json +19 -0
- sfi/pylibpack/rules/pyside2.json +23 -0
- sfi/pylibpack/rules/scipy.json +23 -0
- sfi/pylibpack/rules/shiboken2.json +24 -0
- sfi/pyloadergen/pyloadergen.py +271 -572
- sfi/pypack/pypack.py +822 -471
- sfi/pyprojectparse/__init__.py +0 -0
- sfi/pyprojectparse/pyprojectparse.py +500 -0
- sfi/pysourcepack/pysourcepack.py +308 -369
- sfi/quizbase/__init__.py +0 -0
- sfi/quizbase/quizbase.py +828 -0
- sfi/quizbase/quizbase_gui.py +987 -0
- sfi/regexvalidate/__init__.py +0 -0
- sfi/regexvalidate/regex_help.html +284 -0
- sfi/regexvalidate/regexvalidate.py +468 -0
- sfi/taskkill/taskkill.py +0 -2
- pysfi-0.1.10.dist-info/RECORD +0 -39
- sfi/embedinstall/embedinstall.py +0 -478
- sfi/projectparse/projectparse.py +0 -152
- {pysfi-0.1.10.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
- /sfi/{embedinstall → llmquantize}/__init__.py +0 -0
- /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_())
|
sfi/makepython/makepython.py
CHANGED
|
@@ -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)")
|