pypilot-cli 1.0.4__tar.gz → 1.0.6__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.
- pypilot_cli-1.0.6/.github/workflows/ci.yml +33 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/PKG-INFO +2 -2
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/README.md +1 -1
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/pyproject.toml +1 -1
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/src/pypilot/__init__.py +1 -1
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/src/pypilot/binary.py +50 -12
- pypilot_cli-1.0.6/tests/test_cli.py +112 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/uv.lock +1 -1
- pypilot_cli-1.0.4/tests/test_cli.py +0 -27
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/.github/workflows/publish.yml +0 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/.gitignore +0 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/.python-version +0 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/LICENSE +0 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/src/pypilot/cli.py +0 -0
- {pypilot_cli-1.0.4 → pypilot_cli-1.0.6}/src/pypilot/py.typed +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: py${{ matrix.python }} (${{ matrix.os }})
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest]
|
|
16
|
+
python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
include:
|
|
18
|
+
# The oldest and newest supported Pythons on the other platforms the
|
|
19
|
+
# engine ships for, since archive handling differs between them.
|
|
20
|
+
- { os: windows-latest, python: "3.8" }
|
|
21
|
+
- { os: windows-latest, python: "3.14" }
|
|
22
|
+
- { os: macos-latest, python: "3.8" }
|
|
23
|
+
- { os: macos-latest, python: "3.14" }
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Install uv
|
|
28
|
+
uses: astral-sh/setup-uv@v5
|
|
29
|
+
|
|
30
|
+
# Warnings are errors so a deprecation that a new Python release turns
|
|
31
|
+
# into a behaviour change (as 3.14 did for tarfile) fails here first.
|
|
32
|
+
- name: Test
|
|
33
|
+
run: uv run --python ${{ matrix.python }} python -m pytest -W error
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pypilot-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Deterministic Python environment doctor, dependency solver, and hardware compatibility manager.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Abdullah-Masood-05/pypilot
|
|
6
6
|
Project-URL: Repository, https://github.com/Abdullah-Masood-05/pypilot
|
|
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
[](https://pypi.org/project/pypilot-cli/)
|
|
32
32
|
[](LICENSE)
|
|
33
33
|
[](https://github.com/Abdullah-Masood-05/pypilot)
|
|
34
|
-
[](https://python.org)
|
|
35
35
|
|
|
36
36
|
**PyPilot** is a deterministic Python environment doctor, dependency solver, and toolchain manager. Powered by a high-speed compiled **Rust engine**, PyPilot calculates dependency intersections directly from PyPI wheel metadata, resolves hardware and CUDA build compatibility, and sets up working virtual environments without external AI or API keys.
|
|
37
37
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://pypi.org/project/pypilot-cli/)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/Abdullah-Masood-05/pypilot)
|
|
6
|
-
[](https://python.org)
|
|
7
7
|
|
|
8
8
|
**PyPilot** is a deterministic Python environment doctor, dependency solver, and toolchain manager. Powered by a high-speed compiled **Rust engine**, PyPilot calculates dependency intersections directly from PyPI wheel metadata, resolves hardware and CUDA build compatibility, and sets up working virtual environments without external AI or API keys.
|
|
9
9
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pypilot-cli"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.6"
|
|
8
8
|
description = "Deterministic Python environment doctor, dependency solver, and hardware compatibility manager."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -17,7 +17,7 @@ import zipfile
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
GITHUB_REPO = "Abdullah-Masood-05/pypilot"
|
|
20
|
-
DEFAULT_TAG = "v1.0.
|
|
20
|
+
DEFAULT_TAG = "v1.0.5"
|
|
21
21
|
# Oldest helper accepted from ~/.cargo/bin or PATH.
|
|
22
22
|
MIN_VERSION = (1, 0, 2)
|
|
23
23
|
# The release this package ships with. A cached download older than this is
|
|
@@ -87,7 +87,10 @@ def probe_binary(path: Path | str) -> tuple[int, int, int] | None:
|
|
|
87
87
|
[str(path), "--version"],
|
|
88
88
|
capture_output=True,
|
|
89
89
|
text=True,
|
|
90
|
-
|
|
90
|
+
# macOS scans a freshly downloaded binary on its first launch, which
|
|
91
|
+
# can take several seconds. Timing out here made a good engine look
|
|
92
|
+
# broken and triggered a needless re-download.
|
|
93
|
+
timeout=30,
|
|
91
94
|
check=False,
|
|
92
95
|
)
|
|
93
96
|
if res.returncode == 0:
|
|
@@ -97,6 +100,23 @@ def probe_binary(path: Path | str) -> tuple[int, int, int] | None:
|
|
|
97
100
|
return None
|
|
98
101
|
|
|
99
102
|
|
|
103
|
+
def extract_archive(archive: Path | str, dest: Path | str) -> None:
|
|
104
|
+
"""Extract a downloaded `.zip` or `.tar.gz` release archive into `dest`."""
|
|
105
|
+
if str(archive).endswith(".zip"):
|
|
106
|
+
with zipfile.ZipFile(archive, "r") as zf:
|
|
107
|
+
zf.extractall(dest)
|
|
108
|
+
return
|
|
109
|
+
with tarfile.open(archive, "r:gz") as tf:
|
|
110
|
+
# Python 3.14 filters tar members by default and 3.12/3.13 warn that it
|
|
111
|
+
# is coming. Ask for the same behaviour everywhere it is available (it
|
|
112
|
+
# was backported to the late 3.8-3.11 releases): plain files only, and
|
|
113
|
+
# nothing may land outside `dest`.
|
|
114
|
+
if hasattr(tarfile, "data_filter"):
|
|
115
|
+
tf.extractall(dest, filter="data")
|
|
116
|
+
else:
|
|
117
|
+
tf.extractall(dest)
|
|
118
|
+
|
|
119
|
+
|
|
100
120
|
def download_binary(tag: str = DEFAULT_TAG) -> Path:
|
|
101
121
|
"""Download prebuilt native binary from GitHub releases."""
|
|
102
122
|
slug, ext = get_platform_slug()
|
|
@@ -115,12 +135,7 @@ def download_binary(tag: str = DEFAULT_TAG) -> Path:
|
|
|
115
135
|
with urllib.request.urlopen(req) as resp, open(tmp_archive, "wb") as f:
|
|
116
136
|
shutil.copyfileobj(resp, f)
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
with zipfile.ZipFile(tmp_archive, "r") as zf:
|
|
120
|
-
zf.extractall(tmpdir)
|
|
121
|
-
else:
|
|
122
|
-
with tarfile.open(tmp_archive, "r:gz") as tf:
|
|
123
|
-
tf.extractall(tmpdir)
|
|
138
|
+
extract_archive(tmp_archive, tmpdir)
|
|
124
139
|
|
|
125
140
|
extracted_bin = Path(tmpdir) / bin_name
|
|
126
141
|
if not extracted_bin.exists():
|
|
@@ -131,14 +146,37 @@ def download_binary(tag: str = DEFAULT_TAG) -> Path:
|
|
|
131
146
|
else:
|
|
132
147
|
raise FileNotFoundError(f"{bin_name} not found in downloaded archive {archive_name}")
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if sys.platform != "win32":
|
|
137
|
-
dest_binary.chmod(dest_binary.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
149
|
+
install_binary(extracted_bin, dest_binary)
|
|
138
150
|
|
|
139
151
|
return dest_binary
|
|
140
152
|
|
|
141
153
|
|
|
154
|
+
def install_binary(src: Path, dest: Path) -> None:
|
|
155
|
+
"""Put `src` at `dest` as a new file, never by rewriting `dest` in place.
|
|
156
|
+
|
|
157
|
+
macOS caches what it knows about an executable per file. Overwriting a
|
|
158
|
+
binary that was just run leaves the old cache entry pointing at new bytes,
|
|
159
|
+
and the next launch fails with "Exec format error". Copying to a temporary
|
|
160
|
+
name and renaming over `dest` gives it a fresh file instead.
|
|
161
|
+
|
|
162
|
+
Windows has no such cache, but its antivirus briefly holds a freshly
|
|
163
|
+
written .exe open, which makes an immediate rename fail with "Access is
|
|
164
|
+
denied". There, a plain copy is both sufficient and reliable.
|
|
165
|
+
"""
|
|
166
|
+
if sys.platform == "win32":
|
|
167
|
+
shutil.copy2(src, dest)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
tmp = dest.with_name(f".{dest.name}.{os.getpid()}.tmp")
|
|
171
|
+
try:
|
|
172
|
+
shutil.copy2(src, tmp)
|
|
173
|
+
tmp.chmod(tmp.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
174
|
+
os.replace(tmp, dest)
|
|
175
|
+
finally:
|
|
176
|
+
if tmp.exists():
|
|
177
|
+
tmp.unlink()
|
|
178
|
+
|
|
179
|
+
|
|
142
180
|
def ensure_binary() -> Path:
|
|
143
181
|
"""
|
|
144
182
|
Locates an existing binary on PATH or in cache, or downloads it from GitHub.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import pypilot
|
|
3
|
+
from pypilot.binary import ensure_binary, get_platform_slug
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_platform_slug():
|
|
7
|
+
slug, ext = get_platform_slug()
|
|
8
|
+
assert slug in ("linux-x64", "darwin-x64", "darwin-arm64", "windows-x64")
|
|
9
|
+
assert ext in ("tar.gz", "zip")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_ensure_binary():
|
|
13
|
+
bin_path = ensure_binary()
|
|
14
|
+
assert bin_path.exists()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_pypilot_version():
|
|
18
|
+
res = pypilot.run(["--version"], capture_output=True)
|
|
19
|
+
assert res.returncode == 0
|
|
20
|
+
assert "pypilot" in res.stdout
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_bundled_version_matches_default_tag():
|
|
24
|
+
from pypilot.binary import BUNDLED_VERSION, DEFAULT_TAG, MIN_VERSION
|
|
25
|
+
|
|
26
|
+
assert DEFAULT_TAG == "v" + ".".join(map(str, BUNDLED_VERSION))
|
|
27
|
+
assert BUNDLED_VERSION >= MIN_VERSION
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_extract_archive_reads_tar_gz_without_warnings(tmp_path):
|
|
31
|
+
import io
|
|
32
|
+
import tarfile
|
|
33
|
+
import warnings
|
|
34
|
+
|
|
35
|
+
from pypilot.binary import extract_archive
|
|
36
|
+
|
|
37
|
+
archive = tmp_path / "pypilot-linux-x64.tar.gz"
|
|
38
|
+
payload = b"#!/bin/sh\n"
|
|
39
|
+
with tarfile.open(archive, "w:gz") as tf:
|
|
40
|
+
info = tarfile.TarInfo("pypilot")
|
|
41
|
+
info.size = len(payload)
|
|
42
|
+
info.mode = 0o755
|
|
43
|
+
tf.addfile(info, io.BytesIO(payload))
|
|
44
|
+
|
|
45
|
+
out = tmp_path / "out"
|
|
46
|
+
out.mkdir()
|
|
47
|
+
with warnings.catch_warnings():
|
|
48
|
+
warnings.simplefilter("error") # 3.12/3.13 warn about the 3.14 default
|
|
49
|
+
extract_archive(archive, out)
|
|
50
|
+
assert (out / "pypilot").read_bytes() == payload
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_extract_archive_reads_zip(tmp_path):
|
|
54
|
+
import zipfile
|
|
55
|
+
|
|
56
|
+
from pypilot.binary import extract_archive
|
|
57
|
+
|
|
58
|
+
archive = tmp_path / "pypilot-windows-x64.zip"
|
|
59
|
+
with zipfile.ZipFile(archive, "w") as zf:
|
|
60
|
+
zf.writestr("pypilot.exe", b"MZ")
|
|
61
|
+
|
|
62
|
+
out = tmp_path / "out"
|
|
63
|
+
out.mkdir()
|
|
64
|
+
extract_archive(archive, out)
|
|
65
|
+
assert (out / "pypilot.exe").read_bytes() == b"MZ"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_extract_archive_refuses_members_outside_the_destination(tmp_path):
|
|
69
|
+
import io
|
|
70
|
+
import tarfile
|
|
71
|
+
|
|
72
|
+
import pytest
|
|
73
|
+
|
|
74
|
+
from pypilot.binary import extract_archive
|
|
75
|
+
|
|
76
|
+
if not hasattr(tarfile, "data_filter"):
|
|
77
|
+
pytest.skip("this Python predates tarfile extraction filters")
|
|
78
|
+
|
|
79
|
+
archive = tmp_path / "evil.tar.gz"
|
|
80
|
+
with tarfile.open(archive, "w:gz") as tf:
|
|
81
|
+
info = tarfile.TarInfo("../escaped")
|
|
82
|
+
info.size = 1
|
|
83
|
+
tf.addfile(info, io.BytesIO(b"x"))
|
|
84
|
+
|
|
85
|
+
out = tmp_path / "out"
|
|
86
|
+
out.mkdir()
|
|
87
|
+
with pytest.raises(tarfile.TarError):
|
|
88
|
+
extract_archive(archive, out)
|
|
89
|
+
assert not (tmp_path / "escaped").exists()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_install_binary_replaces_instead_of_rewriting_in_place(tmp_path):
|
|
93
|
+
import os
|
|
94
|
+
import sys
|
|
95
|
+
|
|
96
|
+
from pypilot.binary import install_binary
|
|
97
|
+
|
|
98
|
+
dest = tmp_path / "pypilot"
|
|
99
|
+
dest.write_bytes(b"old engine")
|
|
100
|
+
old_inode = os.stat(dest).st_ino
|
|
101
|
+
|
|
102
|
+
src = tmp_path / "new"
|
|
103
|
+
src.write_bytes(b"new engine")
|
|
104
|
+
install_binary(src, dest)
|
|
105
|
+
|
|
106
|
+
assert dest.read_bytes() == b"new engine"
|
|
107
|
+
# A rename gives `dest` a new file; rewriting in place would keep the inode.
|
|
108
|
+
if sys.platform != "win32":
|
|
109
|
+
assert os.stat(dest).st_ino != old_inode
|
|
110
|
+
assert os.access(dest, os.X_OK)
|
|
111
|
+
# No temporary file is left behind.
|
|
112
|
+
assert sorted(p.name for p in tmp_path.iterdir()) == ["new", "pypilot"]
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
import pypilot
|
|
3
|
-
from pypilot.binary import ensure_binary, get_platform_slug
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_platform_slug():
|
|
7
|
-
slug, ext = get_platform_slug()
|
|
8
|
-
assert slug in ("linux-x64", "darwin-x64", "darwin-arm64", "windows-x64")
|
|
9
|
-
assert ext in ("tar.gz", "zip")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_ensure_binary():
|
|
13
|
-
bin_path = ensure_binary()
|
|
14
|
-
assert bin_path.exists()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_pypilot_version():
|
|
18
|
-
res = pypilot.run(["--version"], capture_output=True)
|
|
19
|
-
assert res.returncode == 0
|
|
20
|
-
assert "pypilot" in res.stdout
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_bundled_version_matches_default_tag():
|
|
24
|
-
from pypilot.binary import BUNDLED_VERSION, DEFAULT_TAG, MIN_VERSION
|
|
25
|
-
|
|
26
|
-
assert DEFAULT_TAG == "v" + ".".join(map(str, BUNDLED_VERSION))
|
|
27
|
-
assert BUNDLED_VERSION >= MIN_VERSION
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|