plesty.server.model =================== .. py:module:: plesty.server.model .. autoapi-nested-parse:: Model layer: fleet configuration, device catalogue, installer and supervisor state. Pure Python — no Qt imports. Everything the presenter needs to know about the bench lives here and is testable without a display. Submodules ---------- .. toctree:: :maxdepth: 1 /reference/plesty/server/model/catalogue/index /reference/plesty/server/model/device/index /reference/plesty/server/model/device_manager/index /reference/plesty/server/model/envfile/index /reference/plesty/server/model/errors/index /reference/plesty/server/model/field_test/index /reference/plesty/server/model/fleet/index /reference/plesty/server/model/home/index /reference/plesty/server/model/job/index /reference/plesty/server/model/ports/index /reference/plesty/server/model/process/index Attributes ---------- .. autoapisummary:: plesty.server.model.DEFAULT_INDEX_URL plesty.server.model.REQUIRED_STANDARD plesty.server.model.SOURCE_GIT plesty.server.model.SOURCE_PYPI plesty.server.model.ENV_FILENAME plesty.server.model.JOB_KIND plesty.server.model.REPORT_BASENAME plesty.server.model.STANDARD_GATES plesty.server.model.TIER_ARTIFACTS plesty.server.model.TIER_SCRIPTS plesty.server.model.FLEET_FILENAME plesty.server.model.FLEET_VERSION plesty.server.model.HOME_ENV plesty.server.model.CANCELLED_EXIT plesty.server.model.LOG_CHUNK plesty.server.model.LOST_EXIT plesty.server.model.AGENT_PORT plesty.server.model.DEFAULT_TCP_PORT plesty.server.model.DEVICE_PORT_BASE plesty.server.model.EXITED plesty.server.model.RUNNING plesty.server.model.STARTING plesty.server.model.STATES plesty.server.model.STOPPED Exceptions ---------- .. autoapisummary:: plesty.server.model.DeviceManagerError plesty.server.model.FleetError plesty.server.model.FieldTestError plesty.server.model.JobError Classes ------- .. autoapisummary:: plesty.server.model.CatalogueEntry plesty.server.model.DeviceSpec plesty.server.model.DeviceManager plesty.server.model.InstalledPackage plesty.server.model.InstalledVersion plesty.server.model.PackageInfo plesty.server.model.FieldTestReport plesty.server.model.FieldTestRun plesty.server.model.GateResult plesty.server.model.Fleet plesty.server.model.Home plesty.server.model.Job plesty.server.model.JobStore plesty.server.model.ProcessRecord plesty.server.model.ProcessStatus Functions --------- .. autoapisummary:: plesty.server.model.parse_catalogue plesty.server.model.module_for plesty.server.model.parse_env plesty.server.model.read_env plesty.server.model.render_env plesty.server.model.field_test_script plesty.server.model.find_reports plesty.server.model.gates_in_help plesty.server.model.options_in_help plesty.server.model.find_fleet plesty.server.model.load_fleet plesty.server.model.save_fleet plesty.server.model.default_home plesty.server.model.tail_file plesty.server.model.allocate_port plesty.server.model.declares_port plesty.server.model.port_from plesty.server.model.port_is_free plesty.server.model.port_listening Package Contents ---------------- .. py:data:: DEFAULT_INDEX_URL :value: 'https://hub.plesty.net/api/modules/' .. py:data:: REQUIRED_STANDARD :value: 'quantum' .. py:class:: CatalogueEntry One module of the hub catalogue. :ivar name: Distribution name (``plesty-pm100d``). :ivar slug: Project slug. :ivar namespace: GitLab path (``plesty/hub/devices/thorlab/pm100d``). :ivar description: One line. :ivar category: ``devices``, ``analyzers`` or ``experiments``. :ivar vendor: Vendor group for devices (``thorlab``), else empty. :ivar repo_url: Clone URL — what a git install uses. :ivar web_url: Project page. :ivar version: Latest release version, or empty. :ivar latest_tag: Latest tag (``v0.2.1``), or empty. :ivar standard: Compliance standard the module pins (``quantum``), or empty. :ivar checked: Whether a ``plesty check`` report is published. :ivar gates_ok: Whether that report passed. :ivar gates_passed: Gates passed in it. :ivar gates_failed: Gates failed in it. :ivar field_tests: Number of published field-test reports. .. py:attribute:: name :type: str .. py:attribute:: slug :type: str :value: '' .. py:attribute:: namespace :type: str :value: '' .. py:attribute:: description :type: str :value: '' .. py:attribute:: category :type: str :value: '' .. py:attribute:: vendor :type: str :value: '' .. py:attribute:: repo_url :type: str :value: '' .. py:attribute:: web_url :type: str :value: '' .. py:attribute:: version :type: str :value: '' .. py:attribute:: latest_tag :type: str :value: '' .. py:attribute:: standard :type: str :value: '' .. py:attribute:: checked :type: bool :value: False .. py:attribute:: gates_ok :type: bool :value: False .. py:attribute:: gates_passed :type: int :value: 0 .. py:attribute:: gates_failed :type: int :value: 0 .. py:attribute:: field_tests :type: int :value: 0 .. py:property:: installable :type: bool a device at the required standard with a repository. :type: Whether the bench offers it .. py:property:: ref :type: str | None the latest tag, else the default branch. :type: The git ref a fresh install checks out .. py:property:: quality :type: str ``ok``, ``failing``, ``unchecked``. :type: One word for listings .. py:method:: matches(query: str) -> bool Case-insensitive substring match on name, slug, vendor and description. .. py:method:: from_registry(raw: collections.abc.Mapping[str, Any]) -> CatalogueEntry :classmethod: Build from one element of the registry's ``modules`` list. .. py:function:: parse_catalogue(document: Any) -> list[CatalogueEntry] Every entry of a ``/api/modules/`` document (a mapping with ``modules`` or a bare list). .. py:class:: DeviceSpec One device server as the fleet declares it. :ivar name: Instance name — the venv, log and state key; unique per bench. :ivar package: PyPI distribution of the device module. :ivar version: Pinned release, or ``None`` for the newest one at install time. :ivar git: Repository URL to install from instead of PyPI, or ``None``. :ivar ref: Branch, tag or commit for a git source; ``None`` = ``v`` when a version is given, else the default branch. :ivar module: Import path served with ``python -m``; derived from *package*. :ivar args: Arguments appended to ``python -m ``. :ivar env: Environment set for the server process on top of the bench's own. :ivar port: TCP port the server binds; probed for liveness. :ivar autostart: Whether ``fleet up``/``fleet run`` start it. .. py:attribute:: name :type: str .. py:attribute:: package :type: str .. py:attribute:: version :type: str | None :value: None .. py:attribute:: git :type: str | None :value: None .. py:attribute:: ref :type: str | None :value: None .. py:attribute:: module :type: str :value: '' .. py:attribute:: args :type: tuple[str, Ellipsis] :value: () .. py:attribute:: env :type: collections.abc.Mapping[str, str] .. py:attribute:: port :type: int :value: 0 .. py:attribute:: autostart :type: bool :value: True .. py:method:: __post_init__() -> None Fill the derived fields and validate the declaration. .. py:property:: port_is_explicit :type: bool Whether this device asked for its port rather than taking the default. True once the bench has allocated one and written it down, so the port a client connected to yesterday is the port it finds today. .. py:property:: launch_args :type: tuple[str, Ellipsis] The arguments ``python -m `` is started with. :attr:`port` is what the supervisor probes; unless the *args* already name a port, it is also handed to the server as ``--tcp-port``, so what is probed and what is bound cannot drift. An environment value (``*_TCP_PORT`` — often ``5555`` straight from a module's ``.env.example``) is deliberately not trusted here: an explicit flag beats the environment in every hub module, and the bench owns the ports. .. py:property:: git_ref :type: str | None The ref a git install checks out (``ref``, else ``v``, else ``None``). .. py:property:: requirement :type: str The pip requirement string. ``plesty-pm100d==0.2.1`` for a PyPI source; ``plesty-pm100d @ git+@v0.2.1`` for a git source. .. py:method:: from_mapping(name: str, raw: collections.abc.Mapping[str, Any]) -> DeviceSpec :classmethod: Build a spec from one ``devices:`` entry of a fleet file. :param name: The mapping key. :param raw: The entry's fields. :raises FleetError: On unknown keys or wrong value types. .. py:method:: to_mapping() -> dict[str, Any] The fleet-file form of this spec (derived defaults omitted). .. py:function:: module_for(package: str) -> str The import path served by ``python -m`` for a PLESTY distribution. :param package: PyPI distribution name, e.g. ``plesty-pm100d``. :returns: ``plesty.pm100d`` — the platform's flat namespace convention. .. py:data:: SOURCE_GIT :value: 'git' .. py:data:: SOURCE_PYPI :value: 'pypi' .. py:class:: DeviceManager(home: plesty.server.model.home.Home) The installed-module inventory under a :class:`Home`. Bind to a home. :param home: The bench home. .. py:attribute:: home .. py:method:: record(package: str, version: str, source: str = SOURCE_PYPI, git: str | None = None, ref: str | None = None, commit: str | None = None, python: str = '', job_id: str | None = None, path: str | os.PathLike[str] | None = None) -> InstalledVersion Write down that *package* *version* is installed; returns the record. :param package: Distribution name. :param version: Installed release. :param source: :data:`SOURCE_PYPI` or :data:`SOURCE_GIT`. :param git: Repository URL for a git source. :param ref: Requested ref for a git source. :param commit: Checked-out commit for a git source. :param python: Interpreter version of the environment. :param job_id: The install job. :param path: Environment location when not the default package dir. .. py:method:: save(installed: InstalledVersion) -> pathlib.Path Write *installed* to its record file; returns the path. .. py:method:: get(package: str, slot: str) -> InstalledVersion | None The record of *package* at *slot* (a version, or ``git-``), or ``None``. .. py:method:: require(package: str, slot: str) -> InstalledVersion Like :meth:`get` but raises :class:`DeviceManagerError` when absent. .. py:method:: package(package: str) -> InstalledPackage Every installed version of *package* (empty when none). .. py:method:: list() -> list[InstalledPackage] Every package with at least one installed version, by name. .. py:method:: info(package: str) -> PackageInfo The package-level record of *package* (empty fields when nothing is known). .. py:method:: remember(package: str, **fields: str) -> PackageInfo Merge non-empty *fields* into the package-level record; returns it. .. py:method:: env_dir(installed: InstalledVersion) -> pathlib.Path The environment directory of *installed* (its ``path``, else the package dir). .. py:method:: resolve(spec: plesty.server.model.device.DeviceSpec) -> InstalledVersion | None The installed version that serves *spec*, or ``None`` when one must be installed. The most recent install among those that :meth:`InstalledVersion.satisfies` the spec wins — for an unpinned PyPI spec that is the newest install, for a git branch the last pull. .. py:method:: forget(package: str, slot: str, remove_env: bool = False) -> bool Drop the record of *package* at *slot*; ``True`` when it existed. :param package: Distribution name. :param slot: Version (release) or ``git-`` (checkout). :param remove_env: Also delete a release's environment directory. .. py:exception:: DeviceManagerError Bases: :py:obj:`ValueError` An installed-version record is missing or inconsistent. Initialize self. See help(type(self)) for accurate signature. .. py:class:: InstalledPackage All installed versions of one device module. :ivar package: Distribution name. :ivar versions: Records keyed by slot (version, or ``git-``), oldest install first. .. py:attribute:: package :type: str .. py:attribute:: versions :type: collections.abc.Mapping[str, InstalledVersion] .. py:method:: __iter__() -> collections.abc.Iterator[InstalledVersion] Iterate over the versions, oldest install first. .. py:method:: __len__() -> int Number of installed versions. .. py:method:: __contains__(version: object) -> bool Whether *version* (a slot, or a release version) is installed. .. py:property:: latest :type: InstalledVersion | None The most recently installed version, or ``None`` when empty. .. py:class:: InstalledVersion One version of one device module present on the bench. :ivar package: PyPI distribution name, e.g. ``plesty-pm100d``. :ivar version: The installed release as reported by the environment. :ivar source: :data:`SOURCE_PYPI` or :data:`SOURCE_GIT`. :ivar git: Repository URL for a git source, else ``None``. :ivar ref: Branch, tag or commit that was requested for a git source. :ivar commit: The commit actually checked out for a git source. :ivar installed_utc: ISO timestamp of the install. :ivar python: Version of the interpreter in the environment. :ivar job_id: The install job that produced it, when known. :ivar path: Where the environment lives when it is not the default ``/packages///`` — a git checkout, whose ``.venv`` is the environment. .. py:attribute:: package :type: str .. py:attribute:: version :type: str .. py:attribute:: source :type: str :value: 'pypi' .. py:attribute:: git :type: str | None :value: None .. py:attribute:: ref :type: str | None :value: None .. py:attribute:: commit :type: str | None :value: None .. py:attribute:: installed_utc :type: str :value: '' .. py:attribute:: python :type: str :value: '' .. py:attribute:: job_id :type: str | None :value: None .. py:attribute:: path :type: str :value: '' .. py:method:: __post_init__() -> None Validate the record. .. py:property:: slot :type: str the version for a release, ``git-`` for a checkout. A checkout moves — its version changes with every pull — so it is kept by the ref it tracks, and two checkouts at the same commit (two refs) never share a record. :type: What identifies the environment within the package .. py:property:: key :type: tuple[str, str] ``(package, slot)`` — what identifies the environment. .. py:method:: satisfies(spec: plesty.server.model.device.DeviceSpec) -> bool Whether this install is what *spec* asks for. A PyPI spec is satisfied by the pinned version (or any PyPI version when unpinned); a git spec by an install from the same repository at the same ref (or ``v``), a commit hash matching by prefix. .. py:method:: to_dict() -> dict[str, Any] JSON form. .. py:method:: from_dict(data: collections.abc.Mapping[str, Any]) -> InstalledVersion :classmethod: Rebuild a record from :meth:`to_dict` output (extra keys ignored). .. py:class:: PackageInfo What the bench knows about a module as a project — independent of any install. Learned from the catalogue (``adopt``) or from a git install, kept in ``/packages//package.json`` and never asked again: the repository a module comes from, its project page, its latest release. :ivar package: Distribution name. :ivar repo_url: Clone URL of the module's repository. :ivar web_url: Project page (GitLab), ``""`` when unknown. :ivar latest_tag: Latest release tag seen (``v0.2.1``), ``""`` when none. :ivar version: Latest release version seen, ``""`` when none. :ivar vendor: Hub vendor group, ``""`` when unknown. :ivar description: One line, ``""`` when unknown. .. py:attribute:: package :type: str .. py:attribute:: repo_url :type: str :value: '' .. py:attribute:: web_url :type: str :value: '' .. py:attribute:: latest_tag :type: str :value: '' .. py:attribute:: version :type: str :value: '' .. py:attribute:: vendor :type: str :value: '' .. py:attribute:: description :type: str :value: '' .. py:method:: to_dict() -> dict[str, Any] JSON form. .. py:method:: from_dict(data: collections.abc.Mapping[str, Any]) -> PackageInfo :classmethod: Rebuild from :meth:`to_dict` output (extra keys ignored). .. py:method:: merged(**fields: str) -> PackageInfo A copy with the non-empty *fields* replacing the current values. .. py:data:: ENV_FILENAME :value: '.env' .. py:function:: parse_env(text: str) -> dict[str, str] Parse dotenv *text* into a mapping (later keys win). .. py:function:: read_env(path: str | os.PathLike[str]) -> dict[str, str] Parse the file at *path*; empty when it does not exist. .. py:function:: render_env(values: collections.abc.Mapping[str, str]) -> str Render a mapping as dotenv text (one ``KEY=VALUE`` per line, quoted when needed). .. py:exception:: FleetError Bases: :py:obj:`ValueError` A fleet file or device declaration is malformed. Initialize self. See help(type(self)) for accurate signature. .. py:data:: JOB_KIND :value: 'field-test' .. py:data:: REPORT_BASENAME :value: 'reports/field-test-run' .. py:data:: STANDARD_GATES :type: tuple[str, Ellipsis] :value: ('discovery', 'connect_lifecycle', 'param_roundtrip', 'param_constraints', 'functions',... .. py:data:: TIER_ARTIFACTS :type: collections.abc.Mapping[str, str] .. py:data:: TIER_SCRIPTS :type: collections.abc.Mapping[str, str] .. py:exception:: FieldTestError Bases: :py:obj:`plesty.server.model.errors.FleetError` A field test cannot be set up as asked. Initialize self. See help(type(self)) for accurate signature. .. py:class:: FieldTestReport What a field test published — the summary a bench needs, not the whole document. :ivar path: The ``field-test*.json`` file. :ivar tier: ``host`` or ``client``, from the file name. :ivar instrument: The ``reports//`` directory name. :ivar module: Module name from the document. :ivar version: Module version the run tested. :ivar ok: Overall outcome. :ivar passed: Gates passed. :ivar failed: Gates failed. :ivar skipped: Gates skipped. :ivar gates: Per-gate results, in run order. :ivar findings: Count of findings per severity (``error``/``warning``/``info``). :ivar modified_utc: ISO timestamp of the file — when the run ended. .. py:attribute:: path :type: pathlib.Path .. py:attribute:: tier :type: str .. py:attribute:: instrument :type: str .. py:attribute:: module :type: str .. py:attribute:: version :type: str .. py:attribute:: ok :type: bool .. py:attribute:: passed :type: int .. py:attribute:: failed :type: int .. py:attribute:: skipped :type: int .. py:attribute:: gates :type: tuple[GateResult, Ellipsis] :value: () .. py:attribute:: findings :type: collections.abc.Mapping[str, int] .. py:attribute:: modified_utc :type: str :value: '' .. py:property:: summary :type: str ``pm100d 0.2.1 [host] ok 9/9`` style. :type: One line .. py:method:: from_path(path: pathlib.Path) -> FieldTestReport :classmethod: Read a published report. :raises FieldTestError: When the file is not a field-test document. .. py:class:: FieldTestRun One field-test request. :ivar tier: ``host`` (instrument, server must be stopped) or ``client`` (running server). :ivar address: ``--address`` — host tier: instrument address or ``mock``; client tier: ``tcp://…``; ``None`` lets the script use ``.env``. :ivar gates: Run only these gates (``None`` = all). :ivar repetitions: ``--repetitions`` for the timing gates (``None`` = script default). :ivar keep_going: Carry on after a failed gate. :ivar keep_server: Host tier: leave a running server alone (only right for ``mock`` or another instrument). :ivar extra: Further arguments passed to the script unchanged. .. py:attribute:: tier :type: str :value: 'host' .. py:attribute:: address :type: str | None :value: None .. py:attribute:: gates :type: tuple[str, Ellipsis] | None :value: None .. py:attribute:: repetitions :type: int | None :value: None .. py:attribute:: keep_going :type: bool :value: False .. py:attribute:: keep_server :type: bool :value: False .. py:attribute:: extra :type: tuple[str, Ellipsis] :value: () .. py:method:: __post_init__() -> None Validate the request. .. py:property:: script :type: str The script this request runs, relative to the checkout. .. py:property:: artifact :type: str The published file name this tier writes. .. py:property:: needs_instrument :type: bool Whether the run opens the instrument itself (host tier, not ``mock``). .. py:method:: argv(report_base: str | None = REPORT_BASENAME, supports: collections.abc.Collection[str] | None = None) -> list[str] The script's arguments for this request. :param report_base: ``--report`` for the host tier's internal ``.md``/``.jsonl`` pair; ``None`` leaves it to the script. :param supports: The option names the script accepts (from its ``--help``), or ``None`` to assume the current scaffold. Older scaffolds take ``--port PORT`` and ``--mock`` where the current one takes ``--address``; the request is spelled in whichever the script understands, and options it lacks are left out. .. py:method:: command(python: pathlib.Path, checkout: pathlib.Path, supports: collections.abc.Collection[str] | None = None) -> list[str] The full command line: *python* running the tier's script in *checkout*. :param python: The checkout's interpreter. :param checkout: The module checkout. :param supports: The script's option names (see :meth:`argv`). :raises FieldTestError: When the checkout has no script for this tier. .. py:method:: to_dict() -> dict[str, Any] Record form (stored on the job's ``detail``). .. py:method:: from_dict(data: collections.abc.Mapping[str, Any]) -> FieldTestRun :classmethod: Rebuild a request from :meth:`to_dict` output. .. py:class:: GateResult One gate of a published report. .. py:attribute:: name :type: str .. py:attribute:: status :type: str .. py:attribute:: duration_s :type: float .. py:attribute:: reason :type: str .. py:function:: field_test_script(checkout: pathlib.Path, tier: str = 'host') -> pathlib.Path | None The *tier*'s script inside *checkout*, or ``None`` when the module has none. :raises FieldTestError: On an unknown tier. .. py:function:: find_reports(checkout: pathlib.Path, since_utc: str | None = None) -> list[FieldTestReport] Every published field-test report under ``/reports/``, newest first. :param checkout: The module checkout. :param since_utc: Only reports modified at or after this ISO timestamp — what a run that started then produced. .. py:function:: gates_in_help(text: str) -> tuple[str, Ellipsis] The gate names a script's ``--help`` offers for ``--gates``, in its order; empty if none. .. py:function:: options_in_help(text: str) -> set[str] The long options an argparse ``--help`` text mentions (``--address``, ``--mock`` …). .. py:data:: FLEET_FILENAME :value: 'fleet.yaml' .. py:data:: FLEET_VERSION :value: 1 .. py:class:: Fleet A parsed fleet file. :ivar devices: Specs keyed by device name, in file order. :ivar home: ``home:`` from the file, or ``None``. :ivar index_url: ``index_url:`` from the file, or ``None``. :ivar path: Where it was read from, or ``None`` for an in-memory fleet. .. py:attribute:: devices :type: collections.abc.Mapping[str, plesty.server.model.device.DeviceSpec] .. py:attribute:: home :type: pathlib.Path | None :value: None .. py:attribute:: index_url :type: str | None :value: None .. py:attribute:: path :type: pathlib.Path | None :value: None .. py:method:: __getitem__(name: str) -> plesty.server.model.device.DeviceSpec The spec of device *name*; :class:`KeyError` when undeclared. .. py:method:: __contains__(name: object) -> bool Whether *name* is a declared device. .. py:method:: __iter__() -> Any Iterate over the specs in file order. .. py:method:: __len__() -> int Number of declared devices. .. py:property:: autostart :type: list[plesty.server.model.device.DeviceSpec] The specs ``fleet up`` starts. .. py:method:: from_mapping(raw: collections.abc.Mapping[str, Any], path: pathlib.Path | None = None) -> Fleet :classmethod: Build a fleet from a parsed YAML document. :param raw: The document. :param path: Where it came from, for error messages and :attr:`path`. :raises FleetError: On a wrong schema version or malformed entries. .. py:method:: to_mapping() -> dict[str, Any] The YAML document form of this fleet. .. py:method:: with_device(spec: plesty.server.model.device.DeviceSpec) -> Fleet A copy with *spec* added or replaced under its name. .. py:method:: without_device(name: str) -> Fleet A copy without device *name* (no error when absent). .. py:function:: find_fleet(explicit: str | os.PathLike[str] | None, home: pathlib.Path) -> pathlib.Path | None Locate the fleet file a command should consult. :param explicit: A path the user gave (``-f``); returned as-is when set. :param home: The bench home, whose ``fleet.yaml`` is the last resort after the working directory's. :returns: The first existing candidate, or ``None`` when no fleet file exists. .. py:function:: load_fleet(path: str | os.PathLike[str]) -> Fleet Read and validate a fleet file. :param path: The YAML file. :raises FleetError: When the file is missing or malformed. .. py:function:: save_fleet(fleet: Fleet, path: str | os.PathLike[str]) -> pathlib.Path Write a fleet file. :param fleet: The fleet to serialize. :param path: Destination file; parents are created. :returns: The written path. .. py:data:: HOME_ENV :value: 'PLESTY_SERVER_HOME' .. py:class:: Home Layout of the plesty-server home directory. :ivar root: The directory everything lives under. .. py:attribute:: root :type: pathlib.Path .. py:method:: resolve(root: str | os.PathLike[str] | None = None) -> Home :classmethod: Build a home from an explicit path or the environment default. :param root: Directory to use; ``None`` selects :func:`default_home`. .. py:property:: venvs :type: pathlib.Path One virtual environment per installed device, keyed by device name. .. py:property:: logs :type: pathlib.Path One append-only log per device server. .. py:property:: state :type: pathlib.Path One JSON record per running server (pid, port, start time). .. py:property:: run :type: pathlib.Path Working directories the servers run in; plesty-lib writes ``logs/`` there. .. py:property:: repos :type: pathlib.Path Git checkouts of device modules installed from a repository. .. py:property:: jobs :type: pathlib.Path One JSON record per long-running job (install, update, field test, command). .. py:property:: fleets :type: pathlib.Path Where the console keeps fleet files by default (``/fleets/``). .. py:property:: tools :type: pathlib.Path Tools the bench installed for itself (the portable git on Windows). .. py:property:: packages :type: pathlib.Path one environment per (package, version), side by side. :type: Installed device modules .. py:method:: package_dir(package: str, version: str) -> pathlib.Path Environment of *package* at *version*. .. py:method:: package_record(package: str, version: str) -> pathlib.Path Record file of *package* at *version* (beside its environment). .. py:property:: last_fleet :type: pathlib.Path Pointer file naming the fleet the console used last (``/last-fleet``). .. py:method:: package_info(package: str) -> pathlib.Path The package-level record of *package*: repository, project page, latest release. .. py:method:: checkout(package: str, ref: str | None) -> pathlib.Path Checkout directory of *package* at *ref* (git sources only). Refs may contain ``/`` (``feat/x``); the directory name flattens them. .. py:method:: job_file(job_id: str) -> pathlib.Path Record file of job *job_id*. .. py:method:: job_log(job_id: str) -> pathlib.Path Log file of job *job_id*. .. py:method:: venv(name: str) -> pathlib.Path Venv directory of the device *name*. .. py:method:: log(name: str) -> pathlib.Path Log file of the device *name*. .. py:method:: state_file(name: str) -> pathlib.Path Process-state file of the device *name*. .. py:method:: run_dir(name: str) -> pathlib.Path Working directory of the device *name*'s server process. .. py:method:: ensure() -> Home Create the directory tree; returns *self* for chaining. .. py:function:: default_home() -> pathlib.Path The home directory the environment or the packaged config designates. :returns: ``$PLESTY_SERVER_HOME`` when set, else ``running.home`` from ``config.yaml`` with ``~`` expanded. .. py:data:: CANCELLED_EXIT :value: -9 .. py:data:: LOG_CHUNK :value: 65536 .. py:data:: LOST_EXIT :value: -1 .. py:class:: Job One unit of long-running work. :ivar id: ``--``, unique per bench. :ivar name: Device instance the job belongs to. :ivar kind: ``install``, ``update``, ``field-test``, ``command`` … :ivar command: The command line. :ivar cwd: Working directory. :ivar pid: Process id while it runs. :ivar started_utc: ISO timestamp of the launch. :ivar finished_utc: ISO timestamp of the end, or ``None`` while running. :ivar returncode: Exit code once finished (``None`` while running; :data:`LOST_EXIT` when the process vanished without one). :ivar log: Path of the log file. :ivar detail: Kind-specific data (e.g. the reports a field test wrote). .. py:attribute:: id :type: str .. py:attribute:: name :type: str .. py:attribute:: kind :type: str .. py:attribute:: command :type: list[str] .. py:attribute:: cwd :type: str .. py:attribute:: pid :type: int | None .. py:attribute:: started_utc :type: str .. py:attribute:: finished_utc :type: str | None :value: None .. py:attribute:: returncode :type: int | None :value: None .. py:attribute:: log :type: str :value: '' .. py:attribute:: detail :type: dict[str, Any] .. py:property:: running :type: bool Whether the job has not finished yet. .. py:property:: state :type: str ``running``, ``done`` (exit 0), ``failed`` (any other exit). .. py:method:: to_dict() -> dict[str, Any] JSON form, with the derived ``state`` included. .. py:method:: from_dict(data: collections.abc.Mapping[str, Any]) -> Job :classmethod: Rebuild a record from :meth:`to_dict` output (extra keys ignored). .. py:exception:: JobError Bases: :py:obj:`RuntimeError` A job record does not exist or cannot be read. Initialize self. See help(type(self)) for accurate signature. .. py:class:: JobStore(home: plesty.server.model.home.Home) Job records and logs under a :class:`Home`. Bind to a home. :param home: The bench home. .. py:attribute:: home .. py:method:: create(name: str, kind: str, command: list[str], cwd: str | os.PathLike[str], pid: int | None = None, detail: collections.abc.Mapping[str, Any] | None = None) -> Job Open a new record and return it (state ``running``). :param name: Device instance the job belongs to. :param kind: Job kind, part of the id. :param command: The command line. :param cwd: Working directory. :param pid: Process id when already known. :param detail: Kind-specific data stored on the record. .. py:method:: save(job: Job) -> pathlib.Path Write *job* to its record file; returns the path. .. py:method:: get(job_id: str) -> Job | None The record of *job_id*, or ``None`` when unknown or unreadable. .. py:method:: require(job_id: str) -> Job Like :meth:`get` but raises :class:`JobError` when unknown. .. py:method:: list(name: str | None = None, kind: str | None = None) -> list[Job] Jobs on record, newest first, optionally filtered by device and kind. .. py:method:: finish(job_id: str, returncode: int) -> Job Close the record of *job_id* with *returncode*; returns it. .. py:method:: refresh(job: Job, alive: collections.abc.Callable[[int], bool], exit_code: collections.abc.Callable[[str], int | None]) -> Job Settle a ``running`` record against what the process layer knows. :param job: The record to bring up to date (mutated and saved when it ends). :param alive: Whether a pid is still running. :param exit_code: The exit code left for a job id, or ``None`` when none. :returns: *job*, finished when an exit code exists or the pid is gone (:data:`LOST_EXIT` then), unchanged while it still runs. .. py:method:: log(job_id: str, offset: int = 0, limit: int = LOG_CHUNK) -> tuple[str, int] Read the job log from byte *offset*. :param job_id: The job. :param offset: Where the previous read stopped (0 for the start). :param limit: Maximum bytes to return. :returns: The text read and the offset to continue from. Decoding is lenient — a chunk may cut a multi-byte character. .. py:method:: tail(job_id: str, lines: int = 50) -> str The last *lines* lines of the job log (empty when there is none yet). .. py:function:: tail_file(path: pathlib.Path, lines: int, block: int = 8192) -> str The last *lines* lines of *path*, read from the end in *block* steps. .. py:data:: AGENT_PORT :value: 5550 .. py:data:: DEFAULT_TCP_PORT :value: 5555 .. py:data:: DEVICE_PORT_BASE :value: 5551 .. py:function:: allocate_port(taken: collections.abc.Iterable[int], base: int = DEVICE_PORT_BASE, is_free: collections.abc.Callable[[int], bool] = port_is_free) -> int The lowest free port from *base* up, skipping *taken* and the agent's. :param taken: Ports already spoken for on this bench. :param base: Where counting starts. :param is_free: Asks the host whether a port can be bound; injected by tests. :returns: A port no other device on the bench holds *and* nothing on the host is listening on. :raises FleetError: When counting runs past the end of the port range. .. py:function:: declares_port(args: tuple[str, Ellipsis], env: collections.abc.Mapping[str, str]) -> bool Whether *args* or *env* name the port themselves. A declaration that names its own port is left alone — the bench allocates only for devices that would otherwise take the default and collide. .. py:function:: port_from(args: tuple[str, Ellipsis], env: collections.abc.Mapping[str, str]) -> int Resolve the TCP port a server binds from its args and environment. :param args: Command-line arguments passed to ``python -m ``. :param env: Environment values the fleet sets for the process. :returns: The port to probe, falling back to :data:`DEFAULT_TCP_PORT`. .. py:function:: port_is_free(port: int, host: str = '127.0.0.1') -> bool Whether *port* can actually be bound right now. The fleet file and the process records say what this bench *believes* it is using. A server that outlived its record, or any other program on the machine, believes nothing — and the socket is the only thing that knows. The socket option differs because the platforms disagree about what ``SO_REUSEADDR`` means. On POSIX it says "a port left in ``TIME_WAIT`` is still free", which is the question. On **Windows** it says "bind even if another socket is listening here" — so the probe succeeded against a live server and every busy port reported free, which silently disabled everything built on this: the port a start moves away from, and the port :func:`allocate_port` hands out. ``SO_EXCLUSIVEADDRUSE`` is the Windows way to ask the question POSIX answers with ``SO_REUSEADDR``. .. py:function:: port_listening(port: int, host: str = '127.0.0.1', timeout: float = 0.3) -> bool Whether something accepts TCP connections on *port* right now. The complement of :func:`port_is_free` for a port a *server* holds: a device server binds ``tcp://*``, and on some hosts a specific-address bind still succeeds beside a wildcard listener — connecting is the one test that answers the question everywhere. .. py:data:: EXITED :value: 'exited' .. py:data:: RUNNING :value: 'running' .. py:data:: STARTING :value: 'starting' .. py:data:: STATES .. py:data:: STOPPED :value: 'stopped' .. py:class:: ProcessRecord What the supervisor persists about a started server. :ivar name: Device instance name. :ivar pid: Process id of the launcher that owns the server. :ivar port: TCP port it was told to bind. :ivar started_utc: ISO timestamp of the launch. :ivar command: The full command line. :ivar cwd: Working directory the server runs in. :ivar log: Path of the log file its output goes to. :ivar python: The interpreter — what a relaunch has to repeat. :ivar module: The ``python -m`` target — likewise. :ivar args: The spec's arguments — likewise. :ivar env: The spec's environment — likewise. :ivar package: Distribution the server runs, for listings. :ivar version: Its installed version, for listings. .. py:attribute:: name :type: str .. py:attribute:: pid :type: int .. py:attribute:: port :type: int .. py:attribute:: started_utc :type: str .. py:attribute:: command :type: list[str] .. py:attribute:: cwd :type: str .. py:attribute:: log :type: str .. py:attribute:: python :type: str :value: '' .. py:attribute:: module :type: str :value: '' .. py:attribute:: args :type: list[str] :value: [] .. py:attribute:: env :type: dict[str, str] .. py:attribute:: package :type: str :value: '' .. py:attribute:: version :type: str :value: '' .. py:method:: to_dict() -> dict[str, Any] JSON form. .. py:method:: from_dict(data: collections.abc.Mapping[str, Any]) -> ProcessRecord :classmethod: Rebuild a record from :meth:`to_dict` output (extra keys ignored). .. py:method:: load(path: pathlib.Path) -> ProcessRecord | None :classmethod: Read a record file; ``None`` when absent or unreadable. .. py:method:: save(path: pathlib.Path) -> pathlib.Path Write the record to *path*; returns it. .. py:class:: ProcessStatus The observed state of one device server. :ivar name: Device instance name. :ivar state: One of :data:`STATES`. :ivar pid: Process id when a record exists. :ivar port: Probed port when a record exists. :ivar started_utc: Launch time when a record exists. :ivar log: Log path when a record exists. :ivar returncode: Exit code for an ``exited`` server, when the launcher recorded one (``-1`` when it vanished without). :ivar detail: For an exited server, the tail of its log — why it died. .. py:attribute:: name :type: str .. py:attribute:: state :type: str .. py:attribute:: pid :type: int | None :value: None .. py:attribute:: port :type: int | None :value: None .. py:attribute:: started_utc :type: str | None :value: None .. py:attribute:: log :type: str | None :value: None .. py:attribute:: returncode :type: int | None :value: None .. py:attribute:: detail :type: str :value: '' .. py:property:: alive :type: bool Whether the process exists (running or still starting). .. py:method:: to_dict() -> dict[str, Any] JSON form (what ``status`` prints and the agent returns).