reyk-cli 0.0.1__tar.gz
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.
- reyk_cli-0.0.1/PKG-INFO +9 -0
- reyk_cli-0.0.1/README.md +0 -0
- reyk_cli-0.0.1/pyproject.toml +20 -0
- reyk_cli-0.0.1/src/reyk_cli/__init__.py +0 -0
- reyk_cli-0.0.1/src/reyk_cli/cli.py +79 -0
- reyk_cli-0.0.1/src/reyk_cli/command_executor.py +13 -0
- reyk_cli-0.0.1/src/reyk_cli/configuration_reader.py +33 -0
- reyk_cli-0.0.1/src/reyk_cli/uv_vendorizer.py +70 -0
reyk_cli-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: reyk-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Manage isolated environment dependencies.
|
|
5
|
+
Requires-Dist: typer>=0.20.0
|
|
6
|
+
Requires-Dist: tomli>=2.4.0 ; python_full_version < '3.11'
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
reyk_cli-0.0.1/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "reyk-cli"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Manage isolated environment dependencies."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = ["typer>=0.20.0", "tomli>=2.4.0; python_version < '3.11'"]
|
|
8
|
+
|
|
9
|
+
[dependency-groups]
|
|
10
|
+
dev = ["pytest>=8.4.2", "tomli-w>=1.2.0"]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["uv_build>=0.8.3,<0.9.0"]
|
|
14
|
+
build-backend = "uv_build"
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
reyk-cli = "reyk_cli.cli:app"
|
|
18
|
+
|
|
19
|
+
[tool.pytest.ini_options]
|
|
20
|
+
pythonpath = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, cast
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from reyk_cli.configuration_reader import DEFAULT_VENDOR_GROUP, read_reyk_configuration
|
|
7
|
+
from reyk_cli.uv_vendorizer import UVBasedVendorizer
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
help="Manage isolated environment dependencies.",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
group_option = typer.Option(
|
|
15
|
+
default=DEFAULT_VENDOR_GROUP,
|
|
16
|
+
help="Dependency group to to vendor.", # https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def main(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
config: Annotated[
|
|
24
|
+
Path,
|
|
25
|
+
typer.Option(
|
|
26
|
+
help="Where to find pyproject.toml containing vendored dependencies",
|
|
27
|
+
file_okay=True,
|
|
28
|
+
dir_okay=False,
|
|
29
|
+
exists=True,
|
|
30
|
+
readable=True,
|
|
31
|
+
),
|
|
32
|
+
] = Path("./pyproject.toml"),
|
|
33
|
+
) -> None:
|
|
34
|
+
reyk_config = read_reyk_configuration(config)
|
|
35
|
+
ctx.obj = UVBasedVendorizer(
|
|
36
|
+
project_root=config.parent,
|
|
37
|
+
vendor_groups=reyk_config.vendor_groups,
|
|
38
|
+
libraries_target_path=Path(reyk_config.libraries_path),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command(help="Vendor the isolated environment dependencies.")
|
|
43
|
+
def sync(ctx: typer.Context) -> None:
|
|
44
|
+
"""Install the isolated environment dependencies."""
|
|
45
|
+
cast(UVBasedVendorizer, ctx.obj).sync_from_group_to_target_path()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command(
|
|
49
|
+
add_help_option=False,
|
|
50
|
+
# Allow any extra args to be passed to `uv`
|
|
51
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
52
|
+
)
|
|
53
|
+
def add(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
group: str = group_option,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Add packages to the isolated environment."""
|
|
58
|
+
vendorizer = cast(UVBasedVendorizer, ctx.obj)
|
|
59
|
+
vendorizer.add_new_requirement_to_pyproject(group, ctx.args)
|
|
60
|
+
vendorizer.sync_from_group_to_target_path()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command(
|
|
64
|
+
add_help_option=False,
|
|
65
|
+
# Allow any extra args to be passed to `uv`
|
|
66
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
67
|
+
)
|
|
68
|
+
def remove(
|
|
69
|
+
ctx: typer.Context,
|
|
70
|
+
group: str = group_option,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Remove packages from the isolated environment."""
|
|
73
|
+
vendorizer = cast(UVBasedVendorizer, ctx.obj)
|
|
74
|
+
vendorizer.remove_requirement_from_pyproject(group, ctx.args)
|
|
75
|
+
vendorizer.sync_from_group_to_target_path()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__": # pragma: no cover
|
|
79
|
+
app()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import logging
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
LOGGER = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_command_exit_on_fail(cmd: list[str]) -> None:
|
|
10
|
+
result = subprocess.run(cmd, check=False) # noqa: S603
|
|
11
|
+
LOGGER.debug(f"Executed {cmd=} -> {result.returncode=}")
|
|
12
|
+
if result.returncode != 0:
|
|
13
|
+
raise typer.Exit(result.returncode)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import tomllib
|
|
7
|
+
except ImportError:
|
|
8
|
+
import tomli as tomllib # type: ignore[reportMissingImports]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TOOLS_CONFIGURATION_NAME = "tool"
|
|
12
|
+
REYK_CONFIGURATION_NAME = "reyk"
|
|
13
|
+
|
|
14
|
+
DEFAULT_LIBRARIES_TARGET_PATH = "libs" # Path the vendored libraries will be in
|
|
15
|
+
DEFAULT_VENDOR_GROUP = "vendor-libs"
|
|
16
|
+
DEFAULT_VENDOR_GROUPS = {DEFAULT_VENDOR_GROUP}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ReykConfiguration:
|
|
21
|
+
libraries_path: str
|
|
22
|
+
vendor_groups: set[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_reyk_configuration(toml_path: Path) -> ReykConfiguration:
|
|
26
|
+
toml_data = tomllib.loads(toml_path.read_text())
|
|
27
|
+
configuration = cast(
|
|
28
|
+
dict[str, Any],
|
|
29
|
+
toml_data.get(TOOLS_CONFIGURATION_NAME, {}).get(REYK_CONFIGURATION_NAME, {}),
|
|
30
|
+
)
|
|
31
|
+
configuration["libraries_path"] = configuration.pop("libraries-path", DEFAULT_LIBRARIES_TARGET_PATH)
|
|
32
|
+
configuration["vendor_groups"] = set(configuration.pop("vendor-groups", DEFAULT_VENDOR_GROUPS))
|
|
33
|
+
return ReykConfiguration(**configuration)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from reyk_cli.command_executor import run_command_exit_on_fail
|
|
8
|
+
|
|
9
|
+
LOCK_FILE_NAME = "vendor.lock"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UVBasedVendorizer:
|
|
13
|
+
def __init__(self, project_root: Path, vendor_groups: set[str], libraries_target_path: Path) -> None:
|
|
14
|
+
self._project_root = project_root
|
|
15
|
+
self._vendor_groups = vendor_groups
|
|
16
|
+
self._libraries_target_path = libraries_target_path
|
|
17
|
+
|
|
18
|
+
def add_new_requirement_to_pyproject(self, vendor_group: str, command_args: Sequence[str]) -> None:
|
|
19
|
+
# The `uv add` command doesn't support installing to a custom target,
|
|
20
|
+
# so we use `--frozen` just to append the package to the `pyproject.toml`.
|
|
21
|
+
self._ensure_group_exists(vendor_group)
|
|
22
|
+
run_command_exit_on_fail(["uv", "add", "--frozen", "--group", vendor_group, *command_args])
|
|
23
|
+
|
|
24
|
+
def remove_requirement_from_pyproject(self, vendor_group: str, command_args: Sequence[str]) -> None:
|
|
25
|
+
# The `uv remove` command doesn't support installing to a custom target,
|
|
26
|
+
# so we use `--frozen` just to remove the package from the `pyproject.toml`.
|
|
27
|
+
self._ensure_group_exists(vendor_group)
|
|
28
|
+
run_command_exit_on_fail(["uv", "remove", "--frozen", "--group", vendor_group, *command_args])
|
|
29
|
+
|
|
30
|
+
def _ensure_group_exists(self, vendor_group: str) -> None:
|
|
31
|
+
if vendor_group not in self._vendor_groups:
|
|
32
|
+
raise typer.BadParameter(f"{vendor_group!s} not in {self._vendor_groups=}")
|
|
33
|
+
|
|
34
|
+
def sync_from_group_to_target_path(self) -> None:
|
|
35
|
+
# `uv pip sync` is the only command that supports installing to a custom target,
|
|
36
|
+
# but it requires a lock file. Therefore, we first export the lock file of the libs group
|
|
37
|
+
# and then use it to install the packages to the desired target.
|
|
38
|
+
self.create_lock_file_on_group()
|
|
39
|
+
self.install_requirements_from_lock_file()
|
|
40
|
+
|
|
41
|
+
def create_lock_file_on_group(self) -> None:
|
|
42
|
+
run_command_exit_on_fail(
|
|
43
|
+
[
|
|
44
|
+
"uv",
|
|
45
|
+
"export",
|
|
46
|
+
*itertools.chain.from_iterable([("--only-group", group) for group in self._vendor_groups]),
|
|
47
|
+
"--no-header",
|
|
48
|
+
"--quiet",
|
|
49
|
+
"--no-emit-project",
|
|
50
|
+
"--output-file",
|
|
51
|
+
str(self.lock_file_path),
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def install_requirements_from_lock_file(self) -> None:
|
|
56
|
+
run_command_exit_on_fail(
|
|
57
|
+
[
|
|
58
|
+
"uv",
|
|
59
|
+
"pip",
|
|
60
|
+
"sync",
|
|
61
|
+
str(self.lock_file_path),
|
|
62
|
+
"--target",
|
|
63
|
+
str(self._libraries_target_path),
|
|
64
|
+
"--allow-empty-requirements",
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def lock_file_path(self) -> Path:
|
|
70
|
+
return self._project_root / LOCK_FILE_NAME
|