pysfi 0.1.7__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 (55) hide show
  1. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
  2. pysfi-0.1.11.dist-info/RECORD +60 -0
  3. pysfi-0.1.11.dist-info/entry_points.txt +28 -0
  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.py +407 -103
  11. sfi/docscan/docscan_gui.py +1282 -596
  12. sfi/docscan/lang/eng.py +152 -0
  13. sfi/docscan/lang/zhcn.py +170 -0
  14. sfi/filedate/filedate.py +185 -112
  15. sfi/gittool/__init__.py +2 -0
  16. sfi/gittool/gittool.py +401 -0
  17. sfi/llmclient/llmclient.py +592 -0
  18. sfi/llmquantize/llmquantize.py +480 -0
  19. sfi/llmserver/llmserver.py +335 -0
  20. sfi/makepython/makepython.py +31 -30
  21. sfi/pdfsplit/pdfsplit.py +173 -173
  22. sfi/pyarchive/pyarchive.py +418 -0
  23. sfi/pyembedinstall/pyembedinstall.py +629 -0
  24. sfi/pylibpack/__init__.py +0 -0
  25. sfi/pylibpack/pylibpack.py +1457 -0
  26. sfi/pylibpack/rules/numpy.json +22 -0
  27. sfi/pylibpack/rules/pymupdf.json +10 -0
  28. sfi/pylibpack/rules/pyqt5.json +19 -0
  29. sfi/pylibpack/rules/pyside2.json +23 -0
  30. sfi/pylibpack/rules/scipy.json +23 -0
  31. sfi/pylibpack/rules/shiboken2.json +24 -0
  32. sfi/pyloadergen/pyloadergen.py +512 -227
  33. sfi/pypack/__init__.py +0 -0
  34. sfi/pypack/pypack.py +1142 -0
  35. sfi/pyprojectparse/__init__.py +0 -0
  36. sfi/pyprojectparse/pyprojectparse.py +500 -0
  37. sfi/pysourcepack/pysourcepack.py +308 -0
  38. sfi/quizbase/__init__.py +0 -0
  39. sfi/quizbase/quizbase.py +828 -0
  40. sfi/quizbase/quizbase_gui.py +987 -0
  41. sfi/regexvalidate/__init__.py +0 -0
  42. sfi/regexvalidate/regex_help.html +284 -0
  43. sfi/regexvalidate/regexvalidate.py +468 -0
  44. sfi/taskkill/taskkill.py +0 -2
  45. sfi/workflowengine/__init__.py +0 -0
  46. sfi/workflowengine/workflowengine.py +444 -0
  47. pysfi-0.1.7.dist-info/RECORD +0 -31
  48. pysfi-0.1.7.dist-info/entry_points.txt +0 -15
  49. sfi/embedinstall/embedinstall.py +0 -418
  50. sfi/projectparse/projectparse.py +0 -152
  51. sfi/pypacker/fspacker.py +0 -91
  52. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
  53. /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
  54. /sfi/{projectparse → llmquantize}/__init__.py +0 -0
  55. /sfi/{pypacker → 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_())
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import json
5
4
  import logging
6
5
  import os
7
6
  import shutil
@@ -35,8 +34,12 @@ def parse_pyproject_toml(directory: Path) -> dict:
35
34
  logger.error(f"No pyproject.toml found in {directory}")
36
35
  return {}
37
36
 
38
- with project_toml.open("rb") as f:
39
- return tomllib.load(f)
37
+ try:
38
+ with project_toml.open("rb") as f:
39
+ return tomllib.load(f)
40
+ except Exception as e:
41
+ logger.error(f"Error parsing pyproject.toml: {e}")
42
+ return {}
40
43
 
41
44
 
42
45
  def _get_build_command_from_toml(directory: Path) -> str | None:
@@ -59,12 +62,11 @@ def _get_build_command_from_toml(directory: Path) -> str | None:
59
62
  logger.error(f"Unknown build-backend: {build_backend}")
60
63
  return None
61
64
 
