ixt-cli 0.8.0__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.
- ixt/__init__.py +8 -0
- ixt/__main__.py +8 -0
- ixt/backends/__init__.py +1 -0
- ixt/backends/binary.py +935 -0
- ixt/backends/binary_resolver.py +307 -0
- ixt/backends/node.py +490 -0
- ixt/backends/python.py +234 -0
- ixt/cli/__init__.py +31 -0
- ixt/cli/argparse_completion.py +557 -0
- ixt/cli/cmd_apply.py +404 -0
- ixt/cli/cmd_cache.py +86 -0
- ixt/cli/cmd_config.py +295 -0
- ixt/cli/cmd_info.py +116 -0
- ixt/cli/cmd_install.py +508 -0
- ixt/cli/cmd_misc.py +261 -0
- ixt/cli/cmd_registry.py +35 -0
- ixt/cli/cmd_upgrade.py +336 -0
- ixt/cli/commands.py +70 -0
- ixt/cli/parser.py +555 -0
- ixt/cli/render.py +85 -0
- ixt/config/__init__.py +5 -0
- ixt/config/asset_index.py +305 -0
- ixt/config/asset_pattern_cache.py +87 -0
- ixt/config/env_policy.py +340 -0
- ixt/config/flags.py +29 -0
- ixt/config/fs_policy.py +17 -0
- ixt/config/heuristics.py +465 -0
- ixt/config/models.py +176 -0
- ixt/config/registry.py +145 -0
- ixt/config/settings.py +173 -0
- ixt/config/setup_toml.py +179 -0
- ixt/config/toml.py +416 -0
- ixt/core/__init__.py +16 -0
- ixt/core/apply.py +564 -0
- ixt/core/apply_actions.py +106 -0
- ixt/core/backend.py +187 -0
- ixt/core/bootstrap.py +410 -0
- ixt/core/cache.py +332 -0
- ixt/core/discover.py +150 -0
- ixt/core/doctor.py +591 -0
- ixt/core/export.py +419 -0
- ixt/core/expose.py +350 -0
- ixt/core/extract.py +261 -0
- ixt/core/hooks.py +182 -0
- ixt/core/identity.py +148 -0
- ixt/core/inject.py +143 -0
- ixt/core/install.py +509 -0
- ixt/core/install_local.py +229 -0
- ixt/core/locks.py +54 -0
- ixt/core/pathlink.py +86 -0
- ixt/core/resolution_stats.py +191 -0
- ixt/core/resolve.py +150 -0
- ixt/core/resolve_cache.py +185 -0
- ixt/core/runtimes.py +192 -0
- ixt/core/save.py +237 -0
- ixt/core/setup_completions.py +11 -0
- ixt/core/setup_path.py +368 -0
- ixt/core/upgrade.py +596 -0
- ixt/data/__init__.py +10 -0
- ixt/data/asset_index.json +574 -0
- ixt/data/heuristics.toml +98 -0
- ixt/data/registry.toml +71 -0
- ixt/libs/__init__.py +3 -0
- ixt/libs/constants.py +4 -0
- ixt/libs/fmt.py +108 -0
- ixt/libs/logger.py +109 -0
- ixt/libs/output.py +25 -0
- ixt/libs/req_spec.py +115 -0
- ixt/libs/semver.py +149 -0
- ixt/libs/shell.py +126 -0
- ixt/libs/style.py +238 -0
- ixt/net/__init__.py +1 -0
- ixt/net/github_api.py +158 -0
- ixt/net/gitlab_api.py +149 -0
- ixt/net/http.py +194 -0
- ixt/net/npm.py +24 -0
- ixt/net/pypi.py +26 -0
- ixt/net/source.py +163 -0
- ixt/platform/__init__.py +131 -0
- ixt/platform/win.py +68 -0
- ixt_cli-0.8.0.dist-info/METADATA +294 -0
- ixt_cli-0.8.0.dist-info/RECORD +84 -0
- ixt_cli-0.8.0.dist-info/WHEEL +4 -0
- ixt_cli-0.8.0.dist-info/entry_points.txt +2 -0
ixt/backends/python.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Python backend — isolated venvs via uv."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import configparser
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ixt.config.settings import Settings, get_settings
|
|
12
|
+
from ixt.core.backend import BackendType
|
|
13
|
+
from ixt.libs.shell import shell_run
|
|
14
|
+
from ixt.platform import get_binary_extension, is_windows
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BinaryNotFoundError(FileNotFoundError):
|
|
18
|
+
"""Raised when a binary is missing from the tool environment."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, binary_name: str, env_dir: Path, available: list[str]):
|
|
21
|
+
self.binary_name = binary_name
|
|
22
|
+
self.env_dir = env_dir
|
|
23
|
+
self.available = available
|
|
24
|
+
avail = ", ".join(available) or "<none>"
|
|
25
|
+
super().__init__(f"Binary '{binary_name}' not found in {env_dir}. Available: {avail}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Filesystem helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _iter_executables(directory: Path) -> list[Path]:
|
|
34
|
+
"""List executable files in *directory*."""
|
|
35
|
+
if not directory.exists():
|
|
36
|
+
return []
|
|
37
|
+
if is_windows():
|
|
38
|
+
allowed = {".exe", ".bat", ".cmd", ".ps1"}
|
|
39
|
+
return sorted(p for p in directory.iterdir() if p.is_file() and p.suffix.lower() in allowed)
|
|
40
|
+
return sorted(p for p in directory.iterdir() if p.is_file() and os.access(p, os.X_OK))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Console-scripts extraction (Python-specific metadata)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_site_packages(venv_dir: Path) -> Path | None:
|
|
49
|
+
"""Locate site-packages inside a ``.venv``."""
|
|
50
|
+
candidates = [venv_dir / "Lib" / "site-packages"]
|
|
51
|
+
candidates.extend(sorted((venv_dir / "lib").glob("python*/site-packages")))
|
|
52
|
+
for c in candidates:
|
|
53
|
+
if c.exists():
|
|
54
|
+
return c
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _dist_info_package_name(dist_info_dir: Path) -> str:
|
|
59
|
+
metadata = dist_info_dir / "METADATA"
|
|
60
|
+
if not metadata.exists():
|
|
61
|
+
return ""
|
|
62
|
+
with metadata.open(encoding="utf-8") as f:
|
|
63
|
+
for line in f:
|
|
64
|
+
if line.startswith("Name:"):
|
|
65
|
+
return line.split(":", 1)[1].strip()
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_console_scripts(
|
|
70
|
+
site_packages: Path,
|
|
71
|
+
) -> dict[str, dict[str, str]]:
|
|
72
|
+
"""Map package names to their console_scripts entry points."""
|
|
73
|
+
scripts: dict[str, dict[str, str]] = {}
|
|
74
|
+
for dist in sorted(site_packages.glob("*.dist-info")):
|
|
75
|
+
pkg = _dist_info_package_name(dist)
|
|
76
|
+
if not pkg:
|
|
77
|
+
continue
|
|
78
|
+
ep = dist / "entry_points.txt"
|
|
79
|
+
if not ep.exists():
|
|
80
|
+
continue
|
|
81
|
+
parser = configparser.ConfigParser()
|
|
82
|
+
parser.read(ep, encoding="utf-8")
|
|
83
|
+
if not parser.has_section("console_scripts"):
|
|
84
|
+
continue
|
|
85
|
+
scripts[pkg] = dict(parser.items("console_scripts"))
|
|
86
|
+
return scripts
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build_pip_metadata(
|
|
90
|
+
venv_dir: Path,
|
|
91
|
+
) -> dict[str, dict[str, dict[str, str]]]:
|
|
92
|
+
site = _find_site_packages(venv_dir)
|
|
93
|
+
if site is None:
|
|
94
|
+
return {"console_scripts": {}}
|
|
95
|
+
return {"console_scripts": _find_console_scripts(site)}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _write_pip_metadata(env_dir: Path, venv_dir: Path) -> Path:
|
|
99
|
+
path = env_dir / "pip_metadata.json"
|
|
100
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
with path.open("w", encoding="utf-8") as f:
|
|
102
|
+
json.dump(_build_pip_metadata(venv_dir), f, indent=2)
|
|
103
|
+
f.write("\n")
|
|
104
|
+
return path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# PythonBackend
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class PythonBackend:
|
|
114
|
+
"""Install Python CLI tools in isolated venvs via ``uv``."""
|
|
115
|
+
|
|
116
|
+
settings: Settings = field(default_factory=get_settings)
|
|
117
|
+
backend_type: BackendType = field(default=BackendType.PYTHON, init=False)
|
|
118
|
+
|
|
119
|
+
# -- path helpers -------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _venv_dir(self, env_dir: Path) -> Path:
|
|
122
|
+
return env_dir / ".venv"
|
|
123
|
+
|
|
124
|
+
def _bin_dir(self, env_dir: Path) -> Path:
|
|
125
|
+
return self._venv_dir(env_dir) / ("Scripts" if is_windows() else "bin")
|
|
126
|
+
|
|
127
|
+
def _python(self, env_dir: Path) -> Path:
|
|
128
|
+
name = "python.exe" if is_windows() else "python"
|
|
129
|
+
return self._bin_dir(env_dir) / name
|
|
130
|
+
|
|
131
|
+
def _uv(self) -> str:
|
|
132
|
+
from ixt.core.bootstrap import ensure_uv
|
|
133
|
+
|
|
134
|
+
return ensure_uv(self.settings)
|
|
135
|
+
|
|
136
|
+
def _run_uv_pip(self, env_dir: Path, command: str, *args: str) -> str:
|
|
137
|
+
result = shell_run(
|
|
138
|
+
[
|
|
139
|
+
self._uv(),
|
|
140
|
+
"pip",
|
|
141
|
+
command,
|
|
142
|
+
"--python",
|
|
143
|
+
str(self._python(env_dir)),
|
|
144
|
+
*args,
|
|
145
|
+
],
|
|
146
|
+
capture_output=True,
|
|
147
|
+
check=True,
|
|
148
|
+
)
|
|
149
|
+
return result.stdout.strip() if result.stdout else ""
|
|
150
|
+
|
|
151
|
+
# -- Backend protocol ---------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def env_exists(self, env_dir: Path) -> bool:
|
|
154
|
+
return self._venv_dir(env_dir).exists()
|
|
155
|
+
|
|
156
|
+
def create_env(self, env_dir: Path) -> bool:
|
|
157
|
+
env_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
if self.env_exists(env_dir):
|
|
159
|
+
return False
|
|
160
|
+
shell_run(
|
|
161
|
+
[self._uv(), "venv", str(self._venv_dir(env_dir))],
|
|
162
|
+
capture_output=True,
|
|
163
|
+
check=True,
|
|
164
|
+
)
|
|
165
|
+
return True
|
|
166
|
+
|
|
167
|
+
def install_packages(
|
|
168
|
+
self,
|
|
169
|
+
env_dir: Path,
|
|
170
|
+
specs: list[str],
|
|
171
|
+
*,
|
|
172
|
+
upgrade: bool = False,
|
|
173
|
+
) -> None:
|
|
174
|
+
args: list[str] = []
|
|
175
|
+
if upgrade:
|
|
176
|
+
args.append("--upgrade")
|
|
177
|
+
args.extend(specs)
|
|
178
|
+
self._run_uv_pip(env_dir, "install", *args)
|
|
179
|
+
_write_pip_metadata(env_dir, self._venv_dir(env_dir))
|
|
180
|
+
|
|
181
|
+
def uninstall_package(self, env_dir: Path, package: str) -> None:
|
|
182
|
+
self._run_uv_pip(env_dir, "uninstall", package)
|
|
183
|
+
|
|
184
|
+
def find_binaries(self, env_dir: Path) -> list[Path]:
|
|
185
|
+
return _iter_executables(self._bin_dir(env_dir))
|
|
186
|
+
|
|
187
|
+
def get_package_binaries(self, env_dir: Path, package_name: str) -> dict[str, str]:
|
|
188
|
+
meta_file = env_dir / "pip_metadata.json"
|
|
189
|
+
if meta_file.exists():
|
|
190
|
+
with meta_file.open(encoding="utf-8") as f:
|
|
191
|
+
data = json.load(f)
|
|
192
|
+
return data.get("console_scripts", {}).get(package_name, {})
|
|
193
|
+
# Fallback: live scan when no cached metadata exists.
|
|
194
|
+
site = _find_site_packages(self._venv_dir(env_dir))
|
|
195
|
+
if site is None:
|
|
196
|
+
return {}
|
|
197
|
+
return _find_console_scripts(site).get(package_name, {})
|
|
198
|
+
|
|
199
|
+
def installed_version(self, env_dir: Path, package_name: str) -> str | None:
|
|
200
|
+
# Try reading version from dist-info METADATA (avoids subprocess).
|
|
201
|
+
site = _find_site_packages(self._venv_dir(env_dir))
|
|
202
|
+
if site is not None:
|
|
203
|
+
normalized = package_name.replace("-", "_").lower()
|
|
204
|
+
for dist in site.glob("*.dist-info"):
|
|
205
|
+
if not dist.name.lower().startswith(normalized + "-"):
|
|
206
|
+
continue
|
|
207
|
+
metadata = dist / "METADATA"
|
|
208
|
+
if not metadata.exists():
|
|
209
|
+
continue
|
|
210
|
+
with metadata.open(encoding="utf-8") as f:
|
|
211
|
+
for line in f:
|
|
212
|
+
if line.startswith("Version:"):
|
|
213
|
+
return line.split(":", 1)[1].strip()
|
|
214
|
+
# Fallback: parse `uv pip freeze` output.
|
|
215
|
+
for line in self.export(env_dir):
|
|
216
|
+
if "==" in line:
|
|
217
|
+
name, version = line.split("==", 1)
|
|
218
|
+
if name.strip().lower() == package_name.lower():
|
|
219
|
+
return version.strip()
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def export(self, env_dir: Path) -> list[str]:
|
|
223
|
+
output = self._run_uv_pip(env_dir, "freeze")
|
|
224
|
+
return [line for line in output.splitlines() if line]
|
|
225
|
+
|
|
226
|
+
# -- extra helpers (Python-specific) ------------------------------------
|
|
227
|
+
|
|
228
|
+
def venv_bin(self, env_dir: Path, name: str, *, fail_if_missing: bool = True) -> Path:
|
|
229
|
+
"""Get a specific binary by name from the venv."""
|
|
230
|
+
path = self._bin_dir(env_dir) / f"{name}{get_binary_extension()}"
|
|
231
|
+
if fail_if_missing and not path.exists():
|
|
232
|
+
available = [p.stem for p in self.find_binaries(env_dir)]
|
|
233
|
+
raise BinaryNotFoundError(name, env_dir, available)
|
|
234
|
+
return path
|
ixt/cli/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""CLI module — entry point for the ixt command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["main"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
"""Entry point for ixt CLI."""
|
|
10
|
+
from ixt.cli.commands import COMMANDS
|
|
11
|
+
from ixt.cli.parser import build_parser, normalize_verbosity
|
|
12
|
+
|
|
13
|
+
parser = build_parser()
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
vlevel = normalize_verbosity(args)
|
|
17
|
+
if vlevel:
|
|
18
|
+
from ixt.libs.logger import Level, set_global_level
|
|
19
|
+
|
|
20
|
+
set_global_level(Level.DEBUG, verbosity=vlevel)
|
|
21
|
+
|
|
22
|
+
if not getattr(args, "command", None):
|
|
23
|
+
parser.print_help()
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
handler = COMMANDS.get(args.command)
|
|
27
|
+
if handler is None:
|
|
28
|
+
parser.print_help()
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
return handler(args)
|