plesty.server.services.installer
Creates the environments the DeviceManager keeps books on.
PyPI source — one relocatable
uvvenv 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>/withuv sync’s.venvas the environment. A checkout is what makes the developer loop possible from afar:Installer.update()pulls and re-syncs, and the repository’s owntests/field_test.pyruns right there. The sync is--no-dev, so the authoring toolchain (plesty check, pytest) is not installed with it — seeSYNC_ARGS.
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
Exceptions
|
Classes
What a repository on disk says it is, in its |
|
Install, update and remove device environments; records them in the manager. |
Functions
|
The |
|
path in the form that compares equal for two spellings of one directory. |
|
Delete the environment of package, saying why when the host will not. |
|
Read what the repository at root is, and refuse what a bench cannot run. |
|
What the module at root says it is — |
|
Run cmd, streaming its merged output to echo as it arrives. |
|
Default |
|
Whether uv failed because the module will not run on the Python it was given. |
|
PEP 503 normalisation, enough to compare distribution names. |
Module Contents
- plesty.server.services.installer.CHECKOUT_VENV = '.venv'
- plesty.server.services.installer.SYNC_ARGS = ['sync', '--no-dev']
- plesty.server.services.installer.REPORTS_DIR = 'reports'
- plesty.server.services.installer.PYTHON_SPEC = '>=3.12'
- plesty.server.services.installer.CONSOLE_PYTHON
- plesty.server.services.installer.PYTHON_REFUSED_HINTS = ("incompatible with the project's python requirement", 'does not satisfy')
- plesty.server.services.installer.STAGING_PREFIX = '.staging-'
- plesty.server.services.installer.PRERELEASE_HINT = 'pre-release'
- plesty.server.services.installer.BROKEN_VENV_HINT = 'not a valid python environment'
- plesty.server.services.installer.VENV_CONFIG = 'pyvenv.cfg'
- plesty.server.services.installer.SCANNER_HINT = 'failed to update windows pe resources'
- plesty.server.services.installer.SCANNER_BACKOFF = (2.0, 5.0, 15.0)
- 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.find_uv() str
The
uvexecutable:PATHfirst, then where uv installs itself.- Raises:
InstallError – When there is no uv to be found.
- Return type:
str
- 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
PermissionErrornames 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:
RuntimeErroruv/gitfailed, or the environment is not in the shape expected.Initialize self. See help(type(self)) for accurate signature.
- plesty.server.services.installer.LOADABLE_TYPES
- class plesty.server.services.installer.LocalModule
What a repository on disk says it is, in its
pyproject.toml.- Variables:
package –
[project] name.module_type –
[tool.plesty] module_type—device,experiment, …description –
[project] description.
- package: str
- module_type: str
- description: str = ''
- property category: str
devicesorexperiments.- Type:
The catalogue category it belongs to
- Return type:
str
- plesty.server.services.installer.local_module(root: pathlib.Path) LocalModule
Read what the repository at root is, and refuse what a bench cannot run.
- Raises:
InstallError – When there is no
pyproject.toml, it names no project, or it is not a device or experiment module.- Parameters:
root (pathlib.Path)
- Return type:
- plesty.server.services.installer._declared_kind(root: pathlib.Path) dict[str, str]
What the module at root says it is —
module_typeanddescription— when it says.Read from its
pyproject.tomllikelocal_module(), but never refusing: a checkout that says nothing is still installed, and simply records nothing.- Parameters:
root (pathlib.Path)
- Return type:
dict[str, str]
- 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
uvexecutable; looked up onPATHby default.git (str | None) – Path of the
gitexecutable; found (or installed) by default.runner (Runner | None) – Executes a command line — injected by tests.
echo (Echo | None) – Where each line
uvandgitwrite goes while they run. Defaults to stdout, which is the job log when run as a job.
- home
- manager
- _uv = None
- _git = None
- _run
- property uv: str
The
uvexecutable; raisesInstallErrorwhen the bench has none.- Return type:
str
- property git: str
The
gitexecutable, 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
PATHfor 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+httpsURLs, and theuvthis spawns to resolve them looks for git onPATHand 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 inuv syncwith “Git executable not found”. The same holds for a git found under%ProgramFiles%but absent fromPATH.- 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
- local_branches(root: pathlib.Path) list[str]
The local branches of the repository at root; empty when it has none or no git.
- Parameters:
root (pathlib.Path)
- Return type:
list[str]
- local_branch(root: pathlib.Path) str
The branch the repository at root is on;
""when detached or not git.- Parameters:
root (pathlib.Path)
- 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
- load_local(folder: str | os.PathLike[str], ref: str | None = None) plesty.server.model.device_manager.InstalledVersion
Put a repository already on disk onto the bench, where it lives.
Nothing is copied or moved: the bench records the folder, what the module is, the version its environment holds and the commit it is on, under
<home>/packages/<package>/, and runs it with the folder’s own.venv— the one the developer’suv runuses. When there is none yet,uv syncmakes it; one that is there is never re-synced, so the developer’s tools in it stay.Loading the same folder again refreshes the record: a new commit, a bumped version.
- Parameters:
folder (str | os.PathLike[str]) – The repository’s root, holding its
pyproject.toml.ref (str | None) – A branch to check out in the folder first; the one it is on when
None. Refused over uncommitted changes, which a switch would carry onto the other branch.
- Returns:
The record.
- Raises:
InstallError – When the folder is not a module this bench can run, the branch cannot be checked out, or the environment cannot be made.
- Return type:
- _seeded(installed: plesty.server.model.device_manager.InstalledVersion) plesty.server.model.device_manager.InstalledVersion
Give the checkout of installed a
.envline for each variable its setup names.An experiment’s
setup.yamlnames the variables holding where each bench is, its token and where it sees the shared disk; the.envbeside it holds their values, set in the console. A value already there is kept. A module with no setup is left as it is.- Parameters:
installed (plesty.server.model.device_manager.InstalledVersion)
- Return type:
- 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/gitfail or the package did not end up installed.- Return type:
- _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:
job_id (str | None)
failure (InstallError)
- Return type:
- 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. Local repository: read it again — it is the developer’s to pull and sync.- Parameters:
installed (plesty.server.model.device_manager.InstalledVersion) – The environment to refresh.
ref (str | None) – Branch, tag or commit to move a checkout to.
job_id (str | None) – The job this runs as.
- Return type:
- uninstall(installed: plesty.server.model.device_manager.InstalledVersion) bool
Delete the environment of installed and forget it.
- Returns:
Truewhen 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:
job_id (str | None)
- Return type:
- _install_checkout(spec: plesty.server.model.device.DeviceSpec, job_id: str | None) plesty.server.model.device_manager.InstalledVersion
- Parameters:
job_id (str | None)
- Return type:
- _uv_call(args: list[str], what: str, quiet: bool = False, venv: pathlib.Path | None = None, creates: pathlib.Path | None = None) subprocess.CompletedProcess[str]
Run
uv, waiting out a scanner that is holding a file it just wrote.uv writes every console script as an
.exeand reopens it at once to patch its resources. An on-access scanner opening that new executable first makes the reopen fail, and uv reports the whole wheel as uninstallable — so an install dies on whichever dependency ships the most scripts, which is why the wheel named in the error moves around (PySide6 on one bench, Sphinx on another) and why it is nothing to do with the module being installed.The wait is the fix: nothing here can make the scanner let go, and the file is fine once it does. Only this failure is retried, and only a few times — a scanner that never releases is a bench problem, and saying so beats retrying until the job looks hung.
A command that builds venv and was interrupted can leave it without
VENV_CONFIG; the retry then failed “not a valid Python environment” every time (#45). Such a.venvis removed before each retry, as_sync_checkout()removes one before its first attempt.- Parameters:
args (list[str])
what (str)
quiet (bool)
venv (pathlib.Path | None)
creates (pathlib.Path | None)
- Return type:
subprocess.CompletedProcess[str]
- _clear_bench_reports(checkout: pathlib.Path) None
Move the bench’s own
reports/files out of the way of the commits a pull brings.A field test writes its report into the checkout, and the procedure then commits that same file to the module. The next pull refused to overwrite the bench’s copy — untracked, or changed by a later run — so the device kept its old code until someone deleted the file by hand on the bench PC (#37). A copy identical to the incoming one is dropped; any other is kept beside it as
<name>.bench-<time><suffix>, said in the log, so no result of a run is lost.Only
reports/is touched — the folder the bench writes. Anything else in the way is the developer’s and still stops the pull.- Parameters:
checkout (pathlib.Path)
- Return type:
None
- _sync_checkout(checkout: pathlib.Path, package: str) None
uv sync --no-devcheckout, rebuilding its.venvif that stops it.The module itself is rebuilt every time (
--reinstall-package). It is installed editable, so its code follows the pull anyway, but uv keeps the version metadata of the first build: after an update the bench,listand a field-test report all named the commit it was first installed from (#36).The environment holds what the device needs to run and nothing else.
tests/field_test.pyis unaffected — it importsplesty.liband the module, never pytest. What no longer works in a fresh checkout is the authoring toolchain:plesty check,plesty initandpytestin the Terminal tab have nothing to run, becauseplesty-sdklives in the dev group. Someone who wants them canuv syncin the checkout by hand and get them back.uv refuses a
.venvthat exists without an interpreter in it rather than replacing it, and nothing removes it, so every later sync fails the same way: one interrupted install wedges the module until someone deletes the directory by hand — which is what a bench operator has no console for, and what the message does not tell them to do. The interruption can be almost anything (a stop mid-install, a scanner taking the interpreter, an uv-managed Python that went away), so this recovers from the state rather than from any one cause.Deleting it is safe: uv owns the directory and rebuilds it from the lock. Only this failure is retried — any other sync error is the module’s own and is raised unchanged.
A
.venvwithoutVENV_CONFIGis removed before the sync, and one still without it afterwards fails the install: its interpreter is the shared one, and an install that “succeeds” into it is worse than one that stops.- Parameters:
checkout (pathlib.Path)
package (str)
- Return type:
None
- _uv_resolve(args: list[str], what: str, venv: pathlib.Path | None = None) subprocess.CompletedProcess[str]
Run a resolving
uvcommand, retrying once with pre-releases allowed.A hub module under development depends on a
.devrelease 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)
venv (pathlib.Path | None)
- 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._python_refused(exc: InstallError) bool
Whether uv failed because the module will not run on the Python it was given.
- Parameters:
exc (InstallError)
- Return type:
bool
- plesty.server.services.installer._canonical(name: str) str
PEP 503 normalisation, enough to compare distribution names.
- Parameters:
name (str)
- Return type:
str