Add YAPF style + ignore, and format tracked Python files
This commit is contained in:
@@ -112,7 +112,10 @@ def run_platform_bootstrap(repo_root: Path) -> int:
|
||||
print("Running platform bootstrap script:", " ".join(cmd))
|
||||
rc = subprocess.run(cmd, cwd=str(repo_root))
|
||||
if rc.returncode != 0:
|
||||
print(f"Bootstrap script failed with exit code {rc.returncode}", file=sys.stderr)
|
||||
print(
|
||||
f"Bootstrap script failed with exit code {rc.returncode}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return int(rc.returncode or 0)
|
||||
|
||||
|
||||
@@ -143,7 +146,9 @@ def _build_playwright_install_cmd(browsers: str | None) -> list[str]:
|
||||
if "all" in items:
|
||||
return base
|
||||
|
||||
allowed = {"chromium", "firefox", "webkit"}
|
||||
allowed = {"chromium",
|
||||
"firefox",
|
||||
"webkit"}
|
||||
invalid = [b for b in items if b not in allowed]
|
||||
if invalid:
|
||||
raise ValueError(
|
||||
@@ -172,7 +177,16 @@ def _install_deno(version: str | None = None) -> int:
|
||||
ps_cmd = f"iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno -Version {ver}"
|
||||
else:
|
||||
ps_cmd = "iwr https://deno.land/x/install/install.ps1 -useb | iex"
|
||||
run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd])
|
||||
run(
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
ps_cmd
|
||||
]
|
||||
)
|
||||
else:
|
||||
# POSIX: use curl + sh installer
|
||||
if version:
|
||||
@@ -220,7 +234,8 @@ def main() -> int:
|
||||
"--browsers",
|
||||
type=str,
|
||||
default="chromium",
|
||||
help="Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)",
|
||||
help=
|
||||
"Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-editable",
|
||||
@@ -234,7 +249,9 @@ def main() -> int:
|
||||
help="Install the Deno runtime (default behavior; kept for explicitness)",
|
||||
)
|
||||
deno_group.add_argument(
|
||||
"--no-deno", action="store_true", help="Skip installing Deno runtime (opt out)"
|
||||
"--no-deno",
|
||||
action="store_true",
|
||||
help="Skip installing Deno runtime (opt out)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deno-version",
|
||||
@@ -312,7 +329,9 @@ def main() -> int:
|
||||
print("'playwright' package not found; installing it via pip...")
|
||||
run([sys.executable, "-m", "pip", "install", "playwright"])
|
||||
|
||||
print("Installing Playwright browsers (this may download several hundred MB)...")
|
||||
print(
|
||||
"Installing Playwright browsers (this may download several hundred MB)..."
|
||||
)
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
except ValueError as exc:
|
||||
@@ -346,7 +365,9 @@ def main() -> int:
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print(f"Installing Python dependencies into local venv from {req_file}...")
|
||||
print(
|
||||
f"Installing Python dependencies into local venv from {req_file}..."
|
||||
)
|
||||
run([str(venv_python), "-m", "pip", "install", "-r", str(req_file)])
|
||||
|
||||
if not args.no_playwright:
|
||||
@@ -354,7 +375,9 @@ def main() -> int:
|
||||
print("'playwright' package not installed in venv; installing it...")
|
||||
run([str(venv_python), "-m", "pip", "install", "playwright"])
|
||||
|
||||
print("Installing Playwright browsers (this may download several hundred MB)...")
|
||||
print(
|
||||
"Installing Playwright browsers (this may download several hundred MB)..."
|
||||
)
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
except ValueError as exc:
|
||||
@@ -373,7 +396,11 @@ def main() -> int:
|
||||
print("Verifying top-level 'CLI' import in venv...")
|
||||
try:
|
||||
rc = subprocess.run(
|
||||
[str(venv_python), "-c", "import importlib; importlib.import_module('CLI')"],
|
||||
[
|
||||
str(venv_python),
|
||||
"-c",
|
||||
"import importlib; importlib.import_module('CLI')"
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
if rc.returncode == 0:
|
||||
@@ -418,7 +445,9 @@ def main() -> int:
|
||||
else:
|
||||
with pth_file.open("w", encoding="utf-8") as fh:
|
||||
fh.write(str(repo_root) + "\n")
|
||||
print(f"Wrote .pth adding repo root to venv site-packages: {pth_file}")
|
||||
print(
|
||||
f"Wrote .pth adding repo root to venv site-packages: {pth_file}"
|
||||
)
|
||||
|
||||
# Re-check whether CLI can be imported now
|
||||
rc2 = subprocess.run(
|
||||
@@ -436,7 +465,9 @@ def main() -> int:
|
||||
"Adding .pth did not make top-level 'CLI' importable; consider creating an egg-link or checking the venv."
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"Warning: failed to verify or modify site-packages for top-level CLI: {exc}")
|
||||
print(
|
||||
f"Warning: failed to verify or modify site-packages for top-level CLI: {exc}"
|
||||
)
|
||||
|
||||
# Optional: install Deno runtime (default: install unless --no-deno is passed)
|
||||
install_deno_requested = True
|
||||
@@ -572,9 +603,14 @@ python -m medeia_macina.cli_entry @args
|
||||
"$bin = '{bin}';"
|
||||
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
|
||||
"if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}"
|
||||
).format(bin=str_bin.replace("\\", "\\\\"))
|
||||
).format(bin=str_bin.replace("\\",
|
||||
"\\\\"))
|
||||
subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", ps_cmd], check=False
|
||||
["powershell",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
ps_cmd],
|
||||
check=False
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -583,7 +619,10 @@ python -m medeia_macina.cli_entry @args
|
||||
|
||||
else:
|
||||
# POSIX
|
||||
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(home / ".local/bin")))
|
||||
user_bin = Path(
|
||||
os.environ.get("XDG_BIN_HOME",
|
||||
str(home / ".local/bin"))
|
||||
)
|
||||
user_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mm_sh = user_bin / "mm"
|
||||
@@ -688,7 +727,10 @@ python -m medeia_macina.cli_entry @args
|
||||
return 0
|
||||
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Error: command failed with exit {exc.returncode}: {exc}", file=sys.stderr)
|
||||
print(
|
||||
f"Error: command failed with exit {exc.returncode}: {exc}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return int(exc.returncode or 1)
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
print(f"Unexpected error: {exc}", file=sys.stderr)
|
||||
|
||||
26
scripts/format_tracked.py
Normal file
26
scripts/format_tracked.py
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Format all tracked Python files using the repo's YAPF style.
|
||||
|
||||
This script intentionally formats only files tracked by git to avoid touching
|
||||
files that are ignored (e.g., venv, site-packages).
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
out = subprocess.check_output(["git", "ls-files", "*.py"]).decode("utf-8")
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print("Failed to get tracked files from git:", exc, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
files = [f for f in (line.strip() for line in out.splitlines()) if f]
|
||||
print(f"Formatting {len(files)} tracked python files...")
|
||||
if not files:
|
||||
print("No tracked python files found.")
|
||||
sys.exit(0)
|
||||
|
||||
for path in files:
|
||||
print(path)
|
||||
subprocess.run([sys.executable, "-m", "yapf", "-i", "--style", ".style.yapf", path], check=False)
|
||||
|
||||
print("Done")
|
||||
@@ -50,7 +50,11 @@ except Exception:
|
||||
def detach_kwargs_for_platform():
|
||||
kwargs = {}
|
||||
if os.name == "nt":
|
||||
CREATE_NEW_PROCESS_GROUP = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
||||
CREATE_NEW_PROCESS_GROUP = getattr(
|
||||
subprocess,
|
||||
"CREATE_NEW_PROCESS_GROUP",
|
||||
0
|
||||
)
|
||||
DETACHED_PROCESS = getattr(subprocess, "DETACHED_PROCESS", 0)
|
||||
flags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||
if flags:
|
||||
@@ -70,7 +74,11 @@ def find_git_executable() -> Optional[str]:
|
||||
# Quick sanity check
|
||||
try:
|
||||
subprocess.run(
|
||||
[git, "--version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
[git,
|
||||
"--version"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
return git
|
||||
except Exception:
|
||||
@@ -88,7 +96,11 @@ def is_git_repo(path: Path) -> bool:
|
||||
return False
|
||||
try:
|
||||
subprocess.run(
|
||||
[git, "-C", str(path), "rev-parse", "--is-inside-work-tree"],
|
||||
[git,
|
||||
"-C",
|
||||
str(path),
|
||||
"rev-parse",
|
||||
"--is-inside-work-tree"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
@@ -99,7 +111,11 @@ def is_git_repo(path: Path) -> bool:
|
||||
|
||||
|
||||
def run_git_clone(
|
||||
git: str, repo: str, dest: Path, branch: Optional[str] = None, depth: Optional[int] = None
|
||||
git: str,
|
||||
repo: str,
|
||||
dest: Path,
|
||||
branch: Optional[str] = None,
|
||||
depth: Optional[int] = None
|
||||
) -> None:
|
||||
# Build git clone with options before the repository argument. Support shallow clones
|
||||
# via --depth when requested.
|
||||
@@ -113,7 +129,12 @@ def run_git_clone(
|
||||
if branch:
|
||||
cmd += ["--branch", branch]
|
||||
cmd += [repo, str(dest)]
|
||||
logging.info("Cloning: %s -> %s (depth=%s)", repo, dest, str(depth) if depth else "full")
|
||||
logging.info(
|
||||
"Cloning: %s -> %s (depth=%s)",
|
||||
repo,
|
||||
dest,
|
||||
str(depth) if depth else "full"
|
||||
)
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
@@ -123,7 +144,11 @@ def run_git_pull(git: str, dest: Path) -> None:
|
||||
|
||||
|
||||
def download_and_extract_zip(
|
||||
repo_url: str, dest: Path, branch_candidates: Tuple[str, ...] = ("main", "master")
|
||||
repo_url: str,
|
||||
dest: Path,
|
||||
branch_candidates: Tuple[str,
|
||||
...] = ("main",
|
||||
"master")
|
||||
) -> None:
|
||||
"""Download the GitHub repo zip and extract it into dest.
|
||||
|
||||
@@ -181,7 +206,10 @@ def download_and_extract_zip(
|
||||
target.unlink()
|
||||
shutil.move(str(entry), str(dest))
|
||||
logging.info(
|
||||
"Downloaded and extracted %s (branch: %s) into %s", repo_url, branch, dest
|
||||
"Downloaded and extracted %s (branch: %s) into %s",
|
||||
repo_url,
|
||||
branch,
|
||||
dest
|
||||
)
|
||||
return
|
||||
except Exception as exc:
|
||||
@@ -277,9 +305,9 @@ def maybe_reexec_under_project_venv(root: Path, disable: bool = False) -> None:
|
||||
script_path = Path(sys.argv[0]).resolve()
|
||||
except Exception:
|
||||
script_path = None
|
||||
args = [str(py), str(script_path) if script_path is not None else sys.argv[0]] + sys.argv[
|
||||
1:
|
||||
]
|
||||
args = [str(py),
|
||||
str(script_path) if script_path is not None else sys.argv[0]
|
||||
] + sys.argv[1:]
|
||||
logging.debug("Exec args: %s", args)
|
||||
os.execvpe(str(py), args, env)
|
||||
except Exception as exc:
|
||||
@@ -325,12 +353,21 @@ def fix_permissions_windows(path: Path, user: Optional[str] = None) -> bool:
|
||||
except Exception:
|
||||
user = getpass.getuser()
|
||||
|
||||
logging.info("Attempting Windows ownership/ACL fix for %s (owner=%s)", path, user)
|
||||
logging.info(
|
||||
"Attempting Windows ownership/ACL fix for %s (owner=%s)",
|
||||
path,
|
||||
user
|
||||
)
|
||||
|
||||
# Try to take ownership (best-effort)
|
||||
try:
|
||||
subprocess.run(
|
||||
["takeown", "/F", str(path), "/R", "/D", "Y"],
|
||||
["takeown",
|
||||
"/F",
|
||||
str(path),
|
||||
"/R",
|
||||
"/D",
|
||||
"Y"],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
@@ -341,14 +378,28 @@ def fix_permissions_windows(path: Path, user: Optional[str] = None) -> bool:
|
||||
rc_setowner = 1
|
||||
rc_grant = 1
|
||||
try:
|
||||
out = subprocess.run(["icacls", str(path), "/setowner", user, "/T", "/C"], check=False)
|
||||
out = subprocess.run(
|
||||
["icacls",
|
||||
str(path),
|
||||
"/setowner",
|
||||
user,
|
||||
"/T",
|
||||
"/C"],
|
||||
check=False
|
||||
)
|
||||
rc_setowner = int(out.returncode)
|
||||
except Exception:
|
||||
rc_setowner = 1
|
||||
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["icacls", str(path), "/grant", f"{user}:(OI)(CI)F", "/T", "/C"], check=False
|
||||
["icacls",
|
||||
str(path),
|
||||
"/grant",
|
||||
f"{user}:(OI)(CI)F",
|
||||
"/T",
|
||||
"/C"],
|
||||
check=False
|
||||
)
|
||||
rc_grant = int(out.returncode)
|
||||
except Exception:
|
||||
@@ -368,7 +419,9 @@ def fix_permissions_windows(path: Path, user: Optional[str] = None) -> bool:
|
||||
|
||||
|
||||
def fix_permissions_unix(
|
||||
path: Path, user: Optional[str] = None, group: Optional[str] = None
|
||||
path: Path,
|
||||
user: Optional[str] = None,
|
||||
group: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Attempt to chown/chmod recursively for a Unix-like system.
|
||||
|
||||
@@ -398,7 +451,13 @@ def fix_permissions_unix(
|
||||
)
|
||||
|
||||
try:
|
||||
subprocess.run(["chown", "-R", f"{user}:{group or pw.pw_gid}", str(path)], check=True)
|
||||
subprocess.run(
|
||||
["chown",
|
||||
"-R",
|
||||
f"{user}:{group or pw.pw_gid}",
|
||||
str(path)],
|
||||
check=True
|
||||
)
|
||||
except Exception:
|
||||
# Best-effort fallback: chown/chmod individual entries
|
||||
for root_dir, dirs, files in os.walk(path):
|
||||
@@ -426,14 +485,20 @@ def fix_permissions_unix(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logging.info("Unix permission fix attempted (some changes may require root privilege).")
|
||||
logging.info(
|
||||
"Unix permission fix attempted (some changes may require root privilege)."
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logging.debug("Unix fix-permissions error: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def fix_permissions(path: Path, user: Optional[str] = None, group: Optional[str] = None) -> bool:
|
||||
def fix_permissions(
|
||||
path: Path,
|
||||
user: Optional[str] = None,
|
||||
group: Optional[str] = None
|
||||
) -> bool:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
return fix_permissions_windows(path, user=user)
|
||||
@@ -554,9 +619,8 @@ def open_in_editor(path: Path) -> bool:
|
||||
pass
|
||||
|
||||
# Linux: use xdg-open only if a display is available and xdg-open exists
|
||||
if shutil.which("xdg-open") and (
|
||||
os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")
|
||||
):
|
||||
if shutil.which("xdg-open") and (os.environ.get("DISPLAY")
|
||||
or os.environ.get("WAYLAND_DISPLAY")):
|
||||
try:
|
||||
subprocess.run(["xdg-open", str(path)], check=False)
|
||||
logging.info("Opened %s with default application", path)
|
||||
@@ -565,7 +629,8 @@ def open_in_editor(path: Path) -> bool:
|
||||
pass
|
||||
|
||||
logging.debug(
|
||||
"No available method to open %s automatically (headless or no opener installed)", path
|
||||
"No available method to open %s automatically (headless or no opener installed)",
|
||||
path
|
||||
)
|
||||
return False
|
||||
except Exception as exc:
|
||||
@@ -574,12 +639,15 @@ def open_in_editor(path: Path) -> bool:
|
||||
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Clone Hydrus into a 'hydrusnetwork' directory.")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Clone Hydrus into a 'hydrusnetwork' directory."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
"-r",
|
||||
default=".",
|
||||
help="Root folder to create the hydrusnetwork directory in (default: current working directory)",
|
||||
help=
|
||||
"Root folder to create the hydrusnetwork directory in (default: current working directory)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dest-name",
|
||||
@@ -588,7 +656,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
help="Name of the destination folder (default: hydrusnetwork)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo", default="https://github.com/hydrusnetwork/hydrus", help="Repository URL to clone"
|
||||
"--repo",
|
||||
default="https://github.com/hydrusnetwork/hydrus",
|
||||
help="Repository URL to clone"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--update",
|
||||
@@ -602,31 +672,40 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
help="Remove existing destination directory before cloning",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--branch", "-b", default=None, help="Branch to clone (passed to git clone --branch)."
|
||||
"--branch",
|
||||
"-b",
|
||||
default=None,
|
||||
help="Branch to clone (passed to git clone --branch)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
type=int,
|
||||
default=1,
|
||||
help="If set, pass --depth to git clone (default: 1 for a shallow clone). Use --full to perform a full clone instead.",
|
||||
help=
|
||||
"If set, pass --depth to git clone (default: 1 for a shallow clone). Use --full to perform a full clone instead.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--full", action="store_true", help="Perform a full clone (no --depth passed to git clone)"
|
||||
"--full",
|
||||
action="store_true",
|
||||
help="Perform a full clone (no --depth passed to git clone)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--git",
|
||||
action="store_true",
|
||||
help="Use git clone instead of fetching repository ZIP (opt-in). Default: fetch ZIP (smaller).",
|
||||
help=
|
||||
"Use git clone instead of fetching repository ZIP (opt-in). Default: fetch ZIP (smaller).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-fallback",
|
||||
action="store_true",
|
||||
help="If set, do not attempt to download ZIP when git is missing (only relevant with --git)",
|
||||
help=
|
||||
"If set, do not attempt to download ZIP when git is missing (only relevant with --git)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix-permissions",
|
||||
action="store_true",
|
||||
help="Fix ownership/permissions on the obtained repo (OS-aware). Requires elevated privileges for some actions.",
|
||||
help=
|
||||
"Fix ownership/permissions on the obtained repo (OS-aware). Requires elevated privileges for some actions.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix-permissions-user",
|
||||
@@ -641,7 +720,8 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
parser.add_argument(
|
||||
"--no-venv",
|
||||
action="store_true",
|
||||
help="Do not create a venv inside the cloned repo (default: create a .venv folder)",
|
||||
help=
|
||||
"Do not create a venv inside the cloned repo (default: create a .venv folder)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--venv-name",
|
||||
@@ -649,7 +729,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
help="Name of the venv directory to create inside the repo (default: .venv)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--recreate-venv", action="store_true", help="Remove existing venv and create a fresh one"
|
||||
"--recreate-venv",
|
||||
action="store_true",
|
||||
help="Remove existing venv and create a fresh one"
|
||||
)
|
||||
# By default install dependencies into the created venv; use --no-install-deps to opt out
|
||||
group_install = parser.add_mutually_exclusive_group()
|
||||
@@ -657,7 +739,8 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
"--install-deps",
|
||||
dest="install_deps",
|
||||
action="store_true",
|
||||
help="Install dependencies from requirements.txt into the created venv (default).",
|
||||
help=
|
||||
"Install dependencies from requirements.txt into the created venv (default).",
|
||||
)
|
||||
group_install.add_argument(
|
||||
"--no-install-deps",
|
||||
@@ -669,17 +752,20 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
parser.add_argument(
|
||||
"--reinstall-deps",
|
||||
action="store_true",
|
||||
help="If present, force re-install dependencies into the created venv using pip --force-reinstall.",
|
||||
help=
|
||||
"If present, force re-install dependencies into the created venv using pip --force-reinstall.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-open-client",
|
||||
action="store_true",
|
||||
help="(ignored) installer no longer opens hydrus_client.py automatically; use the run_client helper to launch the client when ready.",
|
||||
help=
|
||||
"(ignored) installer no longer opens hydrus_client.py automatically; use the run_client helper to launch the client when ready.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run-client",
|
||||
action="store_true",
|
||||
help="Run hydrus_client.py using the repo-local venv's Python (if present). This runs the client in the foreground unless --run-client-detached is specified.",
|
||||
help=
|
||||
"Run hydrus_client.py using the repo-local venv's Python (if present). This runs the client in the foreground unless --run-client-detached is specified.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run-client-detached",
|
||||
@@ -689,7 +775,8 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
parser.add_argument(
|
||||
"--run-client-headless",
|
||||
action="store_true",
|
||||
help="If used with --run-client, attempt to run hydrus_client.py without showing the Qt GUI (best-effort)",
|
||||
help=
|
||||
"If used with --run-client, attempt to run hydrus_client.py without showing the Qt GUI (best-effort)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-service",
|
||||
@@ -759,7 +846,10 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
)
|
||||
return 0
|
||||
|
||||
logging.info("Destination %s is already a git repository.", dest)
|
||||
logging.info(
|
||||
"Destination %s is already a git repository.",
|
||||
dest
|
||||
)
|
||||
print("")
|
||||
print("Select an action:")
|
||||
print(
|
||||
@@ -779,13 +869,21 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
if choice == "1":
|
||||
# Install dependencies into the repository venv (create venv if needed)
|
||||
try:
|
||||
venv_dir = dest / str(getattr(args, "venv_name", ".venv"))
|
||||
venv_dir = dest / str(
|
||||
getattr(args,
|
||||
"venv_name",
|
||||
".venv")
|
||||
)
|
||||
if venv_dir.exists():
|
||||
logging.info("Using existing venv at %s", venv_dir)
|
||||
else:
|
||||
logging.info("Creating venv at %s", venv_dir)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "venv", str(venv_dir)], check=True
|
||||
[sys.executable,
|
||||
"-m",
|
||||
"venv",
|
||||
str(venv_dir)],
|
||||
check=True
|
||||
)
|
||||
venv_py = get_python_in_venv(venv_dir)
|
||||
except Exception as e:
|
||||
@@ -793,24 +891,45 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
return 8
|
||||
|
||||
if not venv_py:
|
||||
logging.error("Could not locate python in venv %s", venv_dir)
|
||||
logging.error(
|
||||
"Could not locate python in venv %s",
|
||||
venv_dir
|
||||
)
|
||||
return 9
|
||||
|
||||
req = find_requirements(dest)
|
||||
if not req:
|
||||
logging.info(
|
||||
"No requirements.txt found in %s; nothing to install.", dest
|
||||
"No requirements.txt found in %s; nothing to install.",
|
||||
dest
|
||||
)
|
||||
return 0
|
||||
|
||||
logging.info("Installing dependencies from %s into venv", req)
|
||||
logging.info(
|
||||
"Installing dependencies from %s into venv",
|
||||
req
|
||||
)
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(venv_py), "-m", "pip", "install", "--upgrade", "pip"],
|
||||
[
|
||||
str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip"
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[str(venv_py), "-m", "pip", "install", "-r", str(req)],
|
||||
[
|
||||
str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-r",
|
||||
str(req)
|
||||
],
|
||||
cwd=str(dest),
|
||||
check=True,
|
||||
)
|
||||
@@ -822,7 +941,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
# Post-install verification
|
||||
pkgs = parse_requirements_file(req)
|
||||
if pkgs:
|
||||
logging.info("Verifying installed packages inside the venv...")
|
||||
logging.info(
|
||||
"Verifying installed packages inside the venv..."
|
||||
)
|
||||
any_missing = False
|
||||
import_map = {
|
||||
"pyyaml": "yaml",
|
||||
@@ -843,7 +964,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
mod = import_map.get(pkg, pkg)
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(venv_py), "-c", f"import {mod}"],
|
||||
[str(venv_py),
|
||||
"-c",
|
||||
f"import {mod}"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
@@ -869,7 +992,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
|
||||
elif choice == "2":
|
||||
if not git:
|
||||
logging.error("Git not found; cannot --update without git")
|
||||
logging.error(
|
||||
"Git not found; cannot --update without git"
|
||||
)
|
||||
return 2
|
||||
try:
|
||||
run_git_pull(git, dest)
|
||||
@@ -882,17 +1007,29 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
elif choice == "3":
|
||||
# Install a user-level service to start the hydrus client on boot
|
||||
try:
|
||||
venv_dir = dest / str(getattr(args, "venv_name", ".venv"))
|
||||
venv_dir = dest / str(
|
||||
getattr(args,
|
||||
"venv_name",
|
||||
".venv")
|
||||
)
|
||||
if not venv_dir.exists():
|
||||
logging.info(
|
||||
"Creating venv at %s to perform service install", venv_dir
|
||||
"Creating venv at %s to perform service install",
|
||||
venv_dir
|
||||
)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "venv", str(venv_dir)], check=True
|
||||
[sys.executable,
|
||||
"-m",
|
||||
"venv",
|
||||
str(venv_dir)],
|
||||
check=True
|
||||
)
|
||||
venv_py = get_python_in_venv(venv_dir)
|
||||
except Exception as e:
|
||||
logging.error("Failed to prepare venv for service install: %s", e)
|
||||
logging.error(
|
||||
"Failed to prepare venv for service install: %s",
|
||||
e
|
||||
)
|
||||
return 8
|
||||
|
||||
if not venv_py:
|
||||
@@ -986,7 +1123,13 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
try:
|
||||
# Default behavior when using git: shallow clone (depth=1) unless --full specified.
|
||||
depth_to_use = None if getattr(args, "full", False) else args.depth
|
||||
run_git_clone(git, args.repo, dest, branch=args.branch, depth=depth_to_use)
|
||||
run_git_clone(
|
||||
git,
|
||||
args.repo,
|
||||
dest,
|
||||
branch=args.branch,
|
||||
depth=depth_to_use
|
||||
)
|
||||
logging.info("Repository cloned into %s", dest)
|
||||
obtained = True
|
||||
obtained_by = "git"
|
||||
@@ -1023,7 +1166,13 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
if not venv_dir.exists():
|
||||
logging.info("Creating venv at %s", venv_dir)
|
||||
try:
|
||||
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
|
||||
subprocess.run(
|
||||
[sys.executable,
|
||||
"-m",
|
||||
"venv",
|
||||
str(venv_dir)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error("Failed to create venv: %s", e)
|
||||
return 8
|
||||
@@ -1038,17 +1187,30 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
logging.info("Venv ready: %s", venv_py)
|
||||
|
||||
# Optionally install or reinstall requirements.txt
|
||||
if getattr(args, "install_deps", False) or getattr(args, "reinstall_deps", False):
|
||||
if getattr(args,
|
||||
"install_deps",
|
||||
False) or getattr(args,
|
||||
"reinstall_deps",
|
||||
False):
|
||||
req = find_requirements(dest)
|
||||
if req and req.exists():
|
||||
logging.info(
|
||||
"Installing dependencies from %s into venv (reinstall=%s)",
|
||||
req,
|
||||
bool(getattr(args, "reinstall_deps", False)),
|
||||
bool(getattr(args,
|
||||
"reinstall_deps",
|
||||
False)),
|
||||
)
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(venv_py), "-m", "pip", "install", "--upgrade", "pip"],
|
||||
[
|
||||
str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip"
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
if getattr(args, "reinstall_deps", False):
|
||||
@@ -1068,7 +1230,14 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
[str(venv_py), "-m", "pip", "install", "-r", str(req)],
|
||||
[
|
||||
str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-r",
|
||||
str(req)
|
||||
],
|
||||
cwd=str(dest),
|
||||
check=True,
|
||||
)
|
||||
@@ -1080,7 +1249,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
# Post-install verification: ensure packages are visible inside the venv
|
||||
pkgs = parse_requirements_file(req)
|
||||
if pkgs:
|
||||
logging.info("Verifying installed packages inside the venv...")
|
||||
logging.info(
|
||||
"Verifying installed packages inside the venv..."
|
||||
)
|
||||
any_missing = False
|
||||
# Small mapping for known differences between package name and import name
|
||||
import_map = {
|
||||
@@ -1101,14 +1272,19 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
for pkg in pkgs:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[str(venv_py), "-m", "pip", "show", pkg],
|
||||
[str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"show",
|
||||
pkg],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if out.returncode != 0 or not out.stdout.strip():
|
||||
logging.warning(
|
||||
"Package '%s' not found in venv (pip show failed).", pkg
|
||||
"Package '%s' not found in venv (pip show failed).",
|
||||
pkg
|
||||
)
|
||||
any_missing = True
|
||||
continue
|
||||
@@ -1116,7 +1292,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
import_name = import_map.get(pkg, pkg)
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(venv_py), "-c", f"import {import_name}"],
|
||||
[
|
||||
str(venv_py),
|
||||
"-c",
|
||||
f"import {import_name}"
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
@@ -1129,7 +1309,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
)
|
||||
any_missing = True
|
||||
except Exception as exc:
|
||||
logging.debug("Verification error for package %s: %s", pkg, exc)
|
||||
logging.debug(
|
||||
"Verification error for package %s: %s",
|
||||
pkg,
|
||||
exc
|
||||
)
|
||||
any_missing = True
|
||||
|
||||
if any_missing:
|
||||
@@ -1182,7 +1366,10 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
logging.info(" source %s/bin/activate", venv_dir)
|
||||
|
||||
# Optionally open/run hydrus_client.py in the repo for convenience (open by default if present).
|
||||
client_candidates = [dest / "hydrus_client.py", dest / "client" / "hydrus_client.py"]
|
||||
client_candidates = [
|
||||
dest / "hydrus_client.py",
|
||||
dest / "client" / "hydrus_client.py"
|
||||
]
|
||||
client_found = None
|
||||
for p in client_candidates:
|
||||
if p.exists():
|
||||
@@ -1197,12 +1384,18 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
if cand.exists():
|
||||
run_client_script = cand
|
||||
break
|
||||
if getattr(args, "install_service", False) or getattr(args, "uninstall_service", False):
|
||||
if getattr(args,
|
||||
"install_service",
|
||||
False) or getattr(args,
|
||||
"uninstall_service",
|
||||
False):
|
||||
if not venv_py:
|
||||
venv_dir = dest / str(getattr(args, "venv_name", ".venv"))
|
||||
venv_py = get_python_in_venv(venv_dir)
|
||||
if not venv_py:
|
||||
logging.error("Could not locate python in repo venv; cannot manage service.")
|
||||
logging.error(
|
||||
"Could not locate python in repo venv; cannot manage service."
|
||||
)
|
||||
else:
|
||||
if getattr(args, "install_service", False):
|
||||
if run_client_script.exists():
|
||||
@@ -1224,7 +1417,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
else:
|
||||
if install_service_auto:
|
||||
ok = install_service_auto(
|
||||
args.service_name, dest, venv_py, headless=True, detached=True
|
||||
args.service_name,
|
||||
dest,
|
||||
venv_py,
|
||||
headless=True,
|
||||
detached=True
|
||||
)
|
||||
if ok:
|
||||
logging.info("Service installed (user-level).")
|
||||
@@ -1253,7 +1450,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
logging.error("Service uninstall failed: %s", e)
|
||||
else:
|
||||
if uninstall_service_auto:
|
||||
ok = uninstall_service_auto(args.service_name, dest, venv_py)
|
||||
ok = uninstall_service_auto(
|
||||
args.service_name,
|
||||
dest,
|
||||
venv_py
|
||||
)
|
||||
if ok:
|
||||
logging.info("Service removed.")
|
||||
else:
|
||||
@@ -1294,17 +1495,27 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
if getattr(args, "run_client_detached", False):
|
||||
cmd.append("--detached")
|
||||
|
||||
logging.info("Running hydrus client via helper: %s", cmd)
|
||||
logging.info(
|
||||
"Running hydrus client via helper: %s",
|
||||
cmd
|
||||
)
|
||||
try:
|
||||
if getattr(args, "run_client_detached", False):
|
||||
kwargs = detach_kwargs_for_platform()
|
||||
kwargs.update({"cwd": str(dest)})
|
||||
kwargs.update({
|
||||
"cwd": str(dest)
|
||||
})
|
||||
subprocess.Popen(cmd, **kwargs)
|
||||
logging.info("Hydrus client launched (detached).")
|
||||
logging.info(
|
||||
"Hydrus client launched (detached)."
|
||||
)
|
||||
else:
|
||||
subprocess.run(cmd, cwd=str(dest))
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error("run_client.py exited non-zero: %s", e)
|
||||
logging.error(
|
||||
"run_client.py exited non-zero: %s",
|
||||
e
|
||||
)
|
||||
else:
|
||||
# Fallback: call the client directly; support headless by setting
|
||||
# QT_QPA_PLATFORM or using xvfb-run on Linux.
|
||||
@@ -1327,23 +1538,34 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
)
|
||||
|
||||
logging.info(
|
||||
"Running hydrus client with %s: %s", venv_py, client_found
|
||||
"Running hydrus client with %s: %s",
|
||||
venv_py,
|
||||
client_found
|
||||
)
|
||||
if getattr(args, "run_client_detached", False):
|
||||
try:
|
||||
kwargs = detach_kwargs_for_platform()
|
||||
kwargs.update({"cwd": str(dest), "env": env})
|
||||
kwargs.update({
|
||||
"cwd": str(dest),
|
||||
"env": env
|
||||
})
|
||||
subprocess.Popen(cmd, **kwargs)
|
||||
logging.info("Hydrus client launched (detached).")
|
||||
logging.info(
|
||||
"Hydrus client launched (detached)."
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.exception(
|
||||
"Failed to launch client detached: %s", exc
|
||||
"Failed to launch client detached: %s",
|
||||
exc
|
||||
)
|
||||
else:
|
||||
try:
|
||||
subprocess.run(cmd, cwd=str(dest), env=env)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error("hydrus client exited non-zero: %s", e)
|
||||
logging.error(
|
||||
"hydrus client exited non-zero: %s",
|
||||
e
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.exception("Failed to run hydrus client: %s", exc)
|
||||
|
||||
@@ -1360,9 +1582,9 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
# Helpful hint: show the new run_client helper and direct run example
|
||||
try:
|
||||
helper_to_show = (
|
||||
run_client_script
|
||||
if (run_client_script and run_client_script.exists())
|
||||
else (script_dir / "run_client.py")
|
||||
run_client_script if
|
||||
(run_client_script and run_client_script.exists()) else
|
||||
(script_dir / "run_client.py")
|
||||
)
|
||||
if venv_py:
|
||||
logging.info(
|
||||
@@ -1375,13 +1597,15 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
)
|
||||
else:
|
||||
logging.info(
|
||||
"To run the Hydrus client: python %s [args]", dest / "hydrus_client.py"
|
||||
"To run the Hydrus client: python %s [args]",
|
||||
dest / "hydrus_client.py"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
logging.debug(
|
||||
"No hydrus_client.py found to open or run (looked in %s).", client_candidates
|
||||
"No hydrus_client.py found to open or run (looked in %s).",
|
||||
client_candidates
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -58,7 +58,10 @@ from SYS.logger import log
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s")
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[%(asctime)s] %(levelname)s: %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_PATH: Optional[Path] = None
|
||||
@@ -101,7 +104,9 @@ def get_local_ip() -> Optional[str]:
|
||||
def create_app():
|
||||
"""Create and configure Flask app with all routes."""
|
||||
if not HAS_FLASK:
|
||||
raise ImportError("Flask not installed. Install with: pip install flask flask-cors")
|
||||
raise ImportError(
|
||||
"Flask not installed. Install with: pip install flask flask-cors"
|
||||
)
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
@@ -117,11 +122,13 @@ def create_app():
|
||||
"""Decorator to check API key authentication if configured."""
|
||||
|
||||
def decorator(f):
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if API_KEY:
|
||||
# Get API key from header or query parameter
|
||||
provided_key = request.headers.get("X-API-Key") or request.args.get("api_key")
|
||||
provided_key = request.headers.get("X-API-Key"
|
||||
) or request.args.get("api_key")
|
||||
if not provided_key or provided_key != API_KEY:
|
||||
return jsonify({"error": "Unauthorized. Invalid or missing API key."}), 401
|
||||
return f(*args, **kwargs)
|
||||
@@ -134,6 +141,7 @@ def create_app():
|
||||
"""Decorator to ensure storage path is configured."""
|
||||
|
||||
def decorator(f):
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not STORAGE_PATH:
|
||||
@@ -193,7 +201,10 @@ def create_app():
|
||||
with LocalLibrarySearchOptimizer(STORAGE_PATH) as db:
|
||||
results = db.search_by_name(query, limit)
|
||||
tag_results = db.search_by_tag(query, limit)
|
||||
all_results = {r["hash"]: r for r in (results + tag_results)}
|
||||
all_results = {
|
||||
r["hash"]: r
|
||||
for r in (results + tag_results)
|
||||
}
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
@@ -386,7 +397,8 @@ def create_app():
|
||||
return jsonify({"error": "File not found"}), 404
|
||||
|
||||
metadata = db.get_metadata(file_path)
|
||||
relationships = metadata.get("relationships", {}) if metadata else {}
|
||||
relationships = metadata.get("relationships",
|
||||
{}) if metadata else {}
|
||||
return jsonify({"hash": file_hash, "relationships": relationships}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Get relationships error: {e}", exc_info=True)
|
||||
@@ -486,15 +498,32 @@ def main():
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Remote Storage Server for Medios-Macina",
|
||||
epilog="Example: python remote_storage_server.py --storage-path /storage/media --port 5000 --api-key mysecretkey",
|
||||
epilog=
|
||||
"Example: python remote_storage_server.py --storage-path /storage/media --port 5000 --api-key mysecretkey",
|
||||
)
|
||||
parser.add_argument("--storage-path", type=str, required=True, help="Path to storage directory")
|
||||
parser.add_argument(
|
||||
"--host", type=str, default="0.0.0.0", help="Server host (default: 0.0.0.0)"
|
||||
"--storage-path",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Path to storage directory"
|
||||
)
|
||||
parser.add_argument("--port", type=int, default=5000, help="Server port (default: 5000)")
|
||||
parser.add_argument(
|
||||
"--api-key", type=str, default=None, help="API key for authentication (optional)"
|
||||
"--host",
|
||||
type=str,
|
||||
default="0.0.0.0",
|
||||
help="Server host (default: 0.0.0.0)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=5000,
|
||||
help="Server port (default: 5000)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
type=str,
|
||||
default=None,
|
||||
help="API key for authentication (optional)"
|
||||
)
|
||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||
|
||||
|
||||
@@ -59,7 +59,8 @@ def find_requirements(root: Path) -> Optional[Path]:
|
||||
for p in root.iterdir():
|
||||
if not p.is_dir():
|
||||
continue
|
||||
for child in (p,):
|
||||
for child in (p,
|
||||
):
|
||||
candidate = child / "requirements.txt"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
@@ -68,10 +69,22 @@ def find_requirements(root: Path) -> Optional[Path]:
|
||||
return None
|
||||
|
||||
|
||||
def install_requirements(venv_py: Path, req_path: Path, reinstall: bool = False) -> bool:
|
||||
def install_requirements(
|
||||
venv_py: Path,
|
||||
req_path: Path,
|
||||
reinstall: bool = False
|
||||
) -> bool:
|
||||
try:
|
||||
print(f"Installing {req_path} into venv ({venv_py})...")
|
||||
subprocess.run([str(venv_py), "-m", "pip", "install", "--upgrade", "pip"], check=True)
|
||||
subprocess.run(
|
||||
[str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip"],
|
||||
check=True
|
||||
)
|
||||
install_cmd = [str(venv_py), "-m", "pip", "install", "-r", str(req_path)]
|
||||
if reinstall:
|
||||
install_cmd = [
|
||||
@@ -138,7 +151,11 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
|
||||
for pkg in packages:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[str(venv_py), "-m", "pip", "show", pkg],
|
||||
[str(venv_py),
|
||||
"-m",
|
||||
"pip",
|
||||
"show",
|
||||
pkg],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
@@ -158,7 +175,9 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
|
||||
import_name = import_map.get(pkg, pkg)
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(venv_py), "-c", f"import {import_name}"],
|
||||
[str(venv_py),
|
||||
"-c",
|
||||
f"import {import_name}"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
@@ -167,7 +186,10 @@ def verify_imports(venv_py: Path, packages: List[str]) -> bool:
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
missing.append(pkg)
|
||||
if missing:
|
||||
print("The following packages were not importable in the venv:", ", ".join(missing))
|
||||
print(
|
||||
"The following packages were not importable in the venv:",
|
||||
", ".join(missing)
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -199,7 +221,9 @@ def install_service_windows(
|
||||
try:
|
||||
schtasks = shutil.which("schtasks")
|
||||
if not schtasks:
|
||||
print("schtasks not available on this system; cannot install Windows scheduled task.")
|
||||
print(
|
||||
"schtasks not available on this system; cannot install Windows scheduled task."
|
||||
)
|
||||
return False
|
||||
|
||||
bat = repo_root / "run-client.bat"
|
||||
@@ -238,7 +262,9 @@ def uninstall_service_windows(service_name: str) -> bool:
|
||||
try:
|
||||
schtasks = shutil.which("schtasks")
|
||||
if not schtasks:
|
||||
print("schtasks not available on this system; cannot remove scheduled task.")
|
||||
print(
|
||||
"schtasks not available on this system; cannot remove scheduled task."
|
||||
)
|
||||
return False
|
||||
cmd = [schtasks, "/Delete", "/TN", service_name, "/F"]
|
||||
subprocess.run(cmd, check=True)
|
||||
@@ -253,13 +279,25 @@ def uninstall_service_windows(service_name: str) -> bool:
|
||||
|
||||
|
||||
def install_service_systemd(
|
||||
service_name: str, repo_root: Path, venv_py: Path, headless: bool = True, detached: bool = True
|
||||
service_name: str,
|
||||
repo_root: Path,
|
||||
venv_py: Path,
|
||||
headless: bool = True,
|
||||
detached: bool = True
|
||||
) -> bool:
|
||||
try:
|
||||
systemctl = shutil.which("systemctl")
|
||||
if not systemctl:
|
||||
print("systemctl not available; falling back to crontab @reboot (if present).")
|
||||
return install_service_cron(service_name, repo_root, venv_py, headless, detached)
|
||||
print(
|
||||
"systemctl not available; falling back to crontab @reboot (if present)."
|
||||
)
|
||||
return install_service_cron(
|
||||
service_name,
|
||||
repo_root,
|
||||
venv_py,
|
||||
headless,
|
||||
detached
|
||||
)
|
||||
|
||||
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
||||
unit_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -270,7 +308,12 @@ def install_service_systemd(
|
||||
unit_file.write_text(content, encoding="utf-8")
|
||||
subprocess.run([systemctl, "--user", "daemon-reload"], check=True)
|
||||
subprocess.run(
|
||||
[systemctl, "--user", "enable", "--now", f"{service_name}.service"], check=True
|
||||
[systemctl,
|
||||
"--user",
|
||||
"enable",
|
||||
"--now",
|
||||
f"{service_name}.service"],
|
||||
check=True
|
||||
)
|
||||
print(f"systemd user service '{service_name}' installed and started.")
|
||||
return True
|
||||
@@ -289,9 +332,15 @@ def uninstall_service_systemd(service_name: str) -> bool:
|
||||
print("systemctl not available; cannot uninstall systemd service.")
|
||||
return False
|
||||
subprocess.run(
|
||||
[systemctl, "--user", "disable", "--now", f"{service_name}.service"], check=False
|
||||
[systemctl,
|
||||
"--user",
|
||||
"disable",
|
||||
"--now",
|
||||
f"{service_name}.service"],
|
||||
check=False
|
||||
)
|
||||
unit_file = Path.home() / ".config" / "systemd" / "user" / f"{service_name}.service"
|
||||
unit_file = Path.home(
|
||||
) / ".config" / "systemd" / "user" / f"{service_name}.service"
|
||||
if unit_file.exists():
|
||||
unit_file.unlink()
|
||||
subprocess.run([systemctl, "--user", "daemon-reload"], check=True)
|
||||
@@ -303,7 +352,11 @@ def uninstall_service_systemd(service_name: str) -> bool:
|
||||
|
||||
|
||||
def install_service_cron(
|
||||
service_name: str, repo_root: Path, venv_py: Path, headless: bool = True, detached: bool = True
|
||||
service_name: str,
|
||||
repo_root: Path,
|
||||
venv_py: Path,
|
||||
headless: bool = True,
|
||||
detached: bool = True
|
||||
) -> bool:
|
||||
try:
|
||||
crontab = shutil.which("crontab")
|
||||
@@ -312,7 +365,11 @@ def install_service_cron(
|
||||
return False
|
||||
entry = f"@reboot {venv_py} {str(repo_root / 'run_client.py')} --detached {'--headless' if headless else '--gui'} # {service_name}\n"
|
||||
proc = subprocess.run(
|
||||
[crontab, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
[crontab,
|
||||
"-l"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
existing = proc.stdout if proc.returncode == 0 else ""
|
||||
if entry.strip() in existing:
|
||||
@@ -337,7 +394,11 @@ def uninstall_service_cron(service_name: str, repo_root: Path, venv_py: Path) ->
|
||||
print("crontab not available; cannot remove reboot cron job.")
|
||||
return False
|
||||
proc = subprocess.run(
|
||||
[crontab, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
[crontab,
|
||||
"-l"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
print("No crontab found for user; nothing to remove.")
|
||||
@@ -356,21 +417,37 @@ def uninstall_service_cron(service_name: str, repo_root: Path, venv_py: Path) ->
|
||||
|
||||
|
||||
def install_service_auto(
|
||||
service_name: str, repo_root: Path, venv_py: Path, headless: bool = True, detached: bool = True
|
||||
service_name: str,
|
||||
repo_root: Path,
|
||||
venv_py: Path,
|
||||
headless: bool = True,
|
||||
detached: bool = True
|
||||
) -> bool:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
return install_service_windows(
|
||||
service_name, repo_root, venv_py, headless=headless, detached=detached
|
||||
service_name,
|
||||
repo_root,
|
||||
venv_py,
|
||||
headless=headless,
|
||||
detached=detached
|
||||
)
|
||||
else:
|
||||
if shutil.which("systemctl"):
|
||||
return install_service_systemd(
|
||||
service_name, repo_root, venv_py, headless=headless, detached=detached
|
||||
service_name,
|
||||
repo_root,
|
||||
venv_py,
|
||||
headless=headless,
|
||||
detached=detached
|
||||
)
|
||||
else:
|
||||
return install_service_cron(
|
||||
service_name, repo_root, venv_py, headless=headless, detached=detached
|
||||
service_name,
|
||||
repo_root,
|
||||
venv_py,
|
||||
headless=headless,
|
||||
detached=detached
|
||||
)
|
||||
except Exception as exc:
|
||||
print("install_service_auto error:", exc)
|
||||
@@ -391,7 +468,11 @@ def uninstall_service_auto(service_name: str, repo_root: Path, venv_py: Path) ->
|
||||
return False
|
||||
|
||||
|
||||
def print_activation_instructions(repo_root: Path, venv_dir: Path, venv_py: Path) -> None:
|
||||
def print_activation_instructions(
|
||||
repo_root: Path,
|
||||
venv_dir: Path,
|
||||
venv_py: Path
|
||||
) -> None:
|
||||
print("\nActivation and run examples:")
|
||||
# PowerShell
|
||||
print(f" PowerShell:\n . {shlex.quote(str(venv_dir))}\\Scripts\\Activate.ps1")
|
||||
@@ -417,7 +498,9 @@ def detach_kwargs_for_platform():
|
||||
return kwargs
|
||||
|
||||
|
||||
def find_venv_python(repo_root: Path, venv_arg: Optional[str], venv_name: str) -> Optional[Path]:
|
||||
def find_venv_python(repo_root: Path,
|
||||
venv_arg: Optional[str],
|
||||
venv_name: str) -> Optional[Path]:
|
||||
# venv_arg may be a python executable or a directory
|
||||
if venv_arg:
|
||||
p = Path(venv_arg)
|
||||
@@ -454,7 +537,9 @@ def _python_can_import(python_exe: Path, modules: List[str]) -> bool:
|
||||
# Build a short import test string. Use semicolons to ensure any import error results in non-zero exit.
|
||||
imports = ";".join([f"import {m}" for m in modules])
|
||||
out = subprocess.run(
|
||||
[str(python_exe), "-c", imports],
|
||||
[str(python_exe),
|
||||
"-c",
|
||||
imports],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=10,
|
||||
@@ -466,11 +551,17 @@ def _python_can_import(python_exe: Path, modules: List[str]) -> bool:
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Run hydrus_client.py using the repo-local venv Python (top-level helper)"
|
||||
description=
|
||||
"Run hydrus_client.py using the repo-local venv Python (top-level helper)"
|
||||
)
|
||||
p.add_argument("--venv", help="Path to venv dir or python executable (overrides default .venv)")
|
||||
p.add_argument(
|
||||
"--venv-name", default=".venv", help="Name of the venv folder to look for (default: .venv)"
|
||||
"--venv",
|
||||
help="Path to venv dir or python executable (overrides default .venv)"
|
||||
)
|
||||
p.add_argument(
|
||||
"--venv-name",
|
||||
default=".venv",
|
||||
help="Name of the venv folder to look for (default: .venv)"
|
||||
)
|
||||
p.add_argument(
|
||||
"--client",
|
||||
@@ -490,22 +581,26 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
p.add_argument(
|
||||
"--reinstall",
|
||||
action="store_true",
|
||||
help="Force re-install dependencies from requirements.txt into the venv (uses --force-reinstall)",
|
||||
help=
|
||||
"Force re-install dependencies from requirements.txt into the venv (uses --force-reinstall)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="Verify that packages from requirements.txt are importable in the venv (after install)",
|
||||
help=
|
||||
"Verify that packages from requirements.txt are importable in the venv (after install)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--no-verify",
|
||||
action="store_true",
|
||||
help="Skip verification and do not prompt to install missing dependencies; proceed to run with the chosen Python",
|
||||
help=
|
||||
"Skip verification and do not prompt to install missing dependencies; proceed to run with the chosen Python",
|
||||
)
|
||||
p.add_argument(
|
||||
"--headless",
|
||||
action="store_true",
|
||||
help="Attempt to launch the client without showing the Qt GUI (best-effort). Default for subsequent runs; first run will show GUI unless --headless is supplied",
|
||||
help=
|
||||
"Attempt to launch the client without showing the Qt GUI (best-effort). Default for subsequent runs; first run will show GUI unless --headless is supplied",
|
||||
)
|
||||
p.add_argument(
|
||||
"--gui",
|
||||
@@ -513,12 +608,15 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
help="Start the client with the GUI visible (overrides headless/default) ",
|
||||
)
|
||||
p.add_argument(
|
||||
"--detached", action="store_true", help="Start the client and do not wait (detached)"
|
||||
"--detached",
|
||||
action="store_true",
|
||||
help="Start the client and do not wait (detached)"
|
||||
)
|
||||
p.add_argument(
|
||||
"--install-service",
|
||||
action="store_true",
|
||||
help="Install a user-level start-on-boot service/scheduled task for the hydrus client",
|
||||
help=
|
||||
"Install a user-level start-on-boot service/scheduled task for the hydrus client",
|
||||
)
|
||||
p.add_argument(
|
||||
"--uninstall-service",
|
||||
@@ -531,7 +629,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
help="Name of the service / scheduled task to install (default: hydrus-client)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--cwd", default=None, help="Working directory to start the client in (default: repo root)"
|
||||
"--cwd",
|
||||
default=None,
|
||||
help="Working directory to start the client in (default: repo root)"
|
||||
)
|
||||
p.add_argument("--quiet", action="store_true", help="Reduce output")
|
||||
p.add_argument(
|
||||
@@ -557,9 +657,13 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
|
||||
def _is_running_in_virtualenv() -> bool:
|
||||
try:
|
||||
return hasattr(sys, "real_prefix") or getattr(sys, "base_prefix", None) != getattr(
|
||||
sys, "prefix", None
|
||||
)
|
||||
return hasattr(sys,
|
||||
"real_prefix") or getattr(sys,
|
||||
"base_prefix",
|
||||
None
|
||||
) != getattr(sys,
|
||||
"prefix",
|
||||
None)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -619,7 +723,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
"Create one with: python -m venv .venv (inside your hydrus repo) and then re-run this helper, or use the installer to create it for you."
|
||||
)
|
||||
print_activation_instructions(
|
||||
repo_root, repo_root / args.venv_name, repo_root / args.venv_name
|
||||
repo_root,
|
||||
repo_root / args.venv_name,
|
||||
repo_root / args.venv_name
|
||||
)
|
||||
return 2
|
||||
|
||||
@@ -645,7 +751,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
if pkgs:
|
||||
okv = verify_imports(venv_py, pkgs)
|
||||
if not okv:
|
||||
print("Verification failed; see instructions above to re-run installation.")
|
||||
print(
|
||||
"Verification failed; see instructions above to re-run installation."
|
||||
)
|
||||
|
||||
# If not installing but user asked to verify, do verification only
|
||||
if args.verify and not (args.install_deps or args.reinstall):
|
||||
@@ -698,7 +806,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
|
||||
if args.install_service:
|
||||
ok = install_service_auto(
|
||||
args.service_name, repo_root, venv_py, headless=use_headless, detached=True
|
||||
args.service_name,
|
||||
repo_root,
|
||||
venv_py,
|
||||
headless=use_headless,
|
||||
detached=True
|
||||
)
|
||||
return 0 if ok else 6
|
||||
if args.uninstall_service:
|
||||
@@ -723,7 +835,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
env = os.environ.copy()
|
||||
if headless:
|
||||
if os.name == "posix" and shutil.which("xvfb-run"):
|
||||
xvfb_cmd = ["xvfb-run", "--auto-servernum", "--server-args=-screen 0 1024x768x24"]
|
||||
xvfb_cmd = [
|
||||
"xvfb-run",
|
||||
"--auto-servernum",
|
||||
"--server-args=-screen 0 1024x768x24"
|
||||
]
|
||||
cmd = xvfb_cmd + cmd
|
||||
if not args.quiet:
|
||||
print("Headless: using xvfb-run to provide a virtual X server")
|
||||
@@ -744,7 +860,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
if args.detached:
|
||||
try:
|
||||
kwargs = detach_kwargs_for_platform()
|
||||
kwargs.update({"cwd": str(cwd), "env": env})
|
||||
kwargs.update({
|
||||
"cwd": str(cwd),
|
||||
"env": env
|
||||
})
|
||||
subprocess.Popen(cmd, **kwargs)
|
||||
print("Hydrus client launched (detached).")
|
||||
return 0
|
||||
|
||||
246
scripts/setup.py
246
scripts/setup.py
@@ -16,39 +16,6 @@ scripts/setup.py
|
||||
|
||||
"""
|
||||
|
||||
""" scripts/setup.py
|
||||
|
||||
Unified project setup helper (Python-only).
|
||||
|
||||
This script installs Python dependencies from `requirements.txt` and then
|
||||
downloads Playwright browser binaries by running `python -m playwright install`.
|
||||
By default this script installs **Chromium** only to conserve space; pass
|
||||
`--browsers all` to install all supported engines (chromium, firefox, webkit).
|
||||
|
||||
When invoked without any arguments, `setup.py` will automatically select and
|
||||
run the platform-specific bootstrap helper (`scripts/bootstrap.ps1` on Windows
|
||||
or `scripts/bootstrap.sh` on POSIX) in **non-interactive (quiet)** mode so a
|
||||
single `python ./scripts/setup.py` call does the usual bootstrap on your OS.
|
||||
|
||||
The platform bootstrap scripts also attempt (best-effort) to install `mpv` if
|
||||
it is not found on your PATH, since some workflows use it.
|
||||
|
||||
Usage:
|
||||
python ./scripts/bootstrap.py # install deps and playwright browsers (or run platform bootstrap if no args)
|
||||
python ./scripts/setup.py --skip-deps
|
||||
python ./scripts/setup.py --playwright-only
|
||||
|
||||
Optional flags:
|
||||
--skip-deps Skip `pip install -r requirements.txt` step
|
||||
--no-playwright Skip running `python -m playwright install` (still installs deps)
|
||||
--playwright-only Install only Playwright browsers (installs playwright package if missing)
|
||||
--browsers Comma-separated list of Playwright browsers to install (default: chromium)
|
||||
--install-editable Install the project in editable mode (pip install -e .) for running tests
|
||||
--install-deno Install the Deno runtime using the official installer
|
||||
--deno-version Pin a specific Deno version to install (e.g., v1.34.3)
|
||||
--upgrade-pip Upgrade pip, setuptools, and wheel before installing deps
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -100,7 +67,16 @@ def run_platform_bootstrap(repo_root: Path) -> int:
|
||||
if not exe:
|
||||
print("PowerShell not found; cannot run bootstrap.ps1", file=sys.stderr)
|
||||
return 1
|
||||
cmd = [exe, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(ps1), "-Quiet"]
|
||||
cmd = [
|
||||
exe,
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(ps1),
|
||||
"-Quiet"
|
||||
]
|
||||
elif sh_script.exists():
|
||||
shell = _find_shell()
|
||||
if not shell:
|
||||
@@ -115,7 +91,10 @@ def run_platform_bootstrap(repo_root: Path) -> int:
|
||||
print("Running platform bootstrap script:", " ".join(cmd))
|
||||
rc = subprocess.run(cmd, cwd=str(repo_root))
|
||||
if rc.returncode != 0:
|
||||
print(f"Bootstrap script failed with exit code {rc.returncode}", file=sys.stderr)
|
||||
print(
|
||||
f"Bootstrap script failed with exit code {rc.returncode}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return int(rc.returncode or 0)
|
||||
|
||||
|
||||
@@ -145,10 +124,14 @@ def _build_playwright_install_cmd(browsers: str | None) -> list[str]:
|
||||
if "all" in items:
|
||||
return base
|
||||
|
||||
allowed = {"chromium", "firefox", "webkit"}
|
||||
allowed = {"chromium",
|
||||
"firefox",
|
||||
"webkit"}
|
||||
invalid = [b for b in items if b not in allowed]
|
||||
if invalid:
|
||||
raise ValueError(f"invalid browsers specified: {invalid}. Valid choices: chromium, firefox, webkit, or 'all'")
|
||||
raise ValueError(
|
||||
f"invalid browsers specified: {invalid}. Valid choices: chromium, firefox, webkit, or 'all'"
|
||||
)
|
||||
return base + items
|
||||
|
||||
|
||||
@@ -171,7 +154,16 @@ def _install_deno(version: str | None = None) -> int:
|
||||
ps_cmd = f"iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno -Version {ver}"
|
||||
else:
|
||||
ps_cmd = "iwr https://deno.land/x/install/install.ps1 -useb | iex"
|
||||
run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd])
|
||||
run(
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
ps_cmd
|
||||
]
|
||||
)
|
||||
else:
|
||||
# POSIX: use curl + sh installer
|
||||
if version:
|
||||
@@ -186,7 +178,10 @@ def _install_deno(version: str | None = None) -> int:
|
||||
print(f"Deno installed at: {shutil.which('deno')}")
|
||||
return 0
|
||||
else:
|
||||
print("Deno installation completed but 'deno' not found in PATH. You may need to add Deno's bin directory to your PATH manually.", file=sys.stderr)
|
||||
print(
|
||||
"Deno installation completed but 'deno' not found in PATH. You may need to add Deno's bin directory to your PATH manually.",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Deno install failed: {exc}", file=sys.stderr)
|
||||
@@ -194,17 +189,58 @@ def _install_deno(version: str | None = None) -> int:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Setup Medios-Macina: install deps and Playwright browsers")
|
||||
parser.add_argument("--skip-deps", action="store_true", help="Skip installing Python dependencies from requirements.txt")
|
||||
parser.add_argument("--no-playwright", action="store_true", help="Skip running 'playwright install' (only install packages)")
|
||||
parser.add_argument("--playwright-only", action="store_true", help="Only run 'playwright install' (skips dependency installation)")
|
||||
parser.add_argument("--browsers", type=str, default="chromium", help="Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)")
|
||||
parser.add_argument("--install-editable", action="store_true", help="Install the project in editable mode (pip install -e .) for running tests")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Setup Medios-Macina: install deps and Playwright browsers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-deps",
|
||||
action="store_true",
|
||||
help="Skip installing Python dependencies from requirements.txt"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-playwright",
|
||||
action="store_true",
|
||||
help="Skip running 'playwright install' (only install packages)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--playwright-only",
|
||||
action="store_true",
|
||||
help="Only run 'playwright install' (skips dependency installation)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--browsers",
|
||||
type=str,
|
||||
default="chromium",
|
||||
help=
|
||||
"Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-editable",
|
||||
action="store_true",
|
||||
help="Install the project in editable mode (pip install -e .) for running tests"
|
||||
)
|
||||
deno_group = parser.add_mutually_exclusive_group()
|
||||
deno_group.add_argument("--install-deno", action="store_true", help="Install the Deno runtime (default behavior; kept for explicitness)")
|
||||
deno_group.add_argument("--no-deno", action="store_true", help="Skip installing Deno runtime (opt out)")
|
||||
parser.add_argument("--deno-version", type=str, default=None, help="Specific Deno version to install (e.g., v1.34.3)")
|
||||
parser.add_argument("--upgrade-pip", action="store_true", help="Upgrade pip/setuptools/wheel before installing requirements")
|
||||
deno_group.add_argument(
|
||||
"--install-deno",
|
||||
action="store_true",
|
||||
help="Install the Deno runtime (default behavior; kept for explicitness)"
|
||||
)
|
||||
deno_group.add_argument(
|
||||
"--no-deno",
|
||||
action="store_true",
|
||||
help="Skip installing Deno runtime (opt out)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deno-version",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Specific Deno version to install (e.g., v1.34.3)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upgrade-pip",
|
||||
action="store_true",
|
||||
help="Upgrade pip/setuptools/wheel before installing requirements"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
@@ -273,7 +309,9 @@ def main() -> int:
|
||||
print("'playwright' package not found; installing it via pip...")
|
||||
run([sys.executable, "-m", "pip", "install", "playwright"])
|
||||
|
||||
print("Installing Playwright browsers (this may download several hundred MB)...")
|
||||
print(
|
||||
"Installing Playwright browsers (this may download several hundred MB)..."
|
||||
)
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
except ValueError as exc:
|
||||
@@ -286,14 +324,30 @@ def main() -> int:
|
||||
|
||||
if args.upgrade_pip:
|
||||
print("Upgrading pip, setuptools, and wheel in local venv...")
|
||||
run([str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"])
|
||||
run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
"setuptools",
|
||||
"wheel"
|
||||
]
|
||||
)
|
||||
|
||||
if not args.skip_deps:
|
||||
req_file = repo_root / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
print(f"requirements.txt not found at {req_file}; skipping dependency installation.", file=sys.stderr)
|
||||
print(
|
||||
f"requirements.txt not found at {req_file}; skipping dependency installation.",
|
||||
file=sys.stderr
|
||||
)
|
||||
else:
|
||||
print(f"Installing Python dependencies into local venv from {req_file}...")
|
||||
print(
|
||||
f"Installing Python dependencies into local venv from {req_file}..."
|
||||
)
|
||||
run([str(venv_python), "-m", "pip", "install", "-r", str(req_file)])
|
||||
|
||||
if not args.no_playwright:
|
||||
@@ -301,7 +355,9 @@ def main() -> int:
|
||||
print("'playwright' package not installed in venv; installing it...")
|
||||
run([str(venv_python), "-m", "pip", "install", "playwright"])
|
||||
|
||||
print("Installing Playwright browsers (this may download several hundred MB)...")
|
||||
print(
|
||||
"Installing Playwright browsers (this may download several hundred MB)..."
|
||||
)
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
except ValueError as exc:
|
||||
@@ -321,20 +377,33 @@ def main() -> int:
|
||||
print("Verifying top-level 'CLI' import in venv...")
|
||||
try:
|
||||
import subprocess as _sub
|
||||
rc = _sub.run([str(venv_python), "-c", "import importlib; importlib.import_module('CLI')"], check=False)
|
||||
rc = _sub.run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-c",
|
||||
"import importlib; importlib.import_module('CLI')"
|
||||
],
|
||||
check=False
|
||||
)
|
||||
if rc.returncode == 0:
|
||||
print("OK: top-level 'CLI' is importable in the venv.")
|
||||
else:
|
||||
print("Top-level 'CLI' not importable; attempting to add repo path to venv site-packages via a .pth file...")
|
||||
cmd = [str(venv_python), "-c", (
|
||||
"import site, sysconfig\n"
|
||||
"out=[]\n"
|
||||
"try:\n out.extend(site.getsitepackages())\nexcept Exception:\n pass\n"
|
||||
"try:\n p = sysconfig.get_paths().get('purelib')\n if p:\n out.append(p)\nexcept Exception:\n pass\n"
|
||||
"seen=[]; res=[]\n"
|
||||
"for x in out:\n if x and x not in seen:\n seen.append(x); res.append(x)\n"
|
||||
"for s in res:\n print(s)\n"
|
||||
)]
|
||||
print(
|
||||
"Top-level 'CLI' not importable; attempting to add repo path to venv site-packages via a .pth file..."
|
||||
)
|
||||
cmd = [
|
||||
str(venv_python),
|
||||
"-c",
|
||||
(
|
||||
"import site, sysconfig\n"
|
||||
"out=[]\n"
|
||||
"try:\n out.extend(site.getsitepackages())\nexcept Exception:\n pass\n"
|
||||
"try:\n p = sysconfig.get_paths().get('purelib')\n if p:\n out.append(p)\nexcept Exception:\n pass\n"
|
||||
"seen=[]; res=[]\n"
|
||||
"for x in out:\n if x and x not in seen:\n seen.append(x); res.append(x)\n"
|
||||
"for s in res:\n print(s)\n"
|
||||
)
|
||||
]
|
||||
out = _sub.check_output(cmd, text=True).strip().splitlines()
|
||||
site_dir = None
|
||||
for sp in out:
|
||||
@@ -342,7 +411,9 @@ def main() -> int:
|
||||
site_dir = Path(sp)
|
||||
break
|
||||
if site_dir is None:
|
||||
print("Could not determine venv site-packages directory; skipping .pth fallback")
|
||||
print(
|
||||
"Could not determine venv site-packages directory; skipping .pth fallback"
|
||||
)
|
||||
else:
|
||||
pth_file = site_dir / "medeia_repo.pth"
|
||||
if pth_file.exists():
|
||||
@@ -356,16 +427,29 @@ def main() -> int:
|
||||
else:
|
||||
with pth_file.open("w", encoding="utf-8") as fh:
|
||||
fh.write(str(repo_root) + "\n")
|
||||
print(f"Wrote .pth adding repo root to venv site-packages: {pth_file}")
|
||||
print(
|
||||
f"Wrote .pth adding repo root to venv site-packages: {pth_file}"
|
||||
)
|
||||
|
||||
# Re-check whether CLI can be imported now
|
||||
rc2 = _sub.run([str(venv_python), "-c", "import importlib; importlib.import_module('CLI')"], check=False)
|
||||
rc2 = _sub.run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-c",
|
||||
"import importlib; importlib.import_module('CLI')"
|
||||
],
|
||||
check=False
|
||||
)
|
||||
if rc2.returncode == 0:
|
||||
print("Top-level 'CLI' import works after adding .pth")
|
||||
else:
|
||||
print("Adding .pth did not make top-level 'CLI' importable; consider creating an egg-link or checking the venv.")
|
||||
print(
|
||||
"Adding .pth did not make top-level 'CLI' importable; consider creating an egg-link or checking the venv."
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"Warning: failed to verify or modify site-packages for top-level CLI: {exc}")
|
||||
print(
|
||||
f"Warning: failed to verify or modify site-packages for top-level CLI: {exc}"
|
||||
)
|
||||
|
||||
# Optional: install Deno runtime (default: install unless --no-deno is passed)
|
||||
install_deno_requested = True
|
||||
@@ -436,6 +520,7 @@ python -m medeia_macina.cli_entry @args
|
||||
)
|
||||
try:
|
||||
# non-interactive mode, which we use so "python ./scripts/bootstrap.py" just
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -501,8 +586,15 @@ python -m medeia_macina.cli_entry @args
|
||||
"$bin = '{bin}';"
|
||||
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
|
||||
"if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}"
|
||||
).format(bin=str_bin.replace('\\','\\\\'))
|
||||
subprocess.run(["powershell","-NoProfile","-Command", ps_cmd], check=False)
|
||||
).format(bin=str_bin.replace('\\',
|
||||
'\\\\'))
|
||||
subprocess.run(
|
||||
["powershell",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
ps_cmd],
|
||||
check=False
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -510,7 +602,10 @@ python -m medeia_macina.cli_entry @args
|
||||
|
||||
else:
|
||||
# POSIX
|
||||
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(home / ".local/bin")))
|
||||
user_bin = Path(
|
||||
os.environ.get("XDG_BIN_HOME",
|
||||
str(home / ".local/bin"))
|
||||
)
|
||||
user_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mm_sh = user_bin / "mm"
|
||||
@@ -615,7 +710,10 @@ python -m medeia_macina.cli_entry @args
|
||||
return 0
|
||||
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Error: command failed with exit {exc.returncode}: {exc}", file=sys.stderr)
|
||||
print(
|
||||
f"Error: command failed with exit {exc.returncode}: {exc}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return int(exc.returncode or 1)
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
print(f"Unexpected error: {exc}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user