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.
- {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/METADATA +1 -1
- pysfi-0.1.14.dist-info/RECORD +68 -0
- {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/entry_points.txt +3 -0
- sfi/__init__.py +19 -2
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +3 -1
- sfi/bumpversion/bumpversion.py +64 -15
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cleanbuild/cleanbuild.py +5 -1
- sfi/cli.py +25 -4
- sfi/condasetup/__init__.py +1 -0
- sfi/condasetup/condasetup.py +91 -76
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +3 -2
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan.py +78 -23
- sfi/docscan/docscan_gui.py +152 -48
- sfi/filedate/filedate.py +12 -5
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +76 -37
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +1145 -201
- sfi/pdfsplit/pdfsplit.py +45 -12
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +561 -463
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +510 -959
- sfi/pyprojectparse/pyprojectparse.py +337 -40
- sfi/pysourcepack/__init__.py +1 -0
- sfi/pysourcepack/pysourcepack.py +210 -131
- sfi/quizbase/quizbase_gui.py +2 -2
- sfi/taskkill/taskkill.py +168 -59
- sfi/which/which.py +11 -3
- pysfi-0.1.12.dist-info/RECORD +0 -62
- sfi/workflowengine/workflowengine.py +0 -444
- {pysfi-0.1.12.dist-info → pysfi-0.1.14.dist-info}/WHEEL +0 -0
- /sfi/{workflowengine → img2pdf}/__init__.py +0 -0
sfi/pysourcepack/pysourcepack.py
CHANGED
|
@@ -5,19 +5,23 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import logging
|
|
7
7
|
import shutil
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from functools import cached_property
|
|
8
11
|
from pathlib import Path
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
from sfi.pyprojectparse.pyprojectparse import Project, Solution
|
|
9
15
|
|
|
10
16
|
__version__ = "0.1.0"
|
|
11
|
-
__all__ = [
|
|
12
|
-
"pack_project",
|
|
13
|
-
]
|
|
17
|
+
__all__ = ["PySourcePacker"]
|
|
14
18
|
|
|
15
19
|
cwd = Path.cwd()
|
|
16
20
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
17
21
|
logger = logging.getLogger(__name__)
|
|
18
22
|
|
|
19
23
|
# Default directories and files to exclude during packaging
|
|
20
|
-
DEFAULT_EXCLUDE = {
|
|
24
|
+
DEFAULT_EXCLUDE: Final = {
|
|
21
25
|
"__pycache__",
|
|
22
26
|
"*.pyc",
|
|
23
27
|
"*.pyo",
|
|
@@ -35,7 +39,7 @@ DEFAULT_EXCLUDE = {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
# Default files to include in packaging
|
|
38
|
-
DEFAULT_INCLUDE = {
|
|
42
|
+
DEFAULT_INCLUDE: Final = {
|
|
39
43
|
"*.py",
|
|
40
44
|
"README.md",
|
|
41
45
|
"LICENSE",
|
|
@@ -108,98 +112,205 @@ def should_include(
|
|
|
108
112
|
return False
|
|
109
113
|
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
@dataclass
|
|
116
|
+
class ProjectPacker:
|
|
117
|
+
"""Helper class to pack individual projects."""
|
|
118
|
+
|
|
119
|
+
parent: PySourcePacker
|
|
120
|
+
project: Project
|
|
121
|
+
project_name: str
|
|
122
|
+
include_patterns: set[str]
|
|
123
|
+
exclude_patterns: set[str]
|
|
124
|
+
|
|
125
|
+
@cached_property
|
|
126
|
+
def is_single_project(self) -> bool:
|
|
127
|
+
"""Check if the project is the only one in the solution."""
|
|
128
|
+
return len(self.parent.solution.projects) == 1
|
|
129
|
+
|
|
130
|
+
@cached_property
|
|
131
|
+
def project_path(self) -> Path:
|
|
132
|
+
"""Get project directory path."""
|
|
133
|
+
return (
|
|
134
|
+
self.parent.root_dir
|
|
135
|
+
if self.is_single_project
|
|
136
|
+
else self.parent.root_dir / self.project_name
|
|
137
|
+
)
|
|
115
138
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
@cached_property
|
|
140
|
+
def output_dir(self) -> Path:
|
|
141
|
+
"""Get output directory path."""
|
|
142
|
+
return self.parent.root_dir / "dist" / "src" / self.project_name
|
|
119
143
|
|
|
120
|
-
|
|
144
|
+
def validate_project_path(self) -> bool:
|
|
145
|
+
"""Validate project path exists and is a directory."""
|
|
146
|
+
if not self.project_path.exists():
|
|
147
|
+
logger.error(f"Project directory {self.project_path} does not exist")
|
|
148
|
+
return False
|
|
121
149
|
|
|
150
|
+
if not self.project_path.is_dir():
|
|
151
|
+
logger.error(f"{self.project_path} is not a directory")
|
|
152
|
+
return False
|
|
122
153
|
|
|
123
|
-
|
|
124
|
-
base_dir: Path,
|
|
125
|
-
projects: dict,
|
|
126
|
-
project_name: str,
|
|
127
|
-
include_patterns: set[str] | None = None,
|
|
128
|
-
exclude_patterns: set[str] | None = None,
|
|
129
|
-
) -> bool:
|
|
130
|
-
"""Pack project source code and resources to dist/src directory.
|
|
154
|
+
return True
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
project_name: Name of the project to pack
|
|
135
|
-
include_patterns: File patterns to include (default: DEFAULT_INCLUDE)
|
|
136
|
-
exclude_patterns: File patterns to exclude (default: DEFAULT_EXCLUDE)
|
|
156
|
+
def pack(self) -> bool:
|
|
157
|
+
"""Pack project source code and resources to dist/src directory.
|
|
137
158
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
Returns:
|
|
160
|
+
True if packing succeeded, False otherwise
|
|
161
|
+
"""
|
|
162
|
+
logger.debug(f"Start packing project: {self.project_name}")
|
|
142
163
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
if not self.project_name:
|
|
165
|
+
logger.error("Project name cannot be empty")
|
|
166
|
+
return False
|
|
146
167
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
logger.debug(
|
|
169
|
+
f"Project path: {self.project_path}, project_name: {self.project_name}"
|
|
170
|
+
)
|
|
171
|
+
if not self.validate_project_path():
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
# Copy files
|
|
177
|
+
copied_files = 0
|
|
178
|
+
copied_dirs = 0
|
|
179
|
+
|
|
180
|
+
logger.info(f"Packing project '{self.project_name}' to {self.output_dir}")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
for item in self.project_path.iterdir():
|
|
184
|
+
if item.is_file():
|
|
185
|
+
# For files, check if should be included
|
|
186
|
+
if should_include(
|
|
187
|
+
item, self.include_patterns, self.exclude_patterns
|
|
188
|
+
):
|
|
189
|
+
dest_file = self.output_dir / item.name
|
|
190
|
+
shutil.copy2(item, dest_file)
|
|
191
|
+
logger.debug(f"Copied file: {item.name}")
|
|
192
|
+
copied_files += 1
|
|
193
|
+
elif item.is_dir() and not should_exclude(item, self.exclude_patterns):
|
|
194
|
+
# Copy directory recursively with exclude patterns
|
|
195
|
+
dest_dir = self.output_dir / item.name
|
|
196
|
+
if dest_dir.exists():
|
|
197
|
+
shutil.rmtree(dest_dir)
|
|
198
|
+
shutil.copytree(
|
|
199
|
+
item, dest_dir, ignore=shutil.ignore_patterns(*DEFAULT_EXCLUDE)
|
|
200
|
+
)
|
|
201
|
+
logger.debug(f"Copied directory: {item.name}")
|
|
202
|
+
copied_dirs += 1
|
|
203
|
+
|
|
204
|
+
logger.info(
|
|
205
|
+
f"Successfully packed {self.project_name}: {copied_files} files, {copied_dirs} directories"
|
|
206
|
+
)
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error packing project {self.project_name}: {e}")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class PySourcePacker:
|
|
216
|
+
"""Main class for packing Python source code."""
|
|
217
|
+
|
|
218
|
+
root_dir: Path
|
|
219
|
+
include_patterns: set[str] = field(default_factory=lambda: DEFAULT_INCLUDE)
|
|
220
|
+
exclude_patterns: set[str] = field(default_factory=lambda: DEFAULT_EXCLUDE)
|
|
221
|
+
|
|
222
|
+
@cached_property
|
|
223
|
+
def solution(self) -> Solution:
|
|
224
|
+
"""Get the solution from the target directory."""
|
|
225
|
+
return Solution.from_directory(self.root_dir)
|
|
226
|
+
|
|
227
|
+
@cached_property
|
|
228
|
+
def projects(self) -> dict[str, Project]:
|
|
229
|
+
"""Get the projects from the solution."""
|
|
230
|
+
return self.solution.projects
|
|
231
|
+
|
|
232
|
+
def pack_project(self, project_name: str) -> bool:
|
|
233
|
+
"""Pack a single project.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
project_name: Name of the project to pack
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if packing succeeded, False otherwise
|
|
240
|
+
"""
|
|
241
|
+
projects = self.solution.projects
|
|
242
|
+
|
|
243
|
+
if project_name not in projects:
|
|
244
|
+
logger.error(f"Project '{project_name}' not found in projects.json")
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
project = projects[project_name]
|
|
248
|
+
packer = ProjectPacker(
|
|
249
|
+
parent=self,
|
|
250
|
+
project=project,
|
|
251
|
+
project_name=project_name,
|
|
252
|
+
include_patterns=self.include_patterns,
|
|
253
|
+
exclude_patterns=self.exclude_patterns,
|
|
254
|
+
)
|
|
150
255
|
|
|
151
|
-
|
|
152
|
-
output_dir = base_dir / "dist" / "src" / project_name
|
|
256
|
+
return packer.pack()
|
|
153
257
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
258
|
+
def run(self, project_name: str | None = None) -> None:
|
|
259
|
+
"""Pack specified project or all projects concurrently.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
project_name: Name of the project to pack, or None to pack all projects
|
|
263
|
+
"""
|
|
157
264
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
265
|
+
if not self.projects:
|
|
266
|
+
logger.error("No projects found in projects.json")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Pack specific project
|
|
270
|
+
if project_name:
|
|
271
|
+
if self.pack_project(project_name):
|
|
272
|
+
logger.info(f"Packed project '{project_name}' successfully")
|
|
273
|
+
else:
|
|
274
|
+
logger.error(f"Failed to pack project '{project_name}'")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
# Pack all projects
|
|
278
|
+
logger.info(f"Start packing, projects: {self.projects}")
|
|
279
|
+
|
|
280
|
+
if len(self.projects) == 1:
|
|
281
|
+
# Single project: process directly
|
|
282
|
+
name = next(iter(self.projects))
|
|
283
|
+
success_count = 1 if self.pack_project(name) else 0
|
|
284
|
+
else:
|
|
285
|
+
# Multiple projects: process concurrently
|
|
286
|
+
logger.info(f"Packing {len(self.projects)} projects concurrently...")
|
|
287
|
+
success_count = 0
|
|
288
|
+
|
|
289
|
+
with ThreadPoolExecutor(max_workers=None) as executor:
|
|
290
|
+
future_to_name = {
|
|
291
|
+
executor.submit(self.pack_project, name): name
|
|
292
|
+
for name in self.projects
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for future in as_completed(future_to_name):
|
|
296
|
+
name = future_to_name[future]
|
|
297
|
+
try:
|
|
298
|
+
if future.result():
|
|
299
|
+
success_count += 1
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error(f"Project {name} packing failed: {e}")
|
|
191
302
|
|
|
192
303
|
logger.info(
|
|
193
|
-
f"
|
|
304
|
+
f"Packed {success_count}/{len(self.projects)} projects successfully"
|
|
194
305
|
)
|
|
195
|
-
return True
|
|
196
306
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
307
|
+
def list_projects(self) -> None:
|
|
308
|
+
"""List all available projects."""
|
|
309
|
+
for name, project in self.projects.items():
|
|
310
|
+
logger.info(f"{name}: {project}")
|
|
200
311
|
|
|
201
312
|
|
|
202
|
-
def
|
|
313
|
+
def parse_args() -> argparse.Namespace:
|
|
203
314
|
"""Create and return an argument parser for the PySourcePack tool."""
|
|
204
315
|
parser = argparse.ArgumentParser(
|
|
205
316
|
description="PySourcePack - Source code packaging tool",
|
|
@@ -241,68 +352,36 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
241
352
|
action="store_true",
|
|
242
353
|
help="Enable debug mode",
|
|
243
354
|
)
|
|
244
|
-
return parser
|
|
355
|
+
return parser.parse_args()
|
|
245
356
|
|
|
246
357
|
|
|
247
358
|
def main() -> None:
|
|
248
|
-
|
|
359
|
+
"""Main entry point for pysourcepack."""
|
|
360
|
+
args = parse_args()
|
|
249
361
|
|
|
250
|
-
|
|
251
|
-
|
|
362
|
+
root_dir = Path(args.directory)
|
|
363
|
+
if not root_dir.exists():
|
|
364
|
+
logger.error(f"Root directory {root_dir} does not exist")
|
|
365
|
+
return
|
|
252
366
|
|
|
253
367
|
if args.debug:
|
|
254
368
|
logger.setLevel(logging.DEBUG)
|
|
255
369
|
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
projects = Solution.from_directory(base_dir, recursive=True, update=True).projects
|
|
260
|
-
|
|
261
|
-
if not project_json.exists():
|
|
262
|
-
logger.error("Failed to create projects.json")
|
|
263
|
-
return
|
|
370
|
+
# Create packer instance
|
|
371
|
+
include_patterns = set(args.include) if args.include else DEFAULT_INCLUDE
|
|
372
|
+
exclude_patterns = set(args.exclude) if args.exclude else DEFAULT_EXCLUDE
|
|
264
373
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
374
|
+
packer = PySourcePacker(
|
|
375
|
+
root_dir=root_dir,
|
|
376
|
+
include_patterns=include_patterns,
|
|
377
|
+
exclude_patterns=exclude_patterns,
|
|
378
|
+
)
|
|
268
379
|
|
|
269
380
|
if args.list:
|
|
270
|
-
|
|
271
|
-
for info in projects.values():
|
|
272
|
-
logger.info(info)
|
|
273
|
-
return
|
|
381
|
+
packer.list_projects()
|
|
274
382
|
|
|
275
|
-
|
|
276
|
-
include_patterns = set(args.include) if args.include else None
|
|
277
|
-
exclude_patterns = set(args.exclude) if args.exclude else None
|
|
383
|
+
packer.run(project_name=args.project)
|
|
278
384
|
|
|
279
|
-
logger.debug(
|
|
280
|
-
f"Start packing, config: base_dir={base_dir}, project_name={args.project}, include_patterns={include_patterns}, exclude_patterns={exclude_patterns}"
|
|
281
|
-
)
|
|
282
|
-
# Pack specified project or all projects
|
|
283
|
-
if args.project:
|
|
284
|
-
if pack_project(
|
|
285
|
-
base_dir=base_dir,
|
|
286
|
-
projects=projects,
|
|
287
|
-
project_name=args.project,
|
|
288
|
-
include_patterns=include_patterns,
|
|
289
|
-
exclude_patterns=exclude_patterns,
|
|
290
|
-
):
|
|
291
|
-
logger.info(f"Packed project '{args.project}' successfully")
|
|
292
|
-
else:
|
|
293
|
-
logger.error(f"Failed to pack project '{args.project}'")
|
|
294
|
-
return
|
|
295
385
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
success_count = 0
|
|
299
|
-
for project_name in projects:
|
|
300
|
-
if pack_project(
|
|
301
|
-
base_dir=base_dir,
|
|
302
|
-
projects=projects,
|
|
303
|
-
project_name=project_name,
|
|
304
|
-
include_patterns=include_patterns,
|
|
305
|
-
exclude_patterns=exclude_patterns,
|
|
306
|
-
):
|
|
307
|
-
success_count += 1
|
|
308
|
-
logger.info(f"Packed {success_count}/{len(projects)} projects successfully")
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
main()
|
sfi/quizbase/quizbase_gui.py
CHANGED
|
@@ -85,7 +85,7 @@ plugin_path = str(qt_dir / "plugins" / "platforms")
|
|
|
85
85
|
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
_WRONG_ANSWERS_FILE = Path.home() / ".
|
|
88
|
+
_WRONG_ANSWERS_FILE = Path.home() / ".pysfi" / "wrong_answers.json"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
@dataclass
|
|
@@ -119,7 +119,7 @@ class ConfigManager:
|
|
|
119
119
|
def __init__(self, config_file: Path | None = None):
|
|
120
120
|
"""Initialize configuration manager."""
|
|
121
121
|
if config_file is None:
|
|
122
|
-
config_dir = Path.home() / ".
|
|
122
|
+
config_dir = Path.home() / ".pysfi"
|
|
123
123
|
config_dir.mkdir(exist_ok=True)
|
|
124
124
|
config_file = config_dir / "quizbase_gui.json"
|
|
125
125
|
self.config_file = config_file
|