plesty.server.services
Services: the model’s side effects on the host.
Everything that touches the operating system — uv and git for
installing, subprocesses for running servers and jobs — lives here. Services
take model objects in and hand model objects back; presenters drive them and
never call the tools directly, so a service is the one place a tool’s quirks
are handled and the one seam tests replace.
Submodules
Attributes
Exceptions
No |
|
|
|
The catalogue could not be fetched or read. |
|
|
|
|
|
|
|
A start or stop request could not be carried out. |
Classes
Install, update and remove device environments; records them in the manager. |
|
Serve |
|
The hub catalogue at one index URL. |
|
Start, observe and cancel jobs under a |
|
Launch and control device servers under a |
Functions
|
Where the bench keeps its own git (whether or not one is there yet). |
|
A usable |
|
Unpack a portable git into |
|
Run cmd, streaming its merged output to echo as it arrives. |
|
Run agent on a daemon thread; returns the thread and its stop event. |
|
GET url with the standard library (the bench may have no |
|
The newest release of package on PyPI, or |
|
Ask the server on port to describe itself. |
|
Send one request to the server on port and return its reply. |
Package Contents
Bases:
RuntimeErrorNo
giton the bench, and none could be installed.Initialize self. See help(type(self)) for accurate signature.
- plesty.server.services.bench_git(home: plesty.server.model.home.Home) pathlib.Path
Where the bench keeps its own git (whether or not one is there yet).
- Parameters:
- Return type:
pathlib.Path
- plesty.server.services.find_git(home: plesty.server.model.home.Home) str | None
A usable
git:PATHfirst, then the usual installs, then the bench’s own.- Parameters:
home (plesty.server.model.home.Home) – The bench home, which may hold a git of its own.
- Returns:
The executable, or
Nonewhen the bench has none.- Return type:
str | None
- plesty.server.services.install_git(home: plesty.server.model.home.Home, echo: Echo | None = None, fetch: Fetcher | None = None) str
Unpack a portable git into
<home>/tools/git; returns the executable.Windows only: it is the platform where a bench routinely has no git and no way to install one. The archive is verified against the pinned digest before anything is unpacked — a mismatch is an error, not a warning.
- Parameters:
home (plesty.server.model.home.Home) – The bench home to install into.
echo (Echo | None) – Where progress lines go.
fetch (Fetcher | None) – Reads the archive URL; injected by tests.
- Raises:
GitUnavailable – Not Windows, unknown architecture, download failed, digest mismatch, or the archive held no
git.- Return type:
str
- plesty.server.services.CHECKOUT_VENV = '.venv'
- plesty.server.services.PYTHON_SPEC = '>=3.12'
- plesty.server.services.Echo
- class plesty.server.services.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
- _find_uv() str
PATHfirst, then where uv installs itself.- Raises:
InstallError – When there is no uv to be found.
- 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
- 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/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.- 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) 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
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)
- 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
- exception plesty.server.services.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.Runner
- plesty.server.services.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.Agent(home: plesty.server.model.home.Home, fleet_path: str | os.PathLike[str] | None = None, bind: str = DEFAULT_BIND, token: str | None = None, workers: int = 4)
Serve
plesty.benchclients for one bench.Configure the agent.
- Parameters:
home (plesty.server.model.home.Home) – The bench home every command runs against.
fleet_path (str | os.PathLike[str] | None) – Fleet file passed to every command (
-f), orNone.bind (str) – ZMQ endpoint to bind.
token (str | None) – Shared secret;
NonereadsPLESTY_BENCH_TOKEN; empty means no check (a bench on a trusted network).workers (int) – Concurrent requests.
- home
- fleet_path
- bind = 'tcp://*:5550'
- token
- workers = 4
- bound: str | None = None
- served = 0
- handle(message: Any) dict[str, Any]
Answer one decoded request (pure; no socket involved).
- Parameters:
message (Any)
- Return type:
dict[str, Any]
- description() dict[str, Any]
What
describereturns.- Return type:
dict[str, Any]
- exec(argv: collections.abc.Sequence[Any]) dict[str, Any]
Run
plesty-server <argv>in-process against this bench; capture everything.- Parameters:
argv (collections.abc.Sequence[Any])
- Return type:
dict[str, Any]
- fetch(path: str) dict[str, Any]
Read a file under the bench home; path is relative to it.
- Parameters:
path (str)
- Return type:
dict[str, Any]
- serve(stop: threading.Event, poll_ms: int = 100, max_replies: int | None = None) int
Bind and answer requests until stop is set; returns the number answered.
- Parameters:
stop (threading.Event) – Set to end the loop.
poll_ms (int) – Socket poll interval.
max_replies (int | None) – Stop once this many replies went out (tests).
- Return type:
int
- plesty.server.services.serve_in_thread(agent: Agent, stop: threading.Event | None = None) tuple[threading.Thread, threading.Event]
Run agent on a daemon thread; returns the thread and its stop event.
- Parameters:
agent (Agent)
stop (threading.Event | None)
- Return type:
tuple[threading.Thread, threading.Event]
- class plesty.server.services.Catalogue(index_url: str | None = None, fetch: Fetcher | None = None)
The hub catalogue at one index URL.
Bind to an index.
- Parameters:
index_url (str | None) –
/api/modules/endpoint; the public hub by default.fetch (Fetcher | None) – Reads a URL; injected by tests.
- index_url = 'https://hub.plesty.net/api/modules/'
- _fetch
- _cache: list[plesty.server.model.catalogue.CatalogueEntry] | None = None
- entries(refresh: bool = False) list[plesty.server.model.catalogue.CatalogueEntry]
Every module the index lists (fetched once, then cached).
- Raises:
CatalogueError – When the index cannot be fetched or is not JSON.
- Parameters:
refresh (bool)
- Return type:
- search(query: str = '', vendor: str = '', category: str = 'devices', standard: str = REQUIRED_STANDARD) list[plesty.server.model.catalogue.CatalogueEntry]
Entries matching query, filtered by vendor, category and standard (
""= any).- Parameters:
query (str)
vendor (str)
category (str)
standard (str)
- Return type:
- vendors(category: str = 'devices') list[str]
The vendor groups present in category.
- Parameters:
category (str)
- Return type:
list[str]
- lookup(package: str) plesty.server.model.catalogue.CatalogueEntry | None
The entry named package (any category, any standard), or
None.- Parameters:
package (str)
- Return type:
- exception plesty.server.services.CatalogueError
Bases:
RuntimeErrorThe catalogue could not be fetched or read.
Initialize self. See help(type(self)) for accurate signature.
- plesty.server.services.http_fetch(url: str, timeout: float = 15.0) bytes
GET url with the standard library (the bench may have no
requests).- Parameters:
url (str)
timeout (float)
- Return type:
bytes
- plesty.server.services.pypi_release(package: str, fetch: Fetcher | None = None) str | None
The newest release of package on PyPI, or
Nonewhen it has none there.- Parameters:
package (str) – Distribution name.
fetch (Fetcher | None) – Reads a URL; injected by tests.
- Return type:
str | None
- class plesty.server.services.JobRunner(home: plesty.server.model.home.Home, store: plesty.server.model.job.JobStore | None = None)
Start, observe and cancel jobs under a
Home.Bind to a home.
- Parameters:
home (plesty.server.model.home.Home) – The bench home.
store (plesty.server.model.job.JobStore | None) – The record store; a fresh one on home by default.
- home
- store
- exit_file(job_id: str) pathlib.Path
Where the launcher records the exit code of job_id.
- Parameters:
job_id (str)
- Return type:
pathlib.Path
- start(name: str, kind: str, command: list[str], cwd: str | os.PathLike[str], env: collections.abc.Mapping[str, str] | None = None, detail: collections.abc.Mapping[str, Any] | None = None) plesty.server.model.job.Job
Launch command as a job and return its record.
- Parameters:
name (str) – Device instance the job belongs to.
kind (str) – Job kind, part of the id.
command (list[str]) – The command line.
cwd (str | os.PathLike[str]) – Working directory.
env (collections.abc.Mapping[str, str] | None) – Environment on top of the bench’s own.
detail (collections.abc.Mapping[str, Any] | None) – Kind-specific data stored on the record.
- Raises:
JobError – When the command cannot be launched.
- Return type:
- get(job_id: str) plesty.server.model.job.Job | None
The job job_id with its state brought up to date, or
None.- Parameters:
job_id (str)
- Return type:
plesty.server.model.job.Job | None
- require(job_id: str) plesty.server.model.job.Job
Like
get()but raisesJobErrorwhen unknown.- Parameters:
job_id (str)
- Return type:
- list(name: str | None = None, kind: str | None = None) list[plesty.server.model.job.Job]
Jobs on record, newest first, brought up to date.
- Parameters:
name (str | None)
kind (str | None)
- Return type:
- wait(job_id: str, timeout: float | None = None, poll: float = 0.25) plesty.server.model.job.Job
Block until the job finishes (or timeout seconds pass); returns it.
- Parameters:
job_id (str)
timeout (float | None)
poll (float)
- Return type:
- cancel(job_id: str) bool
Kill a running job (its whole process tree);
Truewhen it was running.- Parameters:
job_id (str)
- Return type:
bool
- plesty.server.services.PROBE_HOST = '127.0.0.1'
- exception plesty.server.services.AlreadyRunning
Bases:
SupervisorErrorstartwas asked for a device whose server is already up.Initialize self. See help(type(self)) for accurate signature.
- exception plesty.server.services.NotRunning
Bases:
SupervisorErrorstopwas asked for a device that has no live server.Initialize self. See help(type(self)) for accurate signature.
- exception plesty.server.services.PortBusy
Bases:
SupervisorErrorstartfound the device’s port already answering — another server holds it.Initialize self. See help(type(self)) for accurate signature.
- class plesty.server.services.Supervisor(home: plesty.server.model.home.Home, ready_timeout: float | None = None, stop_timeout: float | None = None, probe_timeout_ms: int | None = None)
Launch and control device servers under a
Home.Bind the supervisor to a home.
- Parameters:
home (plesty.server.model.home.Home) – The bench home.
ready_timeout (float | None) – Seconds
start()waits for the first probe answer.stop_timeout (float | None) – Seconds
stop()waits before escalating.probe_timeout_ms (int | None) – Milliseconds one probe waits for its reply.
running.) ((Defaults come from config.yaml)
- home
- ready_timeout
- stop_timeout
- probe_timeout_ms
- record(name: str) plesty.server.model.process.ProcessRecord | None
The persisted launch record of name, or
None.- Parameters:
name (str)
- Return type:
- exit_file(name: str) pathlib.Path
Where the launcher records the exit code of the server of name.
- Parameters:
name (str)
- Return type:
pathlib.Path
- known() list[str]
Names that have a launch record (alive or not).
- Return type:
list[str]
- _drop_record(name: str) None
- Parameters:
name (str)
- Return type:
None
- alive(name: str) bool
Whether the recorded server of name still runs.
- Parameters:
name (str)
- Return type:
bool
- poll(name: str) int | None
Exit code of the server of name,
Nonewhile it runs.LOST_EXITwhen the process is gone without a recorded exit (the bench rebooted, or someone killed the launcher) — and when nothing is known at all.- Parameters:
name (str)
- Return type:
int | None
- status(name: str, probe_port: bool = True) plesty.server.model.process.ProcessStatus
Observe name: record, process existence and (optionally) a probe.
- Parameters:
name (str) – Device instance name.
probe_port (bool) – Whether to send a
describeto tellrunningfromstarting; skipping it is cheaper for bulk listings.
- Return type:
- wait_ready(name: str, timeout: float | None = None) bool
Poll until the server of name answers a probe.
- Returns:
Trueonce it answers;Falseon timeout or when the process exits first.- Parameters:
name (str)
timeout (float | None)
- Return type:
bool
- start(spec: plesty.server.model.device.DeviceSpec, python: pathlib.Path, wait: bool = True, timeout: float | None = None, package: str = '', version: str = '') plesty.server.model.process.ProcessStatus
Launch the server of spec with the interpreter python.
- Parameters:
spec (plesty.server.model.device.DeviceSpec) – Module, arguments, environment and port to run with.
python (pathlib.Path) – Interpreter of the environment the module is installed in.
wait (bool) – Block until the server answers a probe (or fails).
timeout (float | None) – Readiness wait in seconds; defaults to
ready_timeout.package (str) – Distribution name, stored on the record for listings.
version (str) – Installed version, likewise.
- Returns:
The status after launch —
runningwhen it came up,startingwhen wait is off or it has not answered yet,exitedwhen it died before answering (detailcarries the log tail).- Raises:
AlreadyRunning – When a live server of that name exists.
SupervisorError – When the interpreter cannot be executed.
- Return type:
- _start_locked(spec: plesty.server.model.device.DeviceSpec, python: pathlib.Path, wait: bool, timeout: float | None, package: str, version: str) plesty.server.model.process.ProcessStatus
- Parameters:
python (pathlib.Path)
wait (bool)
timeout (float | None)
package (str)
version (str)
- Return type:
- stop(name: str, timeout: float | None = None) bool
Interrupt the server of name, escalating to a kill if it lingers.
Interrupt → wait timeout → terminate → wait 3 s → kill tree.
- Returns:
Truewhen a process was stopped,Falsewhen only a stale record was cleared.- Raises:
NotRunning – When nothing (not even a stale record) is known.
- Parameters:
name (str)
timeout (float | None)
- Return type:
bool
- restart(spec: plesty.server.model.device.DeviceSpec, python: pathlib.Path, wait: bool = True, timeout: float | None = None) plesty.server.model.process.ProcessStatus
Stop the server of spec if it runs, then
start()it again.- Parameters:
python (pathlib.Path)
wait (bool)
timeout (float | None)
- Return type:
- relaunch(record: plesty.server.model.process.ProcessRecord, wait: bool = True) plesty.server.model.process.ProcessStatus
Start the server again exactly as record says it was started.
Used after a host-tier field test: the interpreter, arguments and environment of the interrupted launch are what the bench expects back.
- Parameters:
wait (bool)
- Return type:
- stop_all(names: list[str] | None = None) list[str]
Stop every known (or the given) server; returns the names stopped.
- Parameters:
names (list[str] | None)
- Return type:
list[str]
- tail(name: str, lines: int = 50) str
The last lines lines of the log of name (
""when no log).- Parameters:
name (str)
lines (int)
- Return type:
str
- _wait_exit(name: str, timeout: float) bool
- Parameters:
name (str)
timeout (float)
- Return type:
bool
- exception plesty.server.services.SupervisorError
Bases:
RuntimeErrorA start or stop request could not be carried out.
Initialize self. See help(type(self)) for accurate signature.
- plesty.server.services.probe(port: int, timeout_ms: int = 2000, host_addr: str = PROBE_HOST) bool
Ask the server on port to describe itself.
- Returns:
Truewhen a plesty-lib server answeredstatus: okin time.- Parameters:
port (int)
timeout_ms (int)
host_addr (str)
- Return type:
bool
- plesty.server.services.request(port: int, message: dict[str, Any], timeout_ms: int = 5000, host_addr: str = PROBE_HOST) dict[str, Any]
Send one request to the server on port and return its reply.
The same DEALER socket
probe()uses, with the message left to the caller: this is how a developer asks a running device server anything its plesty-lib command solver understands, without a client library.- Raises:
SupervisorError – When nobody answers in time, or the answer is not JSON the wire format allows.
- Parameters:
port (int)
message (dict[str, Any])
timeout_ms (int)
host_addr (str)
- Return type:
dict[str, Any]