peep-install 0.1.0__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.
- peep_install-0.1.0/PKG-INFO +7 -0
- peep_install-0.1.0/peep/__init__.py +2 -0
- peep_install-0.1.0/peep/build.py +154 -0
- peep_install-0.1.0/peep/cli.py +129 -0
- peep_install-0.1.0/peep/diagnostics.py +181 -0
- peep_install-0.1.0/peep/environment.py +168 -0
- peep_install-0.1.0/peep/installer.py +123 -0
- peep_install-0.1.0/peep/resolver.py +131 -0
- peep_install-0.1.0/peep_install.egg-info/PKG-INFO +7 -0
- peep_install-0.1.0/peep_install.egg-info/SOURCES.txt +14 -0
- peep_install-0.1.0/peep_install.egg-info/dependency_links.txt +1 -0
- peep_install-0.1.0/peep_install.egg-info/entry_points.txt +2 -0
- peep_install-0.1.0/peep_install.egg-info/requires.txt +2 -0
- peep_install-0.1.0/peep_install.egg-info/top_level.txt +1 -0
- peep_install-0.1.0/pyproject.toml +20 -0
- peep_install-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import platform
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class BuildRequirement:
|
|
10
|
+
name: str # Human-readable name e.g. "GCC compiler"
|
|
11
|
+
check_command: str # Command to check if it exists e.g. "gcc"
|
|
12
|
+
install_apt: str | None # apt package name
|
|
13
|
+
install_brew: str | None # brew package name
|
|
14
|
+
install_hint: str | None # Fallback manual hint if auto-install fails
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Known build requirements mapped to PyPI package name fragments
|
|
18
|
+
KNOWN_BUILD_DEPS: dict[str, list[BuildRequirement]] = {
|
|
19
|
+
"numpy": [
|
|
20
|
+
BuildRequirement("GCC compiler", "gcc", "gcc", "gcc", None),
|
|
21
|
+
BuildRequirement("Python headers", "python3-config", "python3-dev", None, "Install python3-dev"),
|
|
22
|
+
],
|
|
23
|
+
"pillow": [
|
|
24
|
+
BuildRequirement("libjpeg", None, "libjpeg-dev", "jpeg", None),
|
|
25
|
+
BuildRequirement("zlib", None, "zlib1g-dev", "zlib", None),
|
|
26
|
+
BuildRequirement("GCC compiler", "gcc", "gcc", "gcc", None),
|
|
27
|
+
],
|
|
28
|
+
"cryptography": [
|
|
29
|
+
BuildRequirement("Rust toolchain", "rustc", None, "rust", "Install from https://rustup.rs"),
|
|
30
|
+
BuildRequirement("GCC compiler", "gcc", "gcc", "gcc", None),
|
|
31
|
+
BuildRequirement("OpenSSL headers", None, "libssl-dev", "openssl", None),
|
|
32
|
+
],
|
|
33
|
+
"lxml": [
|
|
34
|
+
BuildRequirement("libxml2", None, "libxml2-dev", "libxml2", None),
|
|
35
|
+
BuildRequirement("libxslt", None, "libxslt-dev", "libxslt", None),
|
|
36
|
+
BuildRequirement("GCC compiler", "gcc", "gcc", "gcc", None),
|
|
37
|
+
],
|
|
38
|
+
"psycopg2": [
|
|
39
|
+
BuildRequirement("PostgreSQL headers", "pg_config", "libpq-dev", "postgresql", None),
|
|
40
|
+
BuildRequirement("GCC compiler", "gcc", "gcc", "gcc", None),
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Generic build tools needed for any source compilation
|
|
45
|
+
GENERIC_BUILD_DEPS = [
|
|
46
|
+
BuildRequirement("GCC compiler", "gcc", "build-essential", "gcc", None),
|
|
47
|
+
BuildRequirement("Python headers", "python3-config", "python3-dev", None, "Install python3-dev or python-dev"),
|
|
48
|
+
BuildRequirement("Make", "make", "make", "make", None),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_package_manager() -> str | None:
|
|
53
|
+
"""Detect the available system package manager."""
|
|
54
|
+
if platform.system() == "Darwin":
|
|
55
|
+
if shutil.which("brew"):
|
|
56
|
+
return "brew"
|
|
57
|
+
return None
|
|
58
|
+
elif platform.system() == "Linux":
|
|
59
|
+
if shutil.which("apt-get"):
|
|
60
|
+
return "apt"
|
|
61
|
+
elif shutil.which("dnf"):
|
|
62
|
+
return "dnf"
|
|
63
|
+
elif shutil.which("pacman"):
|
|
64
|
+
return "pacman"
|
|
65
|
+
return None
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_present(req: BuildRequirement) -> bool:
|
|
70
|
+
"""Check if a build requirement is already satisfied."""
|
|
71
|
+
if req.check_command:
|
|
72
|
+
return shutil.which(req.check_command) is not None
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _install_dep(req: BuildRequirement, pkg_manager: str) -> bool:
|
|
77
|
+
"""Attempt to install a build dependency via the system package manager."""
|
|
78
|
+
pkg = None
|
|
79
|
+
|
|
80
|
+
if pkg_manager == "apt":
|
|
81
|
+
pkg = req.install_apt
|
|
82
|
+
cmd = ["sudo", "apt-get", "install", "-y", pkg] if pkg else None
|
|
83
|
+
elif pkg_manager == "brew":
|
|
84
|
+
pkg = req.install_brew
|
|
85
|
+
cmd = ["brew", "install", pkg] if pkg else None
|
|
86
|
+
elif pkg_manager == "dnf":
|
|
87
|
+
pkg = req.install_apt # dnf package names are often the same as apt
|
|
88
|
+
cmd = ["sudo", "dnf", "install", "-y", pkg] if pkg else None
|
|
89
|
+
elif pkg_manager == "pacman":
|
|
90
|
+
pkg = req.install_apt
|
|
91
|
+
cmd = ["sudo", "pacman", "-S", "--noconfirm", pkg] if pkg else None
|
|
92
|
+
else:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
if not cmd:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
100
|
+
return result.returncode == 0
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_and_fix_build_deps(package_name: str, needs_compilation: bool) -> dict:
|
|
106
|
+
"""
|
|
107
|
+
Check for and silently fix missing build dependencies for a package.
|
|
108
|
+
Returns a report of what was found and what was done.
|
|
109
|
+
"""
|
|
110
|
+
if not needs_compilation:
|
|
111
|
+
return {"needed": False}
|
|
112
|
+
|
|
113
|
+
pkg_manager = get_package_manager()
|
|
114
|
+
pkg_lower = package_name.lower()
|
|
115
|
+
|
|
116
|
+
# Gather required build deps: generic ones + any package-specific ones
|
|
117
|
+
required: list[BuildRequirement] = list(GENERIC_BUILD_DEPS)
|
|
118
|
+
for key, deps in KNOWN_BUILD_DEPS.items():
|
|
119
|
+
if key in pkg_lower or pkg_lower in key:
|
|
120
|
+
required.extend(deps)
|
|
121
|
+
|
|
122
|
+
# Deduplicate by name
|
|
123
|
+
seen = set()
|
|
124
|
+
unique_required = []
|
|
125
|
+
for r in required:
|
|
126
|
+
if r.name not in seen:
|
|
127
|
+
seen.add(r.name)
|
|
128
|
+
unique_required.append(r)
|
|
129
|
+
|
|
130
|
+
missing = [r for r in unique_required if not _is_present(r)]
|
|
131
|
+
|
|
132
|
+
if not missing:
|
|
133
|
+
return {"needed": True, "missing": [], "fixed": [], "failed": []}
|
|
134
|
+
|
|
135
|
+
fixed = []
|
|
136
|
+
failed = []
|
|
137
|
+
|
|
138
|
+
for req in missing:
|
|
139
|
+
if pkg_manager:
|
|
140
|
+
success = _install_dep(req, pkg_manager)
|
|
141
|
+
if success:
|
|
142
|
+
fixed.append(req.name)
|
|
143
|
+
else:
|
|
144
|
+
failed.append({"dep": req.name, "hint": req.install_hint})
|
|
145
|
+
else:
|
|
146
|
+
failed.append({"dep": req.name, "hint": req.install_hint})
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"needed": True,
|
|
150
|
+
"missing": [r.name for r in missing],
|
|
151
|
+
"fixed": fixed,
|
|
152
|
+
"failed": failed,
|
|
153
|
+
"pkg_manager": pkg_manager,
|
|
154
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from .installer import install, install_many
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="peep",
|
|
9
|
+
description="A smarter pip — handles version conflicts, build errors, and platform issues automatically.",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
13
|
+
|
|
14
|
+
# ── peep install ──────────────────────────────────────────────────────────
|
|
15
|
+
install_parser = subparsers.add_parser("install", help="Install one or more packages")
|
|
16
|
+
install_parser.add_argument("packages", nargs="+", help="Package(s) to install, e.g. requests or requests==2.28.0")
|
|
17
|
+
install_parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed output")
|
|
18
|
+
install_parser.add_argument("--force", action="store_true", help="Force reinstall even if already installed")
|
|
19
|
+
|
|
20
|
+
# ── peep check ───────────────────────────────────────────────────────────
|
|
21
|
+
check_parser = subparsers.add_parser("check", help="Check a package for compatibility without installing")
|
|
22
|
+
check_parser.add_argument("package", help="Package to check")
|
|
23
|
+
check_parser.add_argument("-v", "--verbose", action="store_true")
|
|
24
|
+
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
if args.command == "install":
|
|
28
|
+
_handle_install(args)
|
|
29
|
+
elif args.command == "check":
|
|
30
|
+
_handle_check(args)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _handle_install(args):
|
|
34
|
+
# Parse packages — support both `requests` and `requests==2.28.0`
|
|
35
|
+
parsed = [_parse_package_arg(p) for p in args.packages]
|
|
36
|
+
|
|
37
|
+
if len(parsed) == 1:
|
|
38
|
+
name, version = parsed[0]
|
|
39
|
+
_print_status(f"Installing {name}" + (f"=={version}" if version else "") + "...")
|
|
40
|
+
report = install(name, version=version, verbose=args.verbose)
|
|
41
|
+
_print_report(report)
|
|
42
|
+
else:
|
|
43
|
+
_print_status(f"Installing {len(parsed)} packages...")
|
|
44
|
+
reports = install_many([p for p, _ in parsed], verbose=args.verbose)
|
|
45
|
+
for report in reports:
|
|
46
|
+
_print_report(report)
|
|
47
|
+
|
|
48
|
+
# Exit with error code if any install failed
|
|
49
|
+
if isinstance(reports if len(parsed) > 1 else [report], list):
|
|
50
|
+
results = reports if len(parsed) > 1 else [report]
|
|
51
|
+
if any(not r["success"] for r in results):
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _handle_check(args):
|
|
56
|
+
from .environment import check_python_compatibility, check_platform_compatibility
|
|
57
|
+
from .resolver import resolve_conflicts
|
|
58
|
+
|
|
59
|
+
name, version = _parse_package_arg(args.package)
|
|
60
|
+
_print_status(f"Checking {name}" + (f"=={version}" if version else "") + "...")
|
|
61
|
+
|
|
62
|
+
py = check_python_compatibility(name, version)
|
|
63
|
+
plat = check_platform_compatibility(name, version)
|
|
64
|
+
deps = resolve_conflicts(name, version)
|
|
65
|
+
|
|
66
|
+
print()
|
|
67
|
+
_section("Python compatibility")
|
|
68
|
+
if py.get("compatible") is True:
|
|
69
|
+
_ok(f"Compatible with Python {py.get('current_python')}")
|
|
70
|
+
elif py.get("compatible") is False:
|
|
71
|
+
_warn(f"Requires Python {py.get('requires_python')}, you have {py.get('current_python')}")
|
|
72
|
+
if py.get("suggested_version"):
|
|
73
|
+
_info(f"Suggested version: {name}=={py['suggested_version']}")
|
|
74
|
+
else:
|
|
75
|
+
_info("Could not determine Python compatibility")
|
|
76
|
+
|
|
77
|
+
_section("Platform compatibility")
|
|
78
|
+
if plat.get("has_compatible_wheel"):
|
|
79
|
+
_ok("Compatible wheel available for your platform")
|
|
80
|
+
elif plat.get("needs_compilation"):
|
|
81
|
+
_warn("No wheel available — package will be compiled from source")
|
|
82
|
+
elif not plat.get("has_sdist"):
|
|
83
|
+
_err("No compatible distribution found for your platform")
|
|
84
|
+
|
|
85
|
+
_section("Dependency conflicts")
|
|
86
|
+
if deps["clean"]:
|
|
87
|
+
_ok("No conflicts detected")
|
|
88
|
+
else:
|
|
89
|
+
for conflict in deps["conflicts"]:
|
|
90
|
+
_err(f"{conflict['package']} {conflict['installed']} installed, but {conflict['required']} required by {conflict['required_by']}")
|
|
91
|
+
for upgrade in deps["to_upgrade"]:
|
|
92
|
+
_warn(f"{upgrade['package']} will be upgraded {upgrade['from']} → {upgrade['to']}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _print_report(report: dict):
|
|
96
|
+
pkg = report["package"]
|
|
97
|
+
ver = report.get("final_version")
|
|
98
|
+
|
|
99
|
+
print()
|
|
100
|
+
if report["success"]:
|
|
101
|
+
label = f"{pkg}=={ver}" if ver else pkg
|
|
102
|
+
_ok(f"Successfully installed {label}")
|
|
103
|
+
else:
|
|
104
|
+
_err(f"Failed to install {pkg}: {report.get('error', 'Unknown error')}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_package_arg(pkg_str: str) -> tuple[str, str | None]:
|
|
108
|
+
"""Parse 'requests==2.28.0' into ('requests', '2.28.0'), or 'requests' into ('requests', None)."""
|
|
109
|
+
if "==" in pkg_str:
|
|
110
|
+
name, version = pkg_str.split("==", 1)
|
|
111
|
+
return name.strip(), version.strip()
|
|
112
|
+
return pkg_str.strip(), None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── Terminal output helpers ────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
RESET = "\033[0m"
|
|
118
|
+
GREEN = "\033[32m"
|
|
119
|
+
YELLOW = "\033[33m"
|
|
120
|
+
RED = "\033[31m"
|
|
121
|
+
CYAN = "\033[36m"
|
|
122
|
+
BOLD = "\033[1m"
|
|
123
|
+
|
|
124
|
+
def _print_status(msg): print(f"{BOLD}{CYAN}→ {msg}{RESET}")
|
|
125
|
+
def _section(label): print(f"\n{BOLD}{label}{RESET}")
|
|
126
|
+
def _ok(msg): print(f" {GREEN}✔ {msg}{RESET}")
|
|
127
|
+
def _warn(msg): print(f" {YELLOW}⚠ {msg}{RESET}")
|
|
128
|
+
def _err(msg): print(f" {RED}✘ {msg}{RESET}")
|
|
129
|
+
def _info(msg): print(f" {CYAN}ℹ {msg}{RESET}")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def verify_install(package_name: str) -> dict:
|
|
7
|
+
"""
|
|
8
|
+
Verify a package is actually importable after installation.
|
|
9
|
+
Runs in a subprocess to get a clean import environment.
|
|
10
|
+
Returns a result dict with importable, version, and error if any.
|
|
11
|
+
"""
|
|
12
|
+
# Normalize package name to import name
|
|
13
|
+
import_name = _resolve_import_name(package_name)
|
|
14
|
+
|
|
15
|
+
probe = f"""
|
|
16
|
+
import sys
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
mod = __import__("{import_name}")
|
|
21
|
+
version = getattr(mod, "__version__", None)
|
|
22
|
+
print(json.dumps({{"importable": True, "version": version}}))
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
print(json.dumps({{"importable": False, "error": str(e)}}))
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(json.dumps({{"importable": False, "error": f"Unexpected error: {{e}}"}}))
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
[sys.executable, "-c", probe],
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=15,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
38
|
+
return {
|
|
39
|
+
"importable": False,
|
|
40
|
+
"error": result.stderr.strip() or "No output from import probe",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return json.loads(result.stdout.strip())
|
|
44
|
+
|
|
45
|
+
except subprocess.TimeoutExpired:
|
|
46
|
+
return {"importable": False, "error": "Import probe timed out after 15s"}
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
return {"importable": False, "error": f"Could not parse probe output: {result.stdout.strip()}"}
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return {"importable": False, "error": str(e)}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def triage_import_failure(package_name: str) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
When verify_install fails, attempt to diagnose why.
|
|
56
|
+
Returns a diagnosis dict with cause and suggested fix.
|
|
57
|
+
"""
|
|
58
|
+
import_name = _resolve_import_name(package_name)
|
|
59
|
+
|
|
60
|
+
checks = {
|
|
61
|
+
"missing_entirely": _check_missing(package_name),
|
|
62
|
+
"wrong_python": _check_wrong_python(package_name),
|
|
63
|
+
"broken_deps": _check_broken_deps(package_name),
|
|
64
|
+
"c_extension_broken": _check_c_extension(import_name),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Determine the most likely cause
|
|
68
|
+
if checks["missing_entirely"]:
|
|
69
|
+
cause = "Package is not installed at all"
|
|
70
|
+
fix = f"peep install {package_name}"
|
|
71
|
+
elif checks["wrong_python"]:
|
|
72
|
+
cause = "Package was installed for a different Python interpreter"
|
|
73
|
+
fix = f"peep install {package_name} (ensure you're using the right python)"
|
|
74
|
+
elif checks["broken_deps"]:
|
|
75
|
+
cause = f"Missing or broken dependencies: {checks['broken_deps']}"
|
|
76
|
+
fix = f"peep install {package_name} --force"
|
|
77
|
+
elif checks["c_extension_broken"]:
|
|
78
|
+
cause = "C extension failed to load (likely a build or ABI issue)"
|
|
79
|
+
fix = f"peep install {package_name} --force --no-binary"
|
|
80
|
+
else:
|
|
81
|
+
cause = "Unknown import failure"
|
|
82
|
+
fix = f"peep install {package_name} --force"
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"package": package_name,
|
|
86
|
+
"cause": cause,
|
|
87
|
+
"suggested_fix": fix,
|
|
88
|
+
"checks": checks,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_missing(package_name: str) -> bool:
|
|
93
|
+
"""Check if the package shows up in pip list at all."""
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
[sys.executable, "-m", "pip", "show", package_name],
|
|
96
|
+
capture_output=True, text=True
|
|
97
|
+
)
|
|
98
|
+
return result.returncode != 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_wrong_python(package_name: str) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Check if the package is installed but under a different Python.
|
|
104
|
+
Detects the common case of pip vs pip3 confusion.
|
|
105
|
+
"""
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
[sys.executable, "-m", "pip", "show", package_name],
|
|
108
|
+
capture_output=True, text=True
|
|
109
|
+
)
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
# Check if the install location is within the current sys.prefix
|
|
114
|
+
for line in result.stdout.splitlines():
|
|
115
|
+
if line.startswith("Location:"):
|
|
116
|
+
location = line.split(":", 1)[1].strip()
|
|
117
|
+
return sys.prefix.lower() not in location.lower()
|
|
118
|
+
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _check_broken_deps(package_name: str) -> list[str] | None:
|
|
123
|
+
"""Use pip check to find broken dependencies."""
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
[sys.executable, "-m", "pip", "check"],
|
|
126
|
+
capture_output=True, text=True
|
|
127
|
+
)
|
|
128
|
+
if result.returncode == 0:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# Filter output to lines mentioning this package
|
|
132
|
+
broken = [
|
|
133
|
+
line for line in result.stdout.splitlines()
|
|
134
|
+
if package_name.lower() in line.lower()
|
|
135
|
+
]
|
|
136
|
+
return broken if broken else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _check_c_extension(import_name: str) -> bool:
|
|
140
|
+
"""Check if the failure is specifically from a C extension load error."""
|
|
141
|
+
probe = f"""
|
|
142
|
+
try:
|
|
143
|
+
__import__("{import_name}")
|
|
144
|
+
except ImportError as e:
|
|
145
|
+
print("c_ext" if "cannot import" in str(e).lower() or ".so" in str(e) else "other")
|
|
146
|
+
except Exception:
|
|
147
|
+
print("other")
|
|
148
|
+
else:
|
|
149
|
+
print("ok")
|
|
150
|
+
"""
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
[sys.executable, "-c", probe],
|
|
153
|
+
capture_output=True, text=True,
|
|
154
|
+
timeout=10,
|
|
155
|
+
)
|
|
156
|
+
return result.stdout.strip() == "c_ext"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Map of PyPI package names that differ from their import names
|
|
160
|
+
_IMPORT_NAME_MAP = {
|
|
161
|
+
"pillow": "PIL",
|
|
162
|
+
"opencv-python": "cv2",
|
|
163
|
+
"opencv-python-headless": "cv2",
|
|
164
|
+
"scikit-learn": "sklearn",
|
|
165
|
+
"scikit-image": "skimage",
|
|
166
|
+
"beautifulsoup4": "bs4",
|
|
167
|
+
"pyyaml": "yaml",
|
|
168
|
+
"python-dateutil": "dateutil",
|
|
169
|
+
"python-dotenv": "dotenv",
|
|
170
|
+
"pyzmq": "zmq",
|
|
171
|
+
"pygments": "pygments",
|
|
172
|
+
"typing-extensions": "typing_extensions",
|
|
173
|
+
"attrs": "attr",
|
|
174
|
+
"moviepy": "moviepy.editor",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _resolve_import_name(package_name: str) -> str:
|
|
179
|
+
"""Resolve PyPI package name to its actual Python import name."""
|
|
180
|
+
normalized = package_name.lower().strip()
|
|
181
|
+
return _IMPORT_NAME_MAP.get(normalized, normalized.replace("-", "_"))
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import platform
|
|
3
|
+
import sysconfig
|
|
4
|
+
import requests
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
from packaging.specifiers import SpecifierSet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_current_environment() -> dict:
|
|
10
|
+
"""Snapshot of the current Python + platform environment."""
|
|
11
|
+
return {
|
|
12
|
+
"python_version": Version(platform.python_version()),
|
|
13
|
+
"platform_system": platform.system().lower(), # 'linux', 'windows', 'darwin'
|
|
14
|
+
"platform_machine": platform.machine().lower(), # 'x86_64', 'arm64', etc.
|
|
15
|
+
"platform_tags": _get_platform_tags(),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_platform_tags() -> list[str]:
|
|
20
|
+
"""Get the list of compatible wheel platform tags for this system."""
|
|
21
|
+
try:
|
|
22
|
+
from pip._internal.utils.compatibility_tags import get_supported
|
|
23
|
+
return [str(tag) for tag in get_supported()]
|
|
24
|
+
except ImportError:
|
|
25
|
+
# Fallback: construct basic tags manually
|
|
26
|
+
py = f"cp{sys.version_info.major}{sys.version_info.minor}"
|
|
27
|
+
arch = sysconfig.get_platform().replace("-", "_").replace(".", "_")
|
|
28
|
+
return [f"{py}-{py}-{arch}", f"py3-none-any", f"py{sys.version_info.major}-none-any"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_package_metadata(package_name: str, version: str = None) -> dict | None:
|
|
32
|
+
"""Fetch package metadata from PyPI JSON API."""
|
|
33
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
34
|
+
if version:
|
|
35
|
+
url = f"https://pypi.org/pypi/{package_name}/{version}/json"
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
resp = requests.get(url, timeout=10)
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
return resp.json()
|
|
41
|
+
except requests.RequestException:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_python_compatibility(package_name: str, version: str = None) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Check if a package is compatible with the current Python version.
|
|
48
|
+
Returns a result dict with 'compatible', 'requires_python', and optionally 'suggested_version'.
|
|
49
|
+
"""
|
|
50
|
+
env = get_current_environment()
|
|
51
|
+
metadata = fetch_package_metadata(package_name, version)
|
|
52
|
+
|
|
53
|
+
if metadata is None:
|
|
54
|
+
return {"compatible": None, "error": f"Could not fetch metadata for '{package_name}'"}
|
|
55
|
+
|
|
56
|
+
info = metadata["info"]
|
|
57
|
+
requires_python = info.get("requires_python")
|
|
58
|
+
|
|
59
|
+
if not requires_python:
|
|
60
|
+
return {"compatible": True, "requires_python": None}
|
|
61
|
+
|
|
62
|
+
spec = SpecifierSet(requires_python)
|
|
63
|
+
compatible = env["python_version"] in spec
|
|
64
|
+
|
|
65
|
+
result = {
|
|
66
|
+
"compatible": compatible,
|
|
67
|
+
"requires_python": requires_python,
|
|
68
|
+
"current_python": str(env["python_version"]),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if not compatible:
|
|
72
|
+
# Try to find the latest version that does support this Python
|
|
73
|
+
suggested = _find_compatible_version(metadata["releases"], spec, env["python_version"])
|
|
74
|
+
result["suggested_version"] = suggested
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_compatible_version(releases: dict, broken_spec: SpecifierSet, current_python: Version) -> str | None:
|
|
80
|
+
"""
|
|
81
|
+
Walk all available releases and return the latest one compatible
|
|
82
|
+
with the current Python version.
|
|
83
|
+
"""
|
|
84
|
+
candidates = []
|
|
85
|
+
|
|
86
|
+
for ver_str, files in releases.items():
|
|
87
|
+
if not files:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
v = Version(ver_str)
|
|
91
|
+
except Exception:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Check each release file for its requires_python
|
|
95
|
+
for f in files:
|
|
96
|
+
rp = f.get("requires_python")
|
|
97
|
+
if rp is None:
|
|
98
|
+
candidates.append(v)
|
|
99
|
+
break
|
|
100
|
+
try:
|
|
101
|
+
if current_python in SpecifierSet(rp):
|
|
102
|
+
candidates.append(v)
|
|
103
|
+
break
|
|
104
|
+
except Exception:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if not candidates:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
return str(max(candidates))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def check_platform_compatibility(package_name: str, version: str = None) -> dict:
|
|
114
|
+
"""
|
|
115
|
+
Check if any wheel for this package supports the current platform.
|
|
116
|
+
Falls back to sdist detection if no compatible wheel is found.
|
|
117
|
+
"""
|
|
118
|
+
env = get_current_environment()
|
|
119
|
+
metadata = fetch_package_metadata(package_name, version)
|
|
120
|
+
|
|
121
|
+
if metadata is None:
|
|
122
|
+
return {"compatible": None, "error": f"Could not fetch metadata for '{package_name}'"}
|
|
123
|
+
|
|
124
|
+
target_version = version or metadata["info"]["version"]
|
|
125
|
+
release_files = metadata["releases"].get(target_version, [])
|
|
126
|
+
|
|
127
|
+
has_sdist = False
|
|
128
|
+
has_compatible_wheel = False
|
|
129
|
+
has_any_wheel = False
|
|
130
|
+
|
|
131
|
+
for f in release_files:
|
|
132
|
+
if f["packagetype"] == "sdist":
|
|
133
|
+
has_sdist = True
|
|
134
|
+
elif f["packagetype"] == "bdist_wheel":
|
|
135
|
+
has_any_wheel = True
|
|
136
|
+
filename = f["filename"]
|
|
137
|
+
# Check platform tag compatibility
|
|
138
|
+
if _wheel_is_compatible(filename, env["platform_tags"]):
|
|
139
|
+
has_compatible_wheel = True
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"has_compatible_wheel": has_compatible_wheel,
|
|
143
|
+
"has_sdist": has_sdist,
|
|
144
|
+
"has_any_wheel": has_any_wheel,
|
|
145
|
+
"needs_compilation": has_sdist and not has_compatible_wheel,
|
|
146
|
+
"platform": env["platform_system"],
|
|
147
|
+
"machine": env["platform_machine"],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _wheel_is_compatible(filename: str, supported_tags: list[str]) -> bool:
|
|
152
|
+
"""Check if a wheel filename's tags match any of the supported platform tags."""
|
|
153
|
+
# Wheel filename format: {name}-{version}(-{build})?-{python}-{abi}-{platform}.whl
|
|
154
|
+
try:
|
|
155
|
+
base = filename.replace(".whl", "")
|
|
156
|
+
parts = base.split("-")
|
|
157
|
+
if len(parts) < 5:
|
|
158
|
+
return False
|
|
159
|
+
py_tag, abi_tag, plat_tag = parts[-3], parts[-2], parts[-1]
|
|
160
|
+
wheel_tag = f"{py_tag}-{abi_tag}-{plat_tag}"
|
|
161
|
+
|
|
162
|
+
# Pure Python wheels are always compatible
|
|
163
|
+
if "none-any" in wheel_tag:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
return wheel_tag in supported_tags
|
|
167
|
+
except Exception:
|
|
168
|
+
return False
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from .environment import check_python_compatibility, check_platform_compatibility
|
|
4
|
+
from .resolver import resolve_conflicts
|
|
5
|
+
from .build import check_and_fix_build_deps
|
|
6
|
+
from .diagnostics import verify_install
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def install(package: str, version: str = None, verbose: bool = False) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Full peep install pipeline for a single package.
|
|
12
|
+
Runs all checks, auto-fixes what it can, then delegates to pip.
|
|
13
|
+
Returns a full report of what happened.
|
|
14
|
+
"""
|
|
15
|
+
report = {
|
|
16
|
+
"package": package,
|
|
17
|
+
"version": version,
|
|
18
|
+
"steps": {},
|
|
19
|
+
"success": False,
|
|
20
|
+
"final_version": None,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
target = f"{package}=={version}" if version else package
|
|
24
|
+
resolved_version = version # May be updated by compatibility checks
|
|
25
|
+
|
|
26
|
+
# ── Step 1: Python version compatibility ─────────────────────────────────
|
|
27
|
+
py_check = check_python_compatibility(package, version)
|
|
28
|
+
report["steps"]["python_compatibility"] = py_check
|
|
29
|
+
|
|
30
|
+
if py_check.get("compatible") is False:
|
|
31
|
+
suggested = py_check.get("suggested_version")
|
|
32
|
+
if suggested:
|
|
33
|
+
resolved_version = suggested
|
|
34
|
+
target = f"{package}=={suggested}"
|
|
35
|
+
report["steps"]["python_compatibility"]["action"] = f"Switched to {package}=={suggested}"
|
|
36
|
+
else:
|
|
37
|
+
report["error"] = f"{package} has no version compatible with Python {py_check['current_python']}"
|
|
38
|
+
return report
|
|
39
|
+
|
|
40
|
+
# ── Step 2: Platform compatibility ───────────────────────────────────────
|
|
41
|
+
plat_check = check_platform_compatibility(package, resolved_version)
|
|
42
|
+
report["steps"]["platform_compatibility"] = plat_check
|
|
43
|
+
|
|
44
|
+
needs_compilation = plat_check.get("needs_compilation", False)
|
|
45
|
+
|
|
46
|
+
if not plat_check.get("has_compatible_wheel") and not plat_check.get("has_sdist"):
|
|
47
|
+
report["error"] = f"No compatible distribution found for {package} on {plat_check.get('platform')}/{plat_check.get('machine')}"
|
|
48
|
+
return report
|
|
49
|
+
|
|
50
|
+
# ── Step 3: Dependency conflict resolution ────────────────────────────────
|
|
51
|
+
conflict_check = resolve_conflicts(package, resolved_version)
|
|
52
|
+
report["steps"]["dependency_resolution"] = conflict_check
|
|
53
|
+
|
|
54
|
+
if not conflict_check["clean"]:
|
|
55
|
+
report["error"] = f"Unresolvable dependency conflicts for {package}: {conflict_check['conflicts']}"
|
|
56
|
+
return report
|
|
57
|
+
|
|
58
|
+
# Silently pre-install or upgrade conflicting deps before the main package
|
|
59
|
+
for dep in conflict_check["to_install"] + conflict_check["to_upgrade"]:
|
|
60
|
+
dep_target = f"{dep['package']}=={dep.get('version') or dep.get('to')}" if dep.get("version") or dep.get("to") else dep["package"]
|
|
61
|
+
_run_pip(["install", dep_target], verbose=verbose)
|
|
62
|
+
|
|
63
|
+
# ── Step 4: Build dependency check + fix ─────────────────────────────────
|
|
64
|
+
build_check = check_and_fix_build_deps(package, needs_compilation)
|
|
65
|
+
report["steps"]["build_deps"] = build_check
|
|
66
|
+
|
|
67
|
+
if build_check.get("failed"):
|
|
68
|
+
hints = [f["hint"] for f in build_check["failed"] if f.get("hint")]
|
|
69
|
+
report["error"] = (
|
|
70
|
+
f"Could not auto-install build dependencies: "
|
|
71
|
+
f"{[f['dep'] for f in build_check['failed']]}. "
|
|
72
|
+
f"Manual steps: {hints}"
|
|
73
|
+
)
|
|
74
|
+
return report
|
|
75
|
+
|
|
76
|
+
# ── Step 5: Run pip ───────────────────────────────────────────────────────
|
|
77
|
+
pip_result = _run_pip(["install", target], verbose=verbose)
|
|
78
|
+
report["steps"]["pip"] = pip_result
|
|
79
|
+
|
|
80
|
+
if pip_result["returncode"] != 0:
|
|
81
|
+
# One retry: let pip attempt its own conflict resolution
|
|
82
|
+
retry_result = _run_pip(["install", target, "--upgrade"], verbose=verbose)
|
|
83
|
+
report["steps"]["pip_retry"] = retry_result
|
|
84
|
+
|
|
85
|
+
if retry_result["returncode"] != 0:
|
|
86
|
+
report["error"] = f"pip failed:\n{retry_result['stderr']}"
|
|
87
|
+
return report
|
|
88
|
+
|
|
89
|
+
# ── Step 6: Post-install verification ─────────────────────────────────────
|
|
90
|
+
verify = verify_install(package)
|
|
91
|
+
report["steps"]["verification"] = verify
|
|
92
|
+
|
|
93
|
+
if not verify["importable"]:
|
|
94
|
+
report["error"] = f"Package installed but failed to import: {verify.get('error')}"
|
|
95
|
+
return report
|
|
96
|
+
|
|
97
|
+
report["success"] = True
|
|
98
|
+
report["final_version"] = verify.get("version") or resolved_version
|
|
99
|
+
return report
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def install_many(packages: list[str], verbose: bool = False) -> list[dict]:
|
|
103
|
+
"""Install multiple packages, collecting a report for each."""
|
|
104
|
+
return [install(pkg, verbose=verbose) for pkg in packages]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_pip(args: list[str], verbose: bool = False) -> dict:
|
|
108
|
+
"""Shell out to pip with the given args. Returns returncode, stdout, stderr."""
|
|
109
|
+
cmd = [sys.executable, "-m", "pip"] + args
|
|
110
|
+
|
|
111
|
+
if not verbose:
|
|
112
|
+
args_with_quiet = cmd + ["-q"]
|
|
113
|
+
else:
|
|
114
|
+
args_with_quiet = cmd
|
|
115
|
+
|
|
116
|
+
result = subprocess.run(args_with_quiet, capture_output=True, text=True)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"command": " ".join(args_with_quiet),
|
|
120
|
+
"returncode": result.returncode,
|
|
121
|
+
"stdout": result.stdout.strip(),
|
|
122
|
+
"stderr": result.stderr.strip(),
|
|
123
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
from packaging.version import Version
|
|
4
|
+
from packaging.specifiers import SpecifierSet
|
|
5
|
+
from packaging.requirements import Requirement
|
|
6
|
+
from .environment import fetch_package_metadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_installed_packages() -> dict[str, Version]:
|
|
10
|
+
"""Return a dict of {package_name: installed_version} for the current environment."""
|
|
11
|
+
try:
|
|
12
|
+
result = subprocess.run(
|
|
13
|
+
[sys.executable, "-m", "pip", "list", "--format=freeze"],
|
|
14
|
+
capture_output=True, text=True
|
|
15
|
+
)
|
|
16
|
+
packages = {}
|
|
17
|
+
for line in result.stdout.splitlines():
|
|
18
|
+
if "==" in line:
|
|
19
|
+
name, ver = line.split("==", 1)
|
|
20
|
+
try:
|
|
21
|
+
packages[name.lower().strip()] = Version(ver.strip())
|
|
22
|
+
except Exception:
|
|
23
|
+
continue
|
|
24
|
+
return packages
|
|
25
|
+
except Exception:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fetch_dependencies(package_name: str, version: str = None) -> list[Requirement]:
|
|
30
|
+
"""Fetch the dependency list for a package from PyPI metadata."""
|
|
31
|
+
metadata = fetch_package_metadata(package_name, version)
|
|
32
|
+
if not metadata:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
requires_dist = metadata["info"].get("requires_dist") or []
|
|
36
|
+
deps = []
|
|
37
|
+
|
|
38
|
+
for dep_str in requires_dist:
|
|
39
|
+
try:
|
|
40
|
+
req = Requirement(dep_str)
|
|
41
|
+
# Skip extras/optional dependencies (e.g. dev, test, docs)
|
|
42
|
+
if not req.marker or req.marker.evaluate({"extra": ""}):
|
|
43
|
+
deps.append(req)
|
|
44
|
+
except Exception:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
return deps
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_all_versions(package_name: str) -> list[Version]:
|
|
51
|
+
"""Fetch all available versions of a package from PyPI."""
|
|
52
|
+
metadata = fetch_package_metadata(package_name)
|
|
53
|
+
if not metadata:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
versions = []
|
|
57
|
+
for ver_str in metadata["releases"].keys():
|
|
58
|
+
try:
|
|
59
|
+
versions.append(Version(ver_str))
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
return sorted(versions, reverse=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_conflicts(package_name: str, requested_version: str = None) -> dict:
|
|
67
|
+
"""
|
|
68
|
+
Check the full dependency tree of a package against the current environment.
|
|
69
|
+
Returns a resolution plan: what to install, what to upgrade, what conflicts exist.
|
|
70
|
+
"""
|
|
71
|
+
installed = get_installed_packages()
|
|
72
|
+
deps = fetch_dependencies(package_name, requested_version)
|
|
73
|
+
|
|
74
|
+
to_install = []
|
|
75
|
+
to_upgrade = []
|
|
76
|
+
conflicts = []
|
|
77
|
+
|
|
78
|
+
for req in deps:
|
|
79
|
+
name = req.name.lower()
|
|
80
|
+
specifier = req.specifier # e.g. SpecifierSet(">=1.0,<2.0")
|
|
81
|
+
|
|
82
|
+
if name not in installed:
|
|
83
|
+
# Not installed at all — find best version to install
|
|
84
|
+
best = _best_version(name, specifier)
|
|
85
|
+
to_install.append({"package": req.name, "version": str(best) if best else None, "reason": "not installed"})
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
current = installed[name]
|
|
89
|
+
if current in specifier:
|
|
90
|
+
# Already installed and compatible — nothing to do
|
|
91
|
+
continue
|
|
92
|
+
else:
|
|
93
|
+
# Installed but wrong version
|
|
94
|
+
best = _best_version(name, specifier)
|
|
95
|
+
if best:
|
|
96
|
+
to_upgrade.append({
|
|
97
|
+
"package": req.name,
|
|
98
|
+
"from": str(current),
|
|
99
|
+
"to": str(best),
|
|
100
|
+
"required_by": package_name,
|
|
101
|
+
"specifier": str(specifier),
|
|
102
|
+
})
|
|
103
|
+
else:
|
|
104
|
+
conflicts.append({
|
|
105
|
+
"package": req.name,
|
|
106
|
+
"installed": str(current),
|
|
107
|
+
"required": str(specifier),
|
|
108
|
+
"required_by": package_name,
|
|
109
|
+
"resolvable": False,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"package": package_name,
|
|
114
|
+
"version": requested_version,
|
|
115
|
+
"to_install": to_install,
|
|
116
|
+
"to_upgrade": to_upgrade,
|
|
117
|
+
"conflicts": conflicts,
|
|
118
|
+
"clean": len(conflicts) == 0,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _best_version(package_name: str, specifier: SpecifierSet) -> Version | None:
|
|
123
|
+
"""Find the latest version of a package that satisfies the given specifier."""
|
|
124
|
+
versions = get_all_versions(package_name)
|
|
125
|
+
for v in versions:
|
|
126
|
+
# Skip pre-releases unless specifier explicitly allows them
|
|
127
|
+
if v.is_prerelease:
|
|
128
|
+
continue
|
|
129
|
+
if v in specifier:
|
|
130
|
+
return v
|
|
131
|
+
return None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
peep/__init__.py
|
|
3
|
+
peep/build.py
|
|
4
|
+
peep/cli.py
|
|
5
|
+
peep/diagnostics.py
|
|
6
|
+
peep/environment.py
|
|
7
|
+
peep/installer.py
|
|
8
|
+
peep/resolver.py
|
|
9
|
+
peep_install.egg-info/PKG-INFO
|
|
10
|
+
peep_install.egg-info/SOURCES.txt
|
|
11
|
+
peep_install.egg-info/dependency_links.txt
|
|
12
|
+
peep_install.egg-info/entry_points.txt
|
|
13
|
+
peep_install.egg-info/requires.txt
|
|
14
|
+
peep_install.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
peep
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "peep-install"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A smarter pip that auto-resolves version conflicts, build errors, and platform issues"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"requests>=2.28.0",
|
|
12
|
+
"packaging>=23.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
peep = "peep.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["."]
|
|
20
|
+
include = ["peep*"]
|