plesty init

Initialize a new Plesty project from the official template. The project is created in the directory specified by the global --project-dir option (defaults to the current working directory), making the standard workflow as simple as:

mkdir plesty-my-device && cd plesty-my-device
plesty init device
plesty init [PROJECT_TYPE] [OPTIONS]

PROJECT_TYPE is one of default, device, analyzer, or experiment (defaults to default — a plain library module). device scaffolds the device server skeleton, analyzer a plesty.lib.analyzer.Analyzer transform with the A1 contract-gate tests, and experiment the plan/journal experiment skeleton with the E1 gate tests.

Two further types, mock-test and field-test, add a test to an existing device module instead of creating a project; each has its own page: plesty init mock-test and plesty init field-test.

Naming conventions

All Plesty projects follow a consistent naming scheme derived from the module name (e.g. foo_bar):

Artifact

Convention

Example

Project folder

plesty-<kebab-name>

plesty-foo-bar

Source folder

plesty/<module_name>/

plesty/foo_bar/

PyPI distribution

plesty-<kebab-name>

plesty-foo-bar

Python import

plesty.<module_name>

plesty.foo_bar

The plesty- prefix in the folder name is automatically stripped when inferring the module name, so plesty-foo-bar → foo_bar without needing --name.

Options

Option

Default

Description

--name

inferred

Python module name — lowercase letters, digits, and _ only (e.g. foo_bar). Inferred from the directory name by stripping the plesty- prefix and converting hyphens to underscores.

--author

John Doe

Author name written into pyproject.toml and REUSE.toml

--email

john.doe@example.com

Author email written into pyproject.toml

--standard

pixel

Compliance standard for the generated CI pipeline: pixel, nebula, or quantum. A fresh scaffold is a prototype, so CI starts at pixel; bump it alongside your releases.

--lib-version

newest on PyPI

Pin plesty-lib to this version instead of looking it up. A bare version becomes a lower bound (0.3.5.dev3 → >=0.3.5.dev3); a complete specifier is written as given (==0.4.0, '>=0.3.5.dev3,<0.4'). PyPI is not consulted, so the scaffold is reproducible.

--no-ci

off

Skip generating .gitlab-ci.yml entirely.

--force

off

field-test and mock-test only: replace the test that command generated before.

Directory rules

  • If --project-dir does not exist it is created automatically.

  • If it already exists it must be empty (hidden entries such as .git are ignored).

What init does

After copying the template, plesty init automatically:

  1. Formats generated files — runs ruff format so the scaffold is already plesty check-clean from the first commit.

  2. Creates the initial git commit — required by versioningit to resolve a package version from git history.

  3. Installs the pre-push hook — writes .git/hooks/pre-push to run plesty check automatically before every git push, at the project’s resolved standard (see Standard resolution). Hooks are not cloned: a contributor who clones the project installs it once with plesty update --install-hook.

Examples

# Minimal: module name inferred from directory, pixel CI (prototype), default author
mkdir plesty-power-meter && cd plesty-power-meter
plesty init device

# Explicit name and author
plesty --project-dir ~/projects/plesty-power-meter init device \
  --name power_meter \
  --author "Jane Doe" \
  --email jane@example.com

# Use nebula standard in CI (gates 1–6 only)
plesty --project-dir ~/projects/plesty-scan-exp init experiment \
  --name scan_exp \
  --standard nebula

# Skip CI file entirely
plesty --project-dir ~/projects/plesty-internal-tool init device \
  --name internal_tool \
  --no-ci

Generated project layout

plesty-foo-bar/
├── plesty/
│   └── foo_bar/
│       ├── __init__.py
│       ├── __main__.py          # device and experiment only
│       ├── config.yaml
│       ├── base_device.py       # device only
│       ├── device.py            # device only
│       ├── preflight.py         # device only — is the instrument there? asked before serving
│       ├── analyzer.py          # analyzer only
│       └── experiment.py        # experiment only
├── config/
│   └── default.yaml             # experiment only — the configuration a run starts from
├── docs/
│   ├── index.md
│   └── toc.yaml
├── tests/
│   ├── __init__.py
│   ├── test_foo_bar.py          # smoke tests covering all public methods
│   └── test_preflight.py        # device only — the preflight with its transport mocked
├── packaging/
│   └── versioning.py            # names snapshots for the release their line works towards
├── CHANGELOG.md
├── setup.yaml                   # experiment only — the benches it runs across, and its devices
├── .env.example                # device and experiment only — connection settings template
├── .git/
│   └── hooks/
│       └── pre-push             # runs plesty check (standard from pyproject.toml)
├── .gitlab-ci.yml               # omitted when --no-ci
├── .gitignore                   # ignores .env (keeps .env.example tracked)
├── LICENSE                      # LGPL text, kept for forge (GitLab) detection
├── LICENSES/
│   └── LGPL-3.0-or-later.txt
├── REUSE.toml                   # declares copyright/license for every file
├── pyproject.toml
└── README.md

