240 lines
7.5 KiB
Python
240 lines
7.5 KiB
Python
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
from typing import Any, Dict, Optional
|
|
import tomlkit
|
|
import typer
|
|
|
|
from rich.console import Console
|
|
from safety.console import main_console
|
|
from safety.tool.auth import build_index_url
|
|
from safety.tool.constants import (
|
|
PYPI_ORGANIZATION_REPOSITORY_URL,
|
|
PYPI_PUBLIC_REPOSITORY_URL,
|
|
PYPI_PROJECT_REPOSITORY_URL,
|
|
)
|
|
from safety.utils.pyapp_utils import get_path
|
|
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
else:
|
|
import tomli as tomllib
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def backup_file(path: Path) -> None:
|
|
"""
|
|
Create backup of file if it exists
|
|
"""
|
|
if path.exists():
|
|
backup_path = path.with_name(f"{path.name}.backup")
|
|
shutil.copy2(path, backup_path)
|
|
|
|
|
|
class Uv:
|
|
@classmethod
|
|
def is_installed(cls) -> bool:
|
|
"""
|
|
Checks if the UV program is installed
|
|
|
|
Returns:
|
|
True if UV is installed on system, or false otherwise
|
|
"""
|
|
return shutil.which("uv", path=get_path()) is not None
|
|
|
|
@classmethod
|
|
def is_uv_project_file(cls, file: Path) -> bool:
|
|
try:
|
|
cfg = tomllib.loads(file.read_text())
|
|
return (
|
|
cfg.get("tool", {}).get("uv") is not None
|
|
or (file.parent / "uv.lock").exists()
|
|
)
|
|
except (IOError, ValueError):
|
|
return False
|
|
|
|
@classmethod
|
|
def configure_pyproject(
|
|
cls,
|
|
file: Path,
|
|
org_slug: Optional[str],
|
|
project_id: Optional[str] = None,
|
|
console: Console = main_console,
|
|
) -> Optional[Path]:
|
|
"""
|
|
Configures index url for specified pyproject.toml file.
|
|
|
|
Args:
|
|
file (Path): Path to pyproject.toml file.
|
|
org_slug (Optional[str]): Organization slug.
|
|
project_id (Optional[str]): Project ID.
|
|
console (Console): Console instance.
|
|
"""
|
|
if not cls.is_installed():
|
|
logger.error("UV is not installed.")
|
|
return None
|
|
|
|
repository_url = (
|
|
PYPI_PROJECT_REPOSITORY_URL.format(org_slug, project_id)
|
|
if project_id and org_slug
|
|
else (
|
|
PYPI_ORGANIZATION_REPOSITORY_URL.format(org_slug)
|
|
if org_slug
|
|
else PYPI_PUBLIC_REPOSITORY_URL
|
|
)
|
|
)
|
|
try:
|
|
content = file.read_text()
|
|
doc: Dict[str, Any] = tomlkit.loads(content)
|
|
|
|
if "tool" not in doc:
|
|
doc["tool"] = tomlkit.table()
|
|
if "uv" not in doc["tool"]: # type: ignore
|
|
doc["tool"]["uv"] = tomlkit.table() # type: ignore
|
|
if "index" not in doc["tool"]["uv"]: # type: ignore
|
|
doc["tool"]["uv"]["index"] = tomlkit.aot() # type: ignore
|
|
|
|
index_container = doc["tool"]["uv"] # type: ignore
|
|
cls.filter_out_safety_index(index_container)
|
|
|
|
safety_index = {
|
|
"name": "safety",
|
|
"url": repository_url,
|
|
# In UV default:
|
|
# True = lowest priority
|
|
# False = highest priority
|
|
"default": False,
|
|
}
|
|
non_safety_indexes = (
|
|
doc.get("tool", {}).get("uv", {}).get("index", tomlkit.aot())
|
|
)
|
|
|
|
# Add safety index as first priority
|
|
index_container["index"] = tomlkit.aot() # type: ignore
|
|
index_container["index"].append(safety_index) # type: ignore
|
|
index_container["index"].extend(non_safety_indexes) # type: ignore
|
|
|
|
# Write back to file
|
|
file.write_text(tomlkit.dumps(doc))
|
|
return file
|
|
|
|
except (IOError, ValueError, Exception) as e:
|
|
logger.error(f"Failed to configure {file} file: {e}")
|
|
|
|
return None
|
|
|
|
@classmethod
|
|
def get_user_config_path(cls) -> Path:
|
|
"""
|
|
Returns the path to the user config file for UV.
|
|
|
|
This logic is based on the uv documentation:
|
|
https://docs.astral.sh/uv/configuration/files/
|
|
|
|
"uv will also discover user-level configuration at
|
|
~/.config/uv/uv.toml (or $XDG_CONFIG_HOME/uv/uv.toml) on macOS and Linux,
|
|
or %APPDATA%\\uv\\uv.toml on Windows; ..."
|
|
|
|
Returns:
|
|
Path: The path to the user config file.
|
|
"""
|
|
if sys.platform == "win32":
|
|
return Path(os.environ.get("APPDATA", ""), "uv", "uv.toml")
|
|
else:
|
|
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
if xdg_config_home:
|
|
return Path(xdg_config_home, "uv", "uv.toml")
|
|
else:
|
|
return Path(Path.home(), ".config", "uv", "uv.toml")
|
|
|
|
@classmethod
|
|
def filter_out_safety_index(cls, index_container: Any):
|
|
if "index" not in index_container:
|
|
return
|
|
|
|
indexes = list(index_container["index"])
|
|
index_container["index"] = tomlkit.aot()
|
|
|
|
for index in indexes:
|
|
index_url = index.get("url", "")
|
|
|
|
if ".safetycli.com" in index_url:
|
|
continue
|
|
|
|
index_container["index"].append(index)
|
|
|
|
@classmethod
|
|
def configure_system(
|
|
cls, org_slug: Optional[str], console: Console = main_console
|
|
) -> Optional[Path]:
|
|
"""
|
|
Configures UV system to use to Safety index url.
|
|
"""
|
|
try:
|
|
repository_url = (
|
|
PYPI_ORGANIZATION_REPOSITORY_URL.format(org_slug)
|
|
if org_slug
|
|
else PYPI_PUBLIC_REPOSITORY_URL
|
|
)
|
|
|
|
user_config_path = cls.get_user_config_path()
|
|
|
|
if not user_config_path.exists():
|
|
user_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
content = ""
|
|
else:
|
|
backup_file(user_config_path)
|
|
content = user_config_path.read_text()
|
|
|
|
doc = tomlkit.loads(content)
|
|
if "index" not in doc:
|
|
doc["index"] = tomlkit.aot()
|
|
cls.filter_out_safety_index(index_container=doc)
|
|
|
|
safety_index = tomlkit.aot()
|
|
safety_index.append(
|
|
{
|
|
"name": "safety",
|
|
"url": repository_url,
|
|
# In UV default:
|
|
# True = lowest priority
|
|
# False = highest priority
|
|
"default": False,
|
|
}
|
|
)
|
|
|
|
non_safety_indexes = doc.get("index", tomlkit.aot())
|
|
|
|
# Add safety index as first priority
|
|
doc["index"] = tomlkit.aot()
|
|
doc.append("index", safety_index)
|
|
doc.append("index", non_safety_indexes)
|
|
|
|
user_config_path.write_text(tomlkit.dumps(doc))
|
|
return user_config_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to configure UV system: {e}")
|
|
return None
|
|
|
|
@classmethod
|
|
def reset_system(cls, console: Console = main_console):
|
|
try:
|
|
user_config_path = cls.get_user_config_path()
|
|
if user_config_path.exists():
|
|
backup_file(user_config_path)
|
|
content = user_config_path.read_text()
|
|
doc = tomlkit.loads(content)
|
|
cls.filter_out_safety_index(index_container=doc)
|
|
user_config_path.write_text(tomlkit.dumps(doc))
|
|
except Exception as e:
|
|
msg = "Failed to reset UV global settings"
|
|
logger.error(f"{msg}: {e}")
|
|
|
|
@classmethod
|
|
def build_index_url(cls, ctx: typer.Context, index_url: Optional[str]) -> str:
|
|
return build_index_url(ctx, index_url, "pypi")
|