Fix upgrading and installing pip packages
This commit is contained in:
parent
78e09e03cf
commit
ca4e3dfd79
4 changed files with 105 additions and 20 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -98,6 +98,7 @@
|
|||
"tmpfilename",
|
||||
"ungroup",
|
||||
"unnegated",
|
||||
"Unraid",
|
||||
"upgrader",
|
||||
"urandom",
|
||||
"urlsafe",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if base_dir := os.environ.get("YTP_CONFIG_PATH"):
|
||||
base_dir = Path(base_dir)
|
||||
user_site = base_dir / f"python{sys.version_info.major}.{sys.version_info.minor}-packages"
|
||||
|
||||
if not user_site.exists():
|
||||
user_site.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if user_site.is_dir() and str(user_site) not in sys.path:
|
||||
sys.path.insert(0, str(user_site))
|
||||
|
|
@ -1,13 +1,20 @@
|
|||
import importlib
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
LOG = logging.getLogger("package_installer")
|
||||
|
||||
|
||||
def parse_version(v: str) -> tuple[int, ...]:
|
||||
return tuple(int("".join(filter(str.isdigit, part)) or "0") for part in v.strip().split("."))
|
||||
|
||||
|
||||
class Packages:
|
||||
def __init__(self, env: str | None, file: str | None, upgrade: bool = False):
|
||||
from_env = env.split() if env else []
|
||||
|
|
@ -30,30 +37,68 @@ class Packages:
|
|||
|
||||
|
||||
class PackageInstaller:
|
||||
"""
|
||||
This class is responsible for installing and upgrading pip packages.
|
||||
"""
|
||||
user_site: Path | None = None
|
||||
|
||||
def __init__(self):
|
||||
if base_dir := os.environ.get("YTP_CONFIG_PATH"):
|
||||
base_dir = Path(base_dir)
|
||||
self.user_site = base_dir / f"python{sys.version_info.major}.{sys.version_info.minor}-packages"
|
||||
self.user_site.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if str(self.user_site) not in sys.path:
|
||||
sys.path.insert(0, str(self.user_site))
|
||||
|
||||
def action(self, pkg: str, upgrade: bool = False):
|
||||
try:
|
||||
importlib.import_module(pkg)
|
||||
if upgrade is False:
|
||||
LOG.info(f"'{pkg}' is already installed. Skipping upgrades. as requested.")
|
||||
current_version = self._get_installed_version(pkg)
|
||||
|
||||
if upgrade and current_version:
|
||||
latest_version = self._get_latest_version(pkg)
|
||||
if latest_version and parse_version(current_version) >= parse_version(latest_version):
|
||||
LOG.info(f"'{pkg}' is already the latest version ({current_version}). Skipping upgrade.")
|
||||
return
|
||||
|
||||
LOG.info(f"'{pkg}' is already installed. Checking for upgrades...")
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", pkg],
|
||||
check=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
|
||||
)
|
||||
except ImportError:
|
||||
if current_version:
|
||||
LOG.info(f"'{pkg}' is already installed (version: {current_version}). Proceeding with upgrade...")
|
||||
else:
|
||||
LOG.info(f"'{pkg}' is not installed. Installing...")
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", pkg],
|
||||
check=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
|
||||
)
|
||||
|
||||
self._install_pkg(pkg)
|
||||
|
||||
def _get_installed_version(self, pkg: str) -> str | None:
|
||||
try:
|
||||
return importlib.metadata.version(pkg)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return None
|
||||
|
||||
def _get_latest_version(self, pkg: str) -> str | None:
|
||||
url = f"https://pypi.org/pypi/{pkg}/json"
|
||||
try:
|
||||
with httpx.Client(timeout=5.0) as client:
|
||||
resp = client.get(url)
|
||||
if 200 == resp.status_code:
|
||||
return resp.json()["info"]["version"]
|
||||
LOG.warning(f"Failed to fetch '{pkg}' info: HTTP {resp.status_code}")
|
||||
except Exception as e:
|
||||
LOG.warning(f"Error while querying PyPI for '{pkg}': {e}")
|
||||
return None
|
||||
|
||||
def _install_pkg(self, pkg: str):
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"--disable-pip-version-check",
|
||||
"--target",
|
||||
str(self.user_site),
|
||||
"--no-warn-script-location",
|
||||
pkg,
|
||||
],
|
||||
check=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
|
||||
)
|
||||
|
||||
def check(self, pkgs: Packages):
|
||||
"""
|
||||
|
|
@ -63,7 +108,7 @@ class PackageInstaller:
|
|||
pkgs (GetPackages): the class with packages and their settings.
|
||||
|
||||
"""
|
||||
if not pkgs.has_packages():
|
||||
if not pkgs.has_packages() or not self.user_site:
|
||||
return
|
||||
|
||||
LOG.info(f"Checking for user pip packages: {', '.join(pkgs.packages)}")
|
||||
|
|
|
|||
|
|
@ -40,3 +40,29 @@ async def debug_asyncio(config: Config, encoder: Encoder) -> Response:
|
|||
dumps=encoder.encode,
|
||||
)
|
||||
|
||||
|
||||
@route("GET", "api/dev/pip", "check_pip_packages")
|
||||
async def check_pip_packages(config: Config, encoder: Encoder) -> Response:
|
||||
pkgs = config.pip_packages.split(" ") if config.pip_packages else []
|
||||
if not pkgs:
|
||||
return web.json_response(
|
||||
data={"message": "No pip packages configured."},
|
||||
status=web.HTTPOk.status_code,
|
||||
)
|
||||
|
||||
def _get_installed_version(pkg: str) -> str | None:
|
||||
try:
|
||||
import importlib.metadata
|
||||
|
||||
return importlib.metadata.version(pkg)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return None
|
||||
|
||||
response = {}
|
||||
for pkg in pkgs:
|
||||
if not (pkg := pkg.strip()):
|
||||
continue
|
||||
|
||||
response.update({pkg: _get_installed_version(pkg)})
|
||||
|
||||
return web.json_response(data=response, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
|
|
|||
Loading…
Reference in a new issue