plesty.server.services.installer

Creates the environments the DeviceManager keeps books on.

  • PyPI source — one relocatable uv venv per (package, version) at <home>/packages/<package>/<version>/. An unpinned spec is installed into a staging directory first and moved under the version it resolved to, so the layout never has to guess what “newest” was.

  • Git source — one checkout per (package, ref) at <home>/repos/<package>/<ref>/ with uv sync’s .venv as the environment. A checkout is what makes the developer loop possible from afar: Installer.update() pulls and re-syncs, and the repository’s own tests/field_test.py and plesty check run right there.

uv (and git for repositories) are the only tools used. Both are driven through an injectable Runner, so tests never spawn them, and every line they print goes to an Echo — the job log when run as a job. Neither has to be on PATH: uv is looked up there, git also where Windows puts it and, failing that, installed for the bench (plesty.server.services.gitbin).

Attributes

CHECKOUT_VENV

PYTHON_SPEC

STAGING_PREFIX

PRERELEASE_HINT

UV_DIRECTORIES

UV_DIRECTORIES_POSIX

UV_DIRECTORIES_WINDOWS

GIT_ADVICE

Echo

Runner

Exceptions

InstallError

uv/git failed, or the environment is not in the shape expected.

Classes

Installer

Install, update and remove device environments; records them in the manager.

Functions

_same_dir(→ str)

path in the form that compares equal for two spellings of one directory.

_remove_env(→ None)

Delete the environment of package, saying why when the host will not.

default_runner(→ subprocess.CompletedProcess[str])

Run cmd, streaming its merged output to echo as it arrives.

_print(→ None)

Default Echo: stdout, flushed so a tailed job log sees it.

_canonical(→ str)

PEP 503 normalisation, enough to compare distribution names.

Module Contents