The pyproject.toml is pre-configured with:

  • The PyPI distribution name plesty-foo-bar (kebab-case)

  • packages = ["plesty"] so the module installs under the plesty namespace (import plesty.foo_bar)

  • Dynamic versioning via versioningit, with [tool.hatch.version.next-version] pointing at packaging/versioning.py: a build between two tags is named for the release its branch is working towards — the next minor on exp and main, the next patch on maintain/0.N. An older module adopts the same rule with plesty update --versioning.

  • plesty-lib as a runtime dependency, pinned to the newest version published on PyPI when the project is scaffolded (pre-releases included — that is where the features the templates use land first). The lookup has a 3 s budget; with no route to PyPI the pin falls back to PLESTY_LIB_VERSION in plesty/sdk/commands/init.py, the library the templates are written against. The pin is never lower than that floor: the scaffold uses that library’s operation kinds and rig start-up, and an older one would not run it. --lib-version overrides all of that and skips the lookup — for a tutorial, whose lesson text and reader’s pyproject.toml have to keep agreeing however far the library has moved since the page was written:

    plesty init experiment --name scan_exp --lib-version 0.3.5.dev3
    #   → "plesty-lib>=0.3.5.dev3"
    plesty init experiment --name scan_exp --lib-version '>=0.3.5.dev3,<0.4'
    #   → "plesty-lib>=0.3.5.dev3,<0.4"
    
  • pytest, pytest-cov, mypy, and plesty-sdk under [dependency-groups] dev (included automatically by uv run — no --extra dev needed)

  • Sphinx docs settings under [tool.plesty.docs]

The generated .gitlab-ci.yml uses the unified plesty-standard-ci CI component from plesty-ci and passes access_token: $CI_BOT_TOKEN. CI_BOT_TOKEN is provided as a group-level CI/CD variable inherited by every project under the PLESTY groups — no per-project token setup is needed. Do not create a per-project CI_BOT_TOKEN variable: it would shadow the inherited group variable and expire separately.

Use plesty update to change author info, the PyPI name, or CI standard after initialisation.

Device connection credentials (device only)

Device projects are scaffolded to read their connection credentials from the environment rather than hard-coding them. The generated __main__.py loads three variables via plesty.lib.utils.EnvSettings:

Variable

Meaning

Required

DEVICE_HOST

Target device host / IP

yes

DEVICE_PORT

Target device port

yes

DEVICE_TCP_PORT

ZMQ port the device server binds (default 5555)

no

Resolution precedence per value is CLI argument > environment variable > default. The required DEVICE_HOST/DEVICE_PORT are validated with parser.error(...), so a missing credential produces a clean usage message instead of a traceback.

EnvSettings.load() reads ./.env and then the process environment (the process environment wins) and, by default, also registers the parsed .env values into os.environ. Rename these variables per device as needed — the names are just the scaffold defaults, not a fixed contract.

Workflow:

cp .env.example .env          # then fill in DEVICE_HOST / DEVICE_PORT
python -m plesty.foo_bar      # credentials read from .env
python -m plesty.foo_bar --host 10.0.0.5 --tcp-port 6000   # CLI overrides env

.env.example is committed as a template; the real .env is gitignored. The generated tests/test_foo_bar.py includes a skipif real-device test that consumes the same variables, so one .env drives both the server and the tests. It is skipped unless DEVICE_HOST is set.

Before the server binds, __main__.py runs the generated preflight.py: it asks the transport (TrafficManager.preflight() in plesty-lib) whether the instrument is there, and reports which .env is in effect and which version of the module is installed. It refuses only an instrument the transport looked for and did not find; a transport that cannot tell without opening the connection answers unknown, and the server starts. The author fills in one hook, transport(), beside Device.connect(). --skip-preflight starts the server anyway, and tests/test_preflight.py covers the logic with the transport mocked.

