pysfi 0.1.12__py3-none-any.whl → 0.1.13__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.13.dist-info}/METADATA +1 -1
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/RECORD +35 -27
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/entry_points.txt +2 -0
- sfi/__init__.py +5 -3
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +5 -3
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cli.py +12 -2
- sfi/condasetup/__init__.py +1 -0
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +1 -1
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan_gui.py +150 -46
- sfi/img2pdf/__init__.py +0 -0
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +39 -11
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +507 -124
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +571 -465
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +494 -965
- sfi/pyprojectparse/pyprojectparse.py +328 -28
- 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
- sfi/workflowengine/workflowengine.py +225 -122
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/WHEEL +0 -0
|
@@ -9,7 +9,7 @@ import shutil
|
|
|
9
9
|
import time
|
|
10
10
|
import zipfile
|
|
11
11
|
from contextlib import suppress
|
|
12
|
-
from dataclasses import dataclass
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
13
|
from functools import cached_property
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Final
|
|
@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
|
|
25
25
|
|
|
26
26
|
# Default cache directory
|
|
27
27
|
_DEFAULT_CACHE_DIR = Path.home() / ".pysfi" / ".cache" / "embed-python"
|
|
28
|
-
|
|
28
|
+
_DEFAULT_PYTHON_VER = "3.8.10"
|
|
29
29
|
|
|
30
30
|
# Architecture mapping
|
|
31
31
|
ARCH_DICT: Final = {
|
|
@@ -44,31 +44,34 @@ USER_AGENT: Final = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.3
|
|
|
44
44
|
class EmbedDownloader:
|
|
45
45
|
"""Class for downloading Python embeddable package."""
|
|
46
46
|
|
|
47
|
+
parent: EmbedInstaller
|
|
47
48
|
version: str
|
|
48
|
-
arch: str
|
|
49
|
-
cache_dir: Path
|
|
50
|
-
offline: bool
|
|
51
|
-
skip_speed_test: bool
|
|
52
|
-
timeout: int = 5
|
|
53
49
|
|
|
54
50
|
@cached_property
|
|
55
51
|
def cache_file(self) -> Path:
|
|
56
52
|
"""Get the cache file path for the embeddable package."""
|
|
57
|
-
return
|
|
53
|
+
return (
|
|
54
|
+
self.parent.cache_dir
|
|
55
|
+
/ f"python-{self.version}-embed-{self.parent.arch}.zip"
|
|
56
|
+
)
|
|
58
57
|
|
|
59
58
|
@cached_property
|
|
60
59
|
def download_urls(self) -> list[str]:
|
|
61
60
|
"""Get the list of URLs to try for download."""
|
|
62
|
-
if self.skip_speed_test:
|
|
63
|
-
url = self.get_official_url_template(self.arch).format(
|
|
61
|
+
if self.parent.skip_speed_test:
|
|
62
|
+
url = self.get_official_url_template(self.parent.arch).format(
|
|
63
|
+
version=self.latest_path_version
|
|
64
|
+
)
|
|
64
65
|
logger.info(f"Skipping speed test, using official URL: {url}")
|
|
65
66
|
return [url]
|
|
66
|
-
return self.find_available_urls(
|
|
67
|
+
return self.find_available_urls(
|
|
68
|
+
self.latest_path_version, self.parent.arch, self.parent.timeout
|
|
69
|
+
)
|
|
67
70
|
|
|
68
71
|
def download(self) -> bool:
|
|
69
72
|
"""Download the Python embeddable package to the cache file."""
|
|
70
73
|
return self.has_cached_file or self.download_python_package(
|
|
71
|
-
self.cache_file, self.download_urls, self.offline
|
|
74
|
+
self.cache_file, self.download_urls, self.parent.offline
|
|
72
75
|
)
|
|
73
76
|
|
|
74
77
|
@cached_property
|
|
@@ -91,9 +94,11 @@ class EmbedDownloader:
|
|
|
91
94
|
return is_valid
|
|
92
95
|
|
|
93
96
|
@cached_property
|
|
94
|
-
def
|
|
97
|
+
def latest_path_version(self) -> str | None:
|
|
95
98
|
"""Get the latest version of Python available for download."""
|
|
96
|
-
version = self.get_latest_patch_version(
|
|
99
|
+
version = self.get_latest_patch_version(
|
|
100
|
+
self.version, self.parent.arch, self.parent.cache_dir
|
|
101
|
+
)
|
|
97
102
|
return version if "." in version else None
|
|
98
103
|
|
|
99
104
|
def download_with_progress(self, url: str, dest_path: Path) -> None:
|
|
@@ -102,7 +107,7 @@ class EmbedDownloader:
|
|
|
102
107
|
|
|
103
108
|
req = Request(url, headers={"User-Agent": USER_AGENT})
|
|
104
109
|
try:
|
|
105
|
-
with urlopen(req, timeout=self.timeout) as response:
|
|
110
|
+
with urlopen(req, timeout=self.parent.timeout) as response:
|
|
106
111
|
total_size = int(response.headers.get("content-length", 0))
|
|
107
112
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
108
113
|
|
|
@@ -204,12 +209,12 @@ class EmbedDownloader:
|
|
|
204
209
|
|
|
205
210
|
def get_official_url_template(self, arch: str | None = None) -> str:
|
|
206
211
|
"""Get official URL template based on architecture."""
|
|
207
|
-
arch = arch or self.arch
|
|
212
|
+
arch = arch or self.parent.arch
|
|
208
213
|
return f"https://www.python.org/ftp/python/{{version}}/python-{{version}}-embed-{arch}.zip"
|
|
209
214
|
|
|
210
215
|
def get_mirror_url_templates(self, arch: str | None = None) -> list[str]:
|
|
211
216
|
"""Get mirror URL templates based on architecture."""
|
|
212
|
-
arch = arch or self.arch
|
|
217
|
+
arch = arch or self.parent.arch
|
|
213
218
|
return [
|
|
214
219
|
f"https://mirrors.huaweicloud.com/python/{{version}}/python-{{version}}-embed-{arch}.zip",
|
|
215
220
|
f"https://mirrors.aliyun.com/python-release/{{version}}/python-{{version}}-embed-{arch}.zip",
|
|
@@ -221,7 +226,7 @@ class EmbedDownloader:
|
|
|
221
226
|
self, url: str, timeout: int | None = None, use_get: bool = False
|
|
222
227
|
) -> float:
|
|
223
228
|
"""Test URL speed by making a HEAD or GET request."""
|
|
224
|
-
timeout = timeout or self.timeout
|
|
229
|
+
timeout = timeout or self.parent.timeout
|
|
225
230
|
start_time = time.time()
|
|
226
231
|
|
|
227
232
|
# First attempt HEAD request
|
|
@@ -268,7 +273,7 @@ class EmbedDownloader:
|
|
|
268
273
|
) -> list[str]:
|
|
269
274
|
"""Get list of URLs to test for speed."""
|
|
270
275
|
version = version or self.version
|
|
271
|
-
arch = arch or self.arch
|
|
276
|
+
arch = arch or self.parent.arch
|
|
272
277
|
return [
|
|
273
278
|
template.format(version=version)
|
|
274
279
|
for template in [
|
|
@@ -285,8 +290,8 @@ class EmbedDownloader:
|
|
|
285
290
|
) -> list[str]:
|
|
286
291
|
"""Find all available URLs for downloading Python embeddable package, sorted by speed."""
|
|
287
292
|
version = version or self.version
|
|
288
|
-
arch = arch or self.arch
|
|
289
|
-
timeout = timeout or self.timeout
|
|
293
|
+
arch = arch or self.parent.arch
|
|
294
|
+
timeout = timeout or self.parent.timeout
|
|
290
295
|
official_url = self.get_official_url_template(arch).format(version=version)
|
|
291
296
|
|
|
292
297
|
logger.debug("Testing mirror speeds...")
|
|
@@ -434,37 +439,24 @@ class EmbedDownloader:
|
|
|
434
439
|
return self.resolve_patch_version(major_minor, arch, cache_dir, timeout)
|
|
435
440
|
|
|
436
441
|
|
|
437
|
-
@dataclass(frozen=
|
|
442
|
+
@dataclass(frozen=False)
|
|
438
443
|
class EmbedInstaller:
|
|
439
444
|
"""Class for installing Python embeddable package."""
|
|
440
445
|
|
|
441
446
|
root_dir: Path
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
skip_speed_test: bool
|
|
446
|
-
keep_cache: bool
|
|
447
|
+
cache_dir: Path | None = field(default_factory=lambda: _DEFAULT_CACHE_DIR)
|
|
448
|
+
offline: bool = False
|
|
449
|
+
skip_speed_test: bool = False
|
|
447
450
|
timeout: int = 5
|
|
448
451
|
|
|
449
452
|
def __post_init__(self):
|
|
450
|
-
|
|
453
|
+
if not self.cache_dir:
|
|
454
|
+
object.__setattr__(self, "cache_dir", _DEFAULT_CACHE_DIR)
|
|
451
455
|
|
|
452
456
|
@cached_property
|
|
453
457
|
def solution(self) -> Solution:
|
|
454
458
|
"""Get the solution from the target directory."""
|
|
455
|
-
return Solution.from_directory(self.root_dir)
|
|
456
|
-
|
|
457
|
-
@cached_property
|
|
458
|
-
def downloader(self) -> EmbedDownloader:
|
|
459
|
-
"""Get the embed downloader for the given version and architecture."""
|
|
460
|
-
return EmbedDownloader(
|
|
461
|
-
version=self.version,
|
|
462
|
-
arch=self.arch,
|
|
463
|
-
cache_dir=self.cache_dir,
|
|
464
|
-
offline=self.offline,
|
|
465
|
-
skip_speed_test=self.skip_speed_test,
|
|
466
|
-
timeout=self.timeout,
|
|
467
|
-
)
|
|
459
|
+
return Solution.from_directory(self.root_dir, update=True)
|
|
468
460
|
|
|
469
461
|
@cached_property
|
|
470
462
|
def arch(self) -> str:
|
|
@@ -484,15 +476,10 @@ class EmbedInstaller:
|
|
|
484
476
|
"""Get the path to python.exe in the target directory."""
|
|
485
477
|
return self.runtime_dir / "python.exe"
|
|
486
478
|
|
|
487
|
-
|
|
488
|
-
def cache_file(self) -> Path:
|
|
489
|
-
"""Get the path to the cache file."""
|
|
490
|
-
return self.downloader.cache_file
|
|
491
|
-
|
|
492
|
-
def extract_package(self) -> bool:
|
|
479
|
+
def extract_package(self, downloader: EmbedDownloader) -> bool:
|
|
493
480
|
"""Extract package to target directory."""
|
|
494
|
-
if not zipfile.is_zipfile(
|
|
495
|
-
logger.error(f"Invalid zip file: {
|
|
481
|
+
if not zipfile.is_zipfile(downloader.cache_file):
|
|
482
|
+
logger.error(f"Invalid zip file: {downloader.cache_file}")
|
|
496
483
|
return False
|
|
497
484
|
|
|
498
485
|
try:
|
|
@@ -500,50 +487,71 @@ class EmbedInstaller:
|
|
|
500
487
|
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
501
488
|
|
|
502
489
|
# First get file count for progress
|
|
503
|
-
with zipfile.ZipFile(
|
|
490
|
+
with zipfile.ZipFile(downloader.cache_file, "r") as zip_ref:
|
|
504
491
|
file_count = len(zip_ref.namelist())
|
|
505
492
|
|
|
506
493
|
# Extract the archive
|
|
507
|
-
shutil.unpack_archive(
|
|
494
|
+
shutil.unpack_archive(
|
|
495
|
+
str(downloader.cache_file), str(self.runtime_dir), "zip"
|
|
496
|
+
)
|
|
508
497
|
logger.info(f"Extracted {file_count} files")
|
|
509
498
|
return True
|
|
510
499
|
except (zipfile.BadZipFile, OSError) as e:
|
|
511
500
|
logger.error(f"Failed to extract: {e}")
|
|
512
501
|
return False
|
|
513
502
|
|
|
514
|
-
def
|
|
515
|
-
"""Clean up cache file if not keeping it."""
|
|
516
|
-
if not self.keep_cache and not self.offline:
|
|
517
|
-
try:
|
|
518
|
-
if self.cache_file.exists():
|
|
519
|
-
logger.debug(f"Cleaning up cache: {self.cache_file}")
|
|
520
|
-
self.cache_file.unlink()
|
|
521
|
-
except OSError as e:
|
|
522
|
-
logger.warning(f"Failed to clean up cache file: {e}")
|
|
523
|
-
|
|
524
|
-
def install_package(self) -> bool:
|
|
503
|
+
def run(self) -> bool:
|
|
525
504
|
"""Install Python embeddable package to target directory using embedded methods."""
|
|
526
505
|
# Download the package (latest_version is resolved automatically during download)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
506
|
+
results = []
|
|
507
|
+
total_processed = 0
|
|
508
|
+
extracted = False # Track if we've already extracted for any project
|
|
509
|
+
|
|
510
|
+
for project in self.solution.projects.values():
|
|
511
|
+
if not project.min_python_version:
|
|
512
|
+
logger.warning(
|
|
513
|
+
f"Project {project.name} does not have a minimum Python version"
|
|
514
|
+
)
|
|
515
|
+
continue
|
|
516
|
+
|
|
517
|
+
logger.info(f"Processing project: `{project.name}`")
|
|
518
|
+
total_processed += 1
|
|
519
|
+
downloader = EmbedDownloader(
|
|
520
|
+
parent=self,
|
|
521
|
+
version=project.min_python_version or _DEFAULT_PYTHON_VER,
|
|
522
|
+
)
|
|
530
523
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
524
|
+
if not downloader.download():
|
|
525
|
+
logger.error("Failed to download Python embeddable package")
|
|
526
|
+
continue
|
|
527
|
+
|
|
528
|
+
# Extract the package to runtime directory only once
|
|
529
|
+
# All projects share the same embed Python runtime
|
|
530
|
+
if not extracted:
|
|
531
|
+
if self.python_exe_path.exists():
|
|
532
|
+
logger.info(
|
|
533
|
+
f"Python runtime already exists at {self.runtime_dir}, skipping extraction"
|
|
534
|
+
)
|
|
535
|
+
extracted = True
|
|
536
|
+
elif self.extract_package(downloader):
|
|
537
|
+
extracted = True
|
|
538
|
+
else:
|
|
539
|
+
logger.error("Failed to extract Python embeddable package")
|
|
540
|
+
continue
|
|
535
541
|
|
|
536
|
-
|
|
537
|
-
|
|
542
|
+
# Verify installation by checking for python.exe
|
|
543
|
+
results.append(self.python_exe_path.exists())
|
|
538
544
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
logger.info(f"Installation completed, python.exe: {self.python_exe_path}")
|
|
543
|
-
else:
|
|
544
|
-
logger.error("Installation failed, python.exe not found")
|
|
545
|
+
if total_processed == 0:
|
|
546
|
+
logger.error("No projects with minimum Python version found to process")
|
|
547
|
+
return False
|
|
545
548
|
|
|
546
|
-
|
|
549
|
+
if all(results) and len(results) > 0:
|
|
550
|
+
logger.info("Python embeddable package installed successfully")
|
|
551
|
+
return True
|
|
552
|
+
else:
|
|
553
|
+
logger.error("Failed to install Python embeddable package")
|
|
554
|
+
return False
|
|
547
555
|
|
|
548
556
|
|
|
549
557
|
def parse_args() -> argparse.Namespace:
|
|
@@ -570,7 +578,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
570
578
|
"--version",
|
|
571
579
|
"-v",
|
|
572
580
|
type=str,
|
|
573
|
-
default=
|
|
581
|
+
default=_DEFAULT_PYTHON_VER,
|
|
574
582
|
help="Python version to install (default: 3.8.10)",
|
|
575
583
|
)
|
|
576
584
|
parser.add_argument(
|
|
@@ -580,13 +588,6 @@ def parse_args() -> argparse.Namespace:
|
|
|
580
588
|
default=str(_DEFAULT_CACHE_DIR),
|
|
581
589
|
help="Cache directory for downloaded files",
|
|
582
590
|
)
|
|
583
|
-
parser.add_argument(
|
|
584
|
-
"--keep-cache",
|
|
585
|
-
"-k",
|
|
586
|
-
action="store_true",
|
|
587
|
-
default=True,
|
|
588
|
-
help="Keep downloaded cache files",
|
|
589
|
-
)
|
|
590
591
|
parser.add_argument(
|
|
591
592
|
"--skip-speed-test",
|
|
592
593
|
"-s",
|
|
@@ -617,13 +618,11 @@ def main() -> None:
|
|
|
617
618
|
|
|
618
619
|
if not EmbedInstaller(
|
|
619
620
|
root_dir=Path(args.directory),
|
|
620
|
-
version=args.version,
|
|
621
621
|
cache_dir=cache_dir,
|
|
622
622
|
offline=args.offline,
|
|
623
623
|
skip_speed_test=args.skip_speed_test,
|
|
624
|
-
keep_cache=args.keep_cache,
|
|
625
624
|
timeout=args.timeout,
|
|
626
|
-
).
|
|
625
|
+
).run():
|
|
627
626
|
exit(1)
|
|
628
627
|
|
|
629
628
|
logger.info(f"Installation completed in {time.perf_counter() - t0:.4f} seconds")
|