plesty.server.services.installer.CHECKOUT_VENV = '.venv'
plesty.server.services.installer.PYTHON_SPEC = '>=3.12'
plesty.server.services.installer.STAGING_PREFIX = '.staging-'
plesty.server.services.installer.PRERELEASE_HINT = 'pre-release'
plesty.server.services.installer.UV_DIRECTORIES = ('~/.local/bin', '~/.cargo/bin')
plesty.server.services.installer.UV_DIRECTORIES_POSIX = ('/opt/homebrew/bin', '/usr/local/bin')
plesty.server.services.installer.UV_DIRECTORIES_WINDOWS = ('%LOCALAPPDATA%\\Microsoft\\WindowsApps', '%USERPROFILE%\\scoop\\shims',...
plesty.server.services.installer.GIT_ADVICE = 'install it (macOS: xcode-select --install; Debian/Ubuntu: apt install git) or declare the...
plesty.server.services.installer.Echo
plesty.server.services.installer.Runner
plesty.server.services.installer._same_dir(path: str) str

path in the form that compares equal for two spellings of one directory.

Parameters:

path (str)

Return type:

str

plesty.server.services.installer._remove_env(env: pathlib.Path, package: str) None

Delete the environment of package, saying why when the host will not.

Windows keeps a file open by a running process undeletable, so replacing the environment of a device that is still serving fails there where it succeeds on POSIX. The bare PermissionError names a file deep inside a venv; this names the device instead.

Raises:

InstallError – When the environment is in use.

Parameters:
  • env (pathlib.Path)

  • package (str)

Return type:

None

exception plesty.server.services.installer.InstallError

Bases: RuntimeError

uv/git failed, or the environment is not in the shape expected.

Initialize self. See help(type(self)) for accurate signature.

plesty.server.services.installer.default_runner(cmd: list[str], echo: Echo | None = None) subprocess.CompletedProcess[str]

Run cmd, streaming its merged output to echo as it arrives.

stderr is folded into stdout so the order the tool wrote them in survives; the text is returned as well, so a failure still carries its own detail.

Parameters:
  • cmd (list[str])

  • echo (Echo | None)

Return type:

subprocess.CompletedProcess[str]

class plesty.server.services.installer.Installer(home: plesty.server.model.home.Home, manager: plesty.server.model.device_manager.DeviceManager | None = None, uv: str | None = None, git: str | None = None, runner: Runner | None = None, echo: Echo | None = None)

Install, update and remove device environments; records them in the manager.

Bind the installer to a home.

Parameters:
  • home (plesty.server.model.home.Home) – The bench home.

  • manager (plesty.server.model.device_manager.DeviceManager | None) – The inventory to record into; a fresh one on home by default.

  • uv (str | None) – Path of the uv executable; looked up on PATH by default.

  • git (str | None) – Path of the git executable; found (or installed) by default.

  • runner (Runner | None) – Executes a command line — injected by tests.

  • echo (Echo | None) – Where each line uv and git write goes while they run. Defaults to stdout, which is the job log when run as a job.

home
manager
_uv = None
_git = None
_run
_echo: Echo
property uv: str

The uv executable; raises InstallError when the bench has none.

Return type:

str

_find_uv() str

PATH first, then where uv installs itself.

Raises:

InstallError – When there is no uv to be found.

Return type:

str

property git: str

The git executable, installing one for the bench when it has none.

Raises:

InstallError – When the bench has no git and cannot be given one.

Return type:

str

_on_path(git: str) str

Put git’s directory on PATH for this process and its children.

Calling git by absolute path is enough for the installer’s own clone and checkout — and not enough for the install. A module’s dependencies may be git+https URLs, and the uv this spawns to resolve them looks for git on PATH and nowhere else: on the very bench this feature exists for, one with no git at all, the install would fetch a portable git, clone with it, and then fail in uv sync with “Git executable not found”. The same holds for a git found under %ProgramFiles% but absent from PATH.

Parameters:

git (str)

Return type:

str

_find_or_install_git() str

Locate git, or fetch a portable one (Windows); explains itself either way.

Return type:

str

python(installed: plesty.server.model.device_manager.InstalledVersion) pathlib.Path

The interpreter that runs installed.

Parameters:

installed (plesty.server.model.device_manager.InstalledVersion)

Return type:

pathlib.Path

install(spec: plesty.server.model.device.DeviceSpec, force: bool = False, job_id: str | None = None) plesty.server.model.device_manager.InstalledVersion

Make sure an environment serving spec exists; returns its record.

Parameters:
  • spec (plesty.server.model.device.DeviceSpec) – What to install: package plus a pinned version (PyPI) or a repository and ref (git checkout).

  • force (bool) – Reinstall even when a satisfying environment is on record.

  • job_id (str | None) – The job this runs as, stored on the record.

Raises:

InstallError – When uv/git fail or the package did not end up installed.

Return type:

plesty.server.model.device_manager.InstalledVersion

_install_the_tag_instead(spec: plesty.server.model.device.DeviceSpec, job_id: str | None, failure: InstallError) plesty.server.model.device_manager.InstalledVersion

After a failed release install, install the same version from the repository.

A module is on the hub before it is on PyPI: the registry knows the release from its tag, and the package reaches the index only once the release job has run — so “that release is not on the index” is a routine state of a young module, not a broken bench. The repository holds the same version under v<version>, which is the ref a spec with a version already resolves to.

Raises:

InstallError – When no repository is known for the package, or the repository has no such tag either — carrying both reasons.

Parameters:
Return type:

plesty.server.model.device_manager.InstalledVersion

update(installed: plesty.server.model.device_manager.InstalledVersion, ref: str | None = None, job_id: str | None = None) plesty.server.model.device_manager.InstalledVersion

Bring installed up to date; returns the (possibly new) record.

Git checkout: fetch, check out ref (default: the recorded ref), fast-forward, uv sync. PyPI: reinstall the same version.

Parameters:
Return type:

plesty.server.model.device_manager.InstalledVersion

uninstall(installed: plesty.server.model.device_manager.InstalledVersion) bool

Delete the environment of installed and forget it.

Returns:

True when a directory was removed.

Parameters:

installed (plesty.server.model.device_manager.InstalledVersion)

Return type:

bool

_install_release(spec: plesty.server.model.device.DeviceSpec, job_id: str | None) plesty.server.model.device_manager.InstalledVersion
Parameters:
Return type:

plesty.server.model.device_manager.InstalledVersion

_install_checkout(spec: plesty.server.model.device.DeviceSpec, job_id: str | None) plesty.server.model.device_manager.InstalledVersion
Parameters:
Return type:

plesty.server.model.device_manager.InstalledVersion

_uv_call(args: list[str], what: str, quiet: bool = False) subprocess.CompletedProcess[str]
Parameters:
  • args (list[str])

  • what (str)

  • quiet (bool)

Return type:

subprocess.CompletedProcess[str]

_uv_resolve(args: list[str], what: str) subprocess.CompletedProcess[str]

Run a resolving uv command, retrying once with pre-releases allowed.

A hub module under development depends on a .dev release of plesty-lib, and uv considers only stable versions unless it is told otherwise — it fails with a hint naming the pre-release. Allowing them from the start would pull a pre-release into every install that has a stable answer, so it is the retry, not the rule.

Parameters:
  • args (list[str])

  • what (str)

Return type:

subprocess.CompletedProcess[str]

_git_call(args: list[str], what: str, quiet: bool = False) subprocess.CompletedProcess[str]
Parameters:
  • args (list[str])

  • what (str)

  • quiet (bool)

Return type:

subprocess.CompletedProcess[str]

_call(cmd: list[str], what: str, quiet: bool = False) subprocess.CompletedProcess[str]

Run cmd; quiet keeps a query’s output out of the log (it is read, not watched).

Parameters:
  • cmd (list[str])

  • what (str)

  • quiet (bool)

Return type:

subprocess.CompletedProcess[str]

_installed_version(python: pathlib.Path, package: str) str | None
Parameters:
  • python (pathlib.Path)

  • package (str)

Return type:

str | None

_python_version(python: pathlib.Path) str
Parameters:

python (pathlib.Path)

Return type:

str

plesty.server.services.installer._print(line: str) None

Default Echo: stdout, flushed so a tailed job log sees it.

Parameters:

line (str)

Return type:

None

plesty.server.services.installer._canonical(name: str) str

PEP 503 normalisation, enough to compare distribution names.

Parameters:

name (str)

Return type:

str