The control panel (device only)

The same entry point opens the device’s control panel instead of serving it:

uv sync --extra gui
python -m plesty.foo_bar --gui

Serving and --gui are alternatives — both want the one session the instrument grants, so main() does one or the other. The panel is plesty.lib.ui.DevicePanel over a LocalHandle, which builds every control from the device schema; a parameter added to the schema appears in the window with no GUI code written. The generated open_panel() calls connect() before the panel is built, because it is what the instrument reports on connection — a fitted sensor head’s wavelength range, say — that narrows the schema’s device-wide envelope to the unit in front of the operator.

Operations are filtered by their kind: one whose effect outlives the call is declared @expose_to_api(kind="lifecycle"), and that keeps it off this panel and off the one a remote client renders, since the schema is the only place either can learn it.

Qt goes in a gui extra rather than in dependencies — a headless bench serves the device and never opens a window. The extra carries the same plesty-lib bound as dependencies (the newest published library, or --lib-version):

[project.optional-dependencies]
gui = ["plesty-lib[gui]>=0.6.0"]

Rig start-up and the shared disk (experiment only)

The generated __main__.py is built on plesty.lib.experiment.ExperimentCLI, which owns everything that is the same for every experiment: --config, --resume, --run-root, --dry-run and --manifest, the rig preflight and the graceful stop. The module declares only what differs:

RIG = {"device": ["identity"]}      # sub-device → operations the experiment calls on it
SCHEMA = {"default": [ConfigParameter("points", ...)]}
MANIFEST = Manifest(module=..., schema=SCHEMA, environment=[EnvVar("DEVICE_ADDRESS", ...)])

cli = ExperimentCLI(MANIFEST)
cli.optional()                      # sub-devices the operator may skip; none by default

@cli.rig
def build_devices(args, config, skip): ...

@cli.build
def build_experiment(args, config, run_root, devices): ...

python -m plesty.<module> --manifest prints MANIFEST as JSON, and gate e3 checks that SCHEMA and config/*.yaml declare the same keys — the scaffold passes it as generated.

Before the first step the rig is preflighted: every listed operation must be exposed and identity() must answer. On a failure the CLI prints which sub-device, at which address, from which variable, then offers retry / abort / skip — skip only for the names passed to cli.optional(). A retry calls the @cli.rig builder again, so a .env corrected or a server started in the meantime is picked up. Nothing is measured on a rig that is not what the plan assumes.

setup.yaml — the benches and the devices on them

The scaffold ships the setup.yaml plesty-lib requires of every experiment module (plesty.lib.experiment.setup). It names the benches the experiment runs across — the machine the run is on, bench-local, is the one without an address — and one entry per RIG sub-device naming the bench it runs on (host_name). The file is committed, so it holds no values: each is written as the variable holding it (${LOCAL_DATA}), and a literal address, token or path is refused.

The values go in the checkout’s .env. The first run writes it from .env.example, one line per variable the setup names; LOCAL_DATA is suggested empty, so a run other than --dry-run stops there and names the variable and the file to set it in. Once set, the run is given it as PLESTY_DATA.

Addresses are never written into the module: the bench that serves each sub-device gives the run its <NAME>_ADDRESS (device → DEVICE_ADDRESS). Without a bench, set it in .env or pass --device-address.

Variable

Meaning

LOCAL_DATA

Where the machine the run is on sees the shared data disk — bench-local’s data in setup.yaml. Required for a run

<NAME>_ADDRESS

ZMQ address of one sub-device’s server; its bench provides it

PLESTY_DATA

The shared data disk as this machine sees it, given to the run from LOCAL_DATA. Every path that crosses between machines is relative to it: runs land in <PLESTY_DATA>/runs, and devices write their frames into the run’s raw/<device> folder and report only the file

--run-root overrides where runs land, per call; a relative one is under PLESTY_DATA.

Next steps after scaffolding

plesty init ends with a numbered, project-type-specific Next steps list — environment setup, the files to implement, and the verify/publish loop. The step text ships in the wheel (plesty/sdk/assets/init/next-steps.yaml). Pass the global --quiet flag to print only the result line:

plesty --quiet --project-dir plesty-foo-bar init device