Compare commits

..

13 Commits
v0.2.3 ... main

21 changed files with 169 additions and 31 deletions

View File

@ -1,6 +1,6 @@
[project] [project]
name = "pycage" name = "pycage"
version = "0.2.3" version = "0.2.8"
description = "tool to handle standalone Python installations from the CLI" description = "tool to handle standalone Python installations from the CLI"
authors = [ authors = [
{name = "Florian Förster", email = "f.foerster@d-opt.com"}, {name = "Florian Förster", email = "f.foerster@d-opt.com"},
@ -78,7 +78,7 @@ directory = "reports/coverage"
[tool.bumpversion] [tool.bumpversion]
current_version = "0.2.3" current_version = "0.2.8"
parse = """(?x) parse = """(?x)
(?P<major>0|[1-9]\\d*)\\. (?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\. (?P<minor>0|[1-9]\\d*)\\.

View File

@ -1 +1 @@
pdm build -d build/ pdm build -d build/ --no-sdist

View File

@ -1,8 +1,6 @@
from pathlib import Path
import click import click
from pycage.helpers import get_interpreter, print_error from pycage.helpers import delete_files, print_error
@click.group(help="commands to remove specified files") @click.group(help="commands to remove specified files")
@ -10,22 +8,6 @@ def clean() -> None:
pass pass
def delete_files(
glob_pattern: str,
) -> None:
try:
pth_intp = get_interpreter()
except RuntimeError:
click.echo("Base interpreter path could not be found", err=True)
return
base_folder = pth_intp.parent
assert base_folder.is_dir(), "base folder not a directory"
precomp_files = base_folder.glob(glob_pattern)
for file in precomp_files:
file.unlink(missing_ok=True)
@click.command(help="removes all pre-compiled byte code files in the distribution") @click.command(help="removes all pre-compiled byte code files in the distribution")
def precompiled() -> None: def precompiled() -> None:
try: try:

View File

@ -4,7 +4,7 @@ import subprocess
import click import click
from pycage import clean from pycage import helpers
from pycage.helpers import get_interpreter, print_error from pycage.helpers import get_interpreter, print_error
@ -61,7 +61,7 @@ def compile(
if delete_existing: if delete_existing:
try: try:
clean.delete_files(r"**/*.pyc") helpers.delete_files(r"**/*.pyc")
except Exception as err: except Exception as err:
print_error(err) print_error(err)
return return

17
src/pycage/constants.py Normal file
View File

@ -0,0 +1,17 @@
from pathlib import Path
from typing import Final, TypeAlias
LIB_ROOT_FOLDER: Final[Path] = Path(__file__).parent
if not LIB_ROOT_FOLDER.exists():
raise FileNotFoundError(f"Lib root folder not found under: >{LIB_ROOT_FOLDER}<")
MSVC_FOLDER: Final[Path] = LIB_ROOT_FOLDER / "_files/msvc-redist"
if not MSVC_FOLDER.exists():
raise FileNotFoundError(f"Folder for MSVC Redist files not found under: >{MSVC_FOLDER}<")
PyVersionToReleaseTag: TypeAlias = tuple[str, str]
PY_VERSION_TO_RELEASE_TAG_PAIRS: Final[tuple[PyVersionToReleaseTag, ...]] = (
("3.11.12", "20250409"),
("3.11.13", "20250702"),
("3.11.14", "20251014"),
)

View File

@ -11,7 +11,13 @@ from dopt_basics.io import combine_route
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from pycage import config from pycage import config
from pycage.helpers import delete_folder_recursively, path_verify_existence, print_error from pycage.constants import PY_VERSION_TO_RELEASE_TAG_PAIRS
from pycage.helpers import (
delete_files,
delete_folder_recursively,
path_verify_existence,
print_error,
)
from pycage.types import JsonMetadata, RetrievalInfo from pycage.types import JsonMetadata, RetrievalInfo
# cfg_path = Path.cwd() / "../config/config.toml" # cfg_path = Path.cwd() / "../config/config.toml"
@ -60,7 +66,7 @@ def get_metadata(
route = f"{release_tag}/{target_build}" route = f"{release_tag}/{target_build}"
target_url = combine_route(asset_url, route) target_url = combine_route(asset_url, route)
print(f"Target build:\t{target_build},\nTarget URL:\t{target_url}") click.echo(f"Target build:\t{target_build},\nTarget URL:\t{target_url}")
return RetrievalInfo(url=target_url, file=target_build) return RetrievalInfo(url=target_url, file=target_build)
@ -113,6 +119,28 @@ def extract_archive(
default=None, default=None,
help="specifiy a different target location, default: current working directory", help="specifiy a different target location, default: current working directory",
) )
@click.option(
"-ow",
"--overwrite",
is_flag=True,
show_default=True,
default=False,
help=(
"overwrite installation with specified Python bundle instead of deleting it, any "
"pre-compiled files are deleted to force the re-comilation of the code by the new "
"Python interpreter. CAUTION: No compatibility checks for the installed packages is "
"done. Therefore, it is not guaranteed that the new installation will work. This "
"option is primarily intented for patch versions of Python."
),
)
@click.option(
"-k",
"--keep",
is_flag=True,
show_default=True,
default=False,
help="keep the downloaded archive",
)
@click.option( @click.option(
"-f", "-f",
"--force-reextract", "--force-reextract",
@ -133,6 +161,8 @@ def get(
release_tag: str | None, release_tag: str | None,
force_reextract: bool, force_reextract: bool,
dl_folder: Path | None, dl_folder: Path | None,
overwrite: bool,
keep: bool,
) -> None: ) -> None:
url_metadata = cast(str, config.CFG["metadata"]["URL"]) url_metadata = cast(str, config.CFG["metadata"]["URL"])
os_file_info = config.CFG.os_info os_file_info = config.CFG.os_info
@ -155,21 +185,30 @@ def get(
# destination folder # destination folder
dl_folder = dl_folder if dl_folder is not None else Path.cwd() dl_folder = dl_folder if dl_folder is not None else Path.cwd()
path_verify_existence(dl_folder) path_verify_existence(dl_folder)
# folder_extract = prepare_path(dl_folder, None, None, None, create_folder=True)
target_folder = dl_folder / "python" target_folder = dl_folder / "python"
folder_extract = dl_folder folder_extract = dl_folder
src_file = dl_folder / filename src_file = dl_folder / filename
_overwrite: bool = False
if target_folder.exists() and overwrite:
if click.confirm("Do you really want to overwrite the existing installation?"):
_overwrite = True
else:
click.echo(
"Overwrite option ignored. Delete existing installation before extraction."
)
try: try:
if not src_file.exists(): if not src_file.exists():
click.echo("File not yet available. Download...") click.echo("File not yet available. Download...")
load_file_from_url(url=retrieval_info.url, file_save_path=src_file) load_file_from_url(url=retrieval_info.url, file_save_path=src_file)
delete_folder_recursively(target_folder) if not _overwrite:
delete_folder_recursively(target_folder)
extract_archive(src_file, folder_extract) extract_archive(src_file, folder_extract)
print("Download and extraction successfully.") click.echo("Download and extraction successfully.")
elif force_reextract: elif force_reextract:
click.echo("File already downloaded. Re-extract file...") click.echo("File already downloaded. Re-extract file...")
delete_folder_recursively(target_folder) if not _overwrite:
delete_folder_recursively(target_folder)
extract_archive(src_file, folder_extract) extract_archive(src_file, folder_extract)
click.echo("Re-extraction successfully.") click.echo("Re-extraction successfully.")
else: else:
@ -179,3 +218,37 @@ def get(
) )
except Exception as err: except Exception as err:
print_error(err) print_error(err)
if _overwrite:
click.echo(
"Overwrite option was chosen. Delete any existing pre-compiled "
"files to force recompilation..."
)
try:
delete_files(r"**/*.pyc")
except Exception as err:
print_error(err)
click.echo("Pre-compiled files were removed successfully.")
if not keep:
try:
src_file.unlink()
except Exception as err:
click.echo(
"The archive file could not be deleted because of following exception..."
)
print_error(err)
@click.command(
help=(
"show supported Python version and corresponding release tags from the"
"standalone repository to have a quick lookup which version can be downloaded "
"with which release tag, information can be used with the 'get' command"
)
)
def show_version_release_tag() -> None:
click.echo("Known version and release tag pairs are...")
click.echo("Python Version --> Release Tag")
for pair in PY_VERSION_TO_RELEASE_TAG_PAIRS:
click.echo(f"{pair[0]:<8} --> {pair[1]:>9}")

View File

@ -57,3 +57,19 @@ def get_interpreter() -> Path:
raise RuntimeError("Base interpreter not found") from err raise RuntimeError("Base interpreter not found") from err
return pth_int return pth_int
def delete_files(
glob_pattern: str,
) -> None:
try:
pth_intp = get_interpreter()
except RuntimeError:
click.echo("Base interpreter path could not be found", err=True)
return
base_folder = pth_intp.parent
assert base_folder.is_dir(), "base folder not a directory"
files_to_delete = base_folder.glob(glob_pattern)
for file in files_to_delete:
file.unlink(missing_ok=True)

View File

@ -15,6 +15,7 @@ def cli():
cli.add_command(pycage.get.get) cli.add_command(pycage.get.get)
cli.add_command(pycage.get.show_version_release_tag)
cli.add_command(pycage.venv.venv) cli.add_command(pycage.venv.venv)
cli.add_command(pycage.files.copy) cli.add_command(pycage.files.copy)
cli.add_command(pycage.compile.compile) cli.add_command(pycage.compile.compile)

View File

@ -1,12 +1,14 @@
from __future__ import annotations from __future__ import annotations
import shutil
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
import click import click
from pycage.helpers import get_interpreter from pycage.constants import MSVC_FOLDER
from pycage.helpers import get_interpreter, path_verify_directory, path_verify_existence
@click.group(help="manage virtual environment with downloaded standalone images") @click.group(help="manage virtual environment with downloaded standalone images")
@ -63,6 +65,7 @@ def add_pkg(
package: str, package: str,
index: str | None = None, index: str | None = None,
extra_index: str | None = None, extra_index: str | None = None,
include_prerelease: bool = False,
) -> None: ) -> None:
cmd: list[str] = [ cmd: list[str] = [
str(interpreter), str(interpreter),
@ -79,11 +82,22 @@ def add_pkg(
if extra_index is not None: if extra_index is not None:
add_opts = ["--extra-index-url", extra_index] add_opts = ["--extra-index-url", extra_index]
cmd.extend(add_opts) cmd.extend(add_opts)
if include_prerelease:
add_opts = ["--pre"]
cmd.extend(add_opts)
subprocess.run(cmd) subprocess.run(cmd)
@click.command(help="install packages directly into the virtual environment") @click.command(help="install packages directly into the virtual environment")
@click.option(
"-p",
"--pre",
is_flag=True,
default=False,
show_default=True,
help=("install pre-release versions"),
)
@click.option( @click.option(
"-e", "--extra-index", default=None, help="extra index to lookup packages at by pip" "-e", "--extra-index", default=None, help="extra index to lookup packages at by pip"
) )
@ -93,6 +107,7 @@ def add(
package: str, package: str,
index: str | None, index: str | None,
extra_index: str | None, extra_index: str | None,
pre: bool,
) -> None: ) -> None:
try: try:
pth_intp = get_interpreter() pth_intp = get_interpreter()
@ -105,6 +120,7 @@ def add(
package=package, package=package,
index=index, index=index,
extra_index=extra_index, extra_index=extra_index,
include_prerelease=pre,
) )
@ -145,8 +161,41 @@ def remove(
subprocess.run(cmd) subprocess.run(cmd)
@click.command(
help="add MSVC Redistributable runtime dependencies to the root path of the environment"
)
def add_msvc_redist() -> None:
try:
pth_intp = get_interpreter()
except RuntimeError:
click.echo("Base interpreter path could not be found", err=True)
return
target_path = pth_intp.parent
try:
path_verify_directory(target_path)
except ValueError:
click.echo(f"Target path >{target_path}< does not seem to be a directory", err=True)
return
try:
path_verify_existence(target_path)
except FileNotFoundError:
click.echo(f"Target path >{target_path}< does not seem to exist", err=True)
return
dll_files = MSVC_FOLDER.glob("**/*.dll")
click.echo("Start copying files...")
for dll_src in dll_files:
click.echo(f"Copy file >{dll_src.name}<")
shutil.copy2(dll_src, target_path)
click.echo("Copied files successfully.")
venv.add_command(create) venv.add_command(create)
venv.add_command(add) venv.add_command(add)
venv.add_command(upgrade_pip) venv.add_command(upgrade_pip)
venv.add_command(remove) venv.add_command(remove)
venv.add_command(upgrade_seeders) venv.add_command(upgrade_seeders)
venv.add_command(add_msvc_redist)