plesty.server.services.installer ================================ .. py:module:: plesty.server.services.installer .. autoapi-nested-parse:: Creates the environments the :class:`~plesty.server.model.DeviceManager` keeps books on. * **PyPI source** — one relocatable ``uv`` venv per ``(package, version)`` at ``/packages///``. 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 ``/repos///`` with ``uv sync``'s ``.venv`` as the environment. A checkout is what makes the developer loop possible from afar: :meth:`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 :data:`Runner`, so tests never spawn them, and every line they print goes to an :data:`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 (:mod:`plesty.server.services.gitbin`). Attributes ---------- .. autoapisummary:: plesty.server.services.installer.CHECKOUT_VENV plesty.server.services.installer.PYTHON_SPEC plesty.server.services.installer.STAGING_PREFIX plesty.server.services.installer.PRERELEASE_HINT plesty.server.services.installer.UV_DIRECTORIES plesty.server.services.installer.UV_DIRECTORIES_POSIX plesty.server.services.installer.UV_DIRECTORIES_WINDOWS plesty.server.services.installer.GIT_ADVICE plesty.server.services.installer.Echo plesty.server.services.installer.Runner Exceptions ---------- .. autoapisummary:: plesty.server.services.installer.InstallError Classes ------- .. autoapisummary:: plesty.server.services.installer.Installer Functions --------- .. autoapisummary:: plesty.server.services.installer._same_dir plesty.server.services.installer._remove_env plesty.server.services.installer.default_runner plesty.server.services.installer._print plesty.server.services.installer._canonical Module Contents --------------- .. py:data:: CHECKOUT_VENV :value: '.venv' .. py:data:: PYTHON_SPEC :value: '>=3.12' .. py:data:: STAGING_PREFIX :value: '.staging-' .. py:data:: PRERELEASE_HINT :value: 'pre-release' .. py:data:: UV_DIRECTORIES :value: ('~/.local/bin', '~/.cargo/bin') .. py:data:: UV_DIRECTORIES_POSIX :value: ('/opt/homebrew/bin', '/usr/local/bin') .. py:data:: UV_DIRECTORIES_WINDOWS :value: ('%LOCALAPPDATA%\\Microsoft\\WindowsApps', '%USERPROFILE%\\scoop\\shims',... .. py:data:: GIT_ADVICE :value: 'install it (macOS: xcode-select --install; Debian/Ubuntu: apt install git) — or declare the... .. py:data:: Echo .. py:data:: Runner .. py:function:: _same_dir(path: str) -> str *path* in the form that compares equal for two spellings of one directory. .. py:function:: _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. .. py:exception:: InstallError Bases: :py:obj:`RuntimeError` ``uv``/``git`` failed, or the environment is not in the shape expected. Initialize self. See help(type(self)) for accurate signature. .. py:function:: 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. .. py:class:: 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. :param home: The bench home. :param manager: The inventory to record into; a fresh one on *home* by default. :param uv: Path of the ``uv`` executable; looked up on ``PATH`` by default. :param git: Path of the ``git`` executable; found (or installed) by default. :param runner: Executes a command line — injected by tests. :param echo: Where each line ``uv`` and ``git`` write goes while they run. Defaults to stdout, which is the job log when run as a job. .. py:attribute:: home .. py:attribute:: manager .. py:attribute:: _uv :value: None .. py:attribute:: _git :value: None .. py:attribute:: _run .. py:attribute:: _echo :type: Echo .. py:property:: uv :type: str The ``uv`` executable; raises :class:`InstallError` when the bench has none. .. py:method:: _find_uv() -> str ``PATH`` first, then where uv installs itself. :raises InstallError: When there is no uv to be found. .. py:property:: git :type: 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. .. py:method:: _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``. .. py:method:: _find_or_install_git() -> str Locate git, or fetch a portable one (Windows); explains itself either way. .. py:method:: python(installed: plesty.server.model.device_manager.InstalledVersion) -> pathlib.Path The interpreter that runs *installed*. .. py:method:: 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. :param spec: What to install: package plus a pinned version (PyPI) or a repository and ref (git checkout). :param force: Reinstall even when a satisfying environment is on record. :param job_id: The job this runs as, stored on the record. :raises InstallError: When ``uv``/``git`` fail or the package did not end up installed. .. py:method:: _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``, 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. .. py:method:: 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. :param installed: The environment to refresh. :param ref: Branch, tag or commit to move a checkout to. :param job_id: The job this runs as. .. py:method:: uninstall(installed: plesty.server.model.device_manager.InstalledVersion) -> bool Delete the environment of *installed* and forget it. :returns: ``True`` when a directory was removed. .. py:method:: _install_release(spec: plesty.server.model.device.DeviceSpec, job_id: str | None) -> plesty.server.model.device_manager.InstalledVersion .. py:method:: _install_checkout(spec: plesty.server.model.device.DeviceSpec, job_id: str | None) -> plesty.server.model.device_manager.InstalledVersion .. py:method:: _uv_call(args: list[str], what: str, quiet: bool = False) -> subprocess.CompletedProcess[str] .. py:method:: _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. .. py:method:: _git_call(args: list[str], what: str, quiet: bool = False) -> subprocess.CompletedProcess[str] .. py:method:: _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). .. py:method:: _installed_version(python: pathlib.Path, package: str) -> str | None .. py:method:: _python_version(python: pathlib.Path) -> str .. py:function:: _print(line: str) -> None Default :data:`Echo`: stdout, flushed so a tailed job log sees it. .. py:function:: _canonical(name: str) -> str PEP 503 normalisation, enough to compare distribution names.