62
- logger.error("No `build-system` or `build-backend` found in pyproject.toml: ")
63
- logger.error(json.dumps(project_data, indent=2, ensure_ascii=False, sort_keys=True))
65
+ logger.error("No `build-system` or `build-backend` found in pyproject.toml")
64
66
  return None
65
67
 
66
68
 
67
- def _get_build_command(directory: Path):
69
+ def _get_build_command(directory: Path) -> str | None:
68
70
  """Get build command from directory."""
69
71
  project_path = directory / "pyproject.toml"
70
72
  if project_path.is_file():
@@ -87,39 +89,38 @@ class Command:
87
89
  cmds: list[str] | Callable[..., Any] | None = None
88
90
 
89
91
 
90
- def _clean(root_dir: Path):
92
+ def _clean(root_dir: Path = cwd):
91
93
  _run_command(["rm", "-rf", "dist", "build", "*.egg-info"], root_dir)
92
94
 
93
95
 
94
- BUILD_COMMAND = _get_build_command(cwd) or ""
95
- _COMMANDS = [
96
- Command(name="build", alias="b", cmds=[BUILD_COMMAND, "build"]),
97
- Command(name="bumpversion", alias="bump", cmds=["bumpversion", "patch"]),
98
- Command(name="clean", alias="c", cmds=_clean),
99
- Command(name="publish", alias="p"), # No preset commands
100
- Command(name="test", alias="t", cmds=lambda: os.system("pytest")),
101
- Command(name="test-benchmark", alias="tb", cmds=lambda: os.system("pytest -m benchmark")),
102
- Command(name="test-coverage", alias="tc", cmds=lambda: os.system("pytest --cov=sfi")),
103
- Command(name="token", alias="tk", cmds=lambda: _set_token(BUILD_COMMAND)),
104
- ]
105
- _COMMAND_DICT = {command.name: command for command in _COMMANDS}
106
- _COMMAND_DICT.update({command.alias: command for command in _COMMANDS})
107
- _CHOICES = [command.alias for command in _COMMANDS]
108
- _CHOICES.extend([command.name for command in _COMMANDS])
109
-
110
-
111
- def main():
96
+ def main() -> None:
97
+ # Get build command
98
+ build_command = _get_build_command(cwd) or ""
99
+ commands = [
100
+ Command(name="build", alias="b", cmds=[build_command, "build"]),
101
+ Command(name="bumpversion", alias="bump", cmds=["bumpversion", "patch", "--tag"]),
102
+ Command(name="clean", alias="c", cmds=_clean),
103
+ Command(name="publish", alias="p"), # No preset commands
104
+ Command(name="test", alias="t", cmds=lambda: os.system("pytest")),
105
+ Command(name="test-benchmark", alias="tb", cmds=lambda: os.system("pytest -m benchmark")),
106
+ Command(name="test-coverage", alias="tc", cmds=lambda: os.system("pytest --cov=sfi")),
107
+ Command(name="token", alias="tk", cmds=lambda: _set_token(build_command)),
108
+ ]
109
+ command_dict = {command.name: command for command in commands}
110
+ command_dict.update({command.alias: command for command in commands})
111
+ choices = [command.alias for command in commands]
112
+ choices.extend([command.name for command in commands])
113
+
114
+ # Parse args
112
115
  parser = argparse.ArgumentParser(description="Make Python")
113
- parser.add_argument("command", type=str, choices=_CHOICES, help=f"Command to run, options: {_CHOICES}")
114
116
  parser.add_argument("--debug", "-d", action="store_true", help="Enable debug mode")
115
-
117
+ parser.add_argument("command", type=str, choices=choices, help=f"Command to run, options: {choices}")
116
118
  args = parser.parse_args()
117
119
  if args.debug:
118
120
  logger.setLevel(logging.DEBUG)
119
121
 
120
- build_command = _get_build_command(cwd) or ""
121
- logger.info(f"Using build command: {build_command}")
122
- command = _COMMAND_DICT.get(args.command)
122
+ logger.debug(f"Using build command: {build_command}")
123
+ command = command_dict.get(args.command)
123
124
  if command:
124
125
  if callable(command.cmds):
125
126
  command.cmds()