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 |
|
|
Source folder |
|
|
PyPI distribution |
|
|
Python import |
|
|
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 |
|---|---|---|
|
inferred |
Python module name — lowercase letters, digits, and |
|
|
Author name written into |
|
|
Author email written into |
|
|
Compliance standard for the generated CI pipeline: |
|
newest on PyPI |
Pin |
|
off |
Skip generating |
|
off |
|
Directory rules
If
--project-dirdoes not exist it is created automatically.If it already exists it must be empty (hidden entries such as
.gitare ignored).
What init does
After copying the template, plesty init automatically:
Formats generated files — runs
ruff formatso the scaffold is alreadyplesty check-clean from the first commit.Creates the initial git commit — required by
versioningitto resolve a package version from git history.Installs the pre-push hook — writes
.git/hooks/pre-pushto runplesty checkautomatically before everygit push, at the project’s resolved standard (see Standard resolution). Hooks are not cloned: a contributor who clones the project installs it once withplesty 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 theplestynamespace (import plesty.foo_bar)Dynamic versioning via
versioningit, with[tool.hatch.version.next-version]pointing atpackaging/versioning.py: a build between two tags is named for the release its branch is working towards — the next minor onexpandmain, the next patch onmaintain/0.N. An older module adopts the same rule withplesty update --versioning.plesty-libas 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 toPLESTY_LIB_VERSIONinplesty/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-versionoverrides all of that and skips the lookup — for a tutorial, whose lesson text and reader’spyproject.tomlhave 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, andplesty-sdkunder[dependency-groups] dev(included automatically byuv run— no--extra devneeded)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 |
|---|---|---|
|
Target device host / IP |
yes |
|
Target device port |
yes |
|
ZMQ port the device server binds (default |
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"]
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