Python API¶
The pdk package can be used as a library, for example to validate views from another tool or a CI pipeline.
This reference is generated from the docstrings in the source code. The command line and the MCP server are the
supported interfaces; the Python API may change between minor versions until 1.0.
Example: validate a view from Python.
from pathlib import Path
from pdk.bundle import load_bundle
from pdk.diagnostics import render
from pdk.validate import validate_file
result = validate_file(Path("view.json"), load_bundle())
print(render(result.diagnostics, title="view.json"))
errors = [d for d in result.diagnostics if d["severity"] == "error"]
Validation¶
Validate raw view.json against the exact schemas of the installed Perspective version.
- class pdk.validate.FileResult(path: 'Path', diagnostics: 'list[Diagnostic]', project: 'Project | None' = None, view_path: 'str | None' = None)[source]¶
- Parameters:
path (Path)
diagnostics (list[Diagnostic])
project (Project | None)
view_path (str | None)
- pdk.validate.validate_text(text, bundle, *, project=None, view_path=None, profile=None, compiler=None)[source]¶
With compiler (pdk.sync.compiler: the gateway’s Jython), a view without errors also gets its scripts compiled.
- Parameters:
text (str)
bundle (Bundle)
project (Project | None)
view_path (str | None)
profile (str | None)
compiler (Callable[[str], list[Diagnostic]] | None)
- Return type:
list[Diagnostic]
Diagnostics: the stable code table, construction, ordering and budgeted rendering.
- pdk.diagnostics.ordered(diagnostics)[source]¶
Deduplicate and order: severity, then document position, then code.
- Parameters:
diagnostics (list[Diagnostic])
- Return type:
list[Diagnostic]
- pdk.diagnostics.render(diagnostics, *, title, budget=DEFAULT_BUDGET, focus=None)[source]¶
Compact, budgeted text for agents. focus expands matching diagnostics with their detail.
- Parameters:
diagnostics (list[Diagnostic])
title (str)
budget (int)
focus (str | None)
- Return type:
str
Schema bundle: extraction from the installed Perspective module and loading for validation.
A bundle is a verbatim copy of the JSON resources Perspective itself ships (component descriptors, schemas/, the Designer’s DOM event list) plus a manifest. It lives under schemas/<perspective-version>/ and is not versioned: regenerate it with pdk schemas extract.
- exception pdk.bundle.BundleError[source]¶
The schema bundle is missing, incomplete or cannot be extracted.
- pdk.bundle.extract_bundle(modl_path, out_root, ignition_home=None)[source]¶
Copy Perspective’s schema resources from modl_path into out_root/<version>/.
- Parameters:
modl_path (Path)
out_root (Path)
ignition_home (Path | None)
- Return type:
Path
- class pdk.bundle.Component(id: 'str', name: 'str', category: 'str', tooltip: 'str', schema: 'dict[str, Any]', child_position_schema: 'dict[str, Any] | None', events: 'dict[str, dict[str, Any]]', deprecated: 'bool', default_meta_name: 'str | None', library: 'str', variants: 'tuple[dict[str, Any], ...]' = (), extension_functions: 'dict[str, dict[str, Any]]' = <factory>)[source]¶
- Parameters:
id (str)
name (str)
category (str)
tooltip (str)
schema (dict[str, Any])
child_position_schema (dict[str, Any] | None)
events (dict[str, dict[str, Any]])
deprecated (bool)
default_meta_name (str | None)
library (str)
variants (tuple[dict[str, Any], ...])
extension_functions (dict[str, dict[str, Any]])
- pdk.bundle.is_typed_value(value, tag=None)[source]¶
Ignition’s typed JSON encoding, e.g. a Date: {“$”: [“ts”, 0, 1757428595830], “$ts”: 1757428589273}.
- Parameters:
value (Any)
tag (str | None)
- Return type:
bool
- class pdk.bundle.Functions(expression, script, ignition=None)[source]¶
What code may call, as read from a running gateway (pdk schemas functions): expression functions by lower-case name, and the names under system.* (functions and the modules on the way to them).
- Parameters:
expression (dict[str, str])
script (frozenset[str])
ignition (str | None)
- pdk.bundle.load_functions(base)[source]¶
The functions.json next to a bundle, or None (offline checks of function names are then skipped).
- Parameters:
base (Path)
- Return type:
Functions | None
- pdk.bundle.save_functions(base, listing, ignition)[source]¶
Write the gateway’s /functions listing next to the bundle.
- Parameters:
base (Path)
listing (dict[str, Any])
ignition (str | None)
- Return type:
Path
- class pdk.bundle.Bundle(version: 'str', root: 'Path', manifest: 'dict[str, Any]', components: 'dict[str, Component]', schemas: 'dict[str, dict[str, Any]]', dom_events: 'dict[str, str]', registry: 'Registry', validator_class: 'type[Validator]' = <class 'jsonschema.validators.Draft4Validator'>, functions: 'Functions | None' = None, _validators: 'dict[int, Validator]'=<factory>)[source]¶
- Parameters:
Catalog: compact, budgeted answers about components, props, bindings and events, from the exact schemas.
- pdk.catalog.find_component(bundle, name)[source]¶
By id (ia.display.label), short id (label, display.label), display name (Label), or the start of a short id or display name when only one component has it (numeric entry -> ia.input.numeric-entry-field).
Read-only view of an Ignition project directory: views, their params and style classes.
- class pdk.project.ViewParam(name: 'str', direction: 'str', default: 'Any')[source]¶
- Parameters:
name (str)
direction (str)
default (Any)
- class pdk.project.Project(root: 'Path', parent: 'Project | None' = None, _views: 'dict[str, Path] | None'=None, _params: 'dict[str, dict[str, ViewParam]]'=<factory>, _stylesheet_classes: 'set[str] | None' = None, _named_queries: 'list[str] | None' = None)[source]¶
- Parameters:
Gateway¶
Client for the pdk-gateway module: loopback HTTP plus the token the module writes on every startup.
- exception pdk.gateway.client.GatewayError(code, message, *, status=None, payload=None)[source]¶
A failed call to the module, with a stable G-code, a hint and the module’s JSON payload.
- Parameters:
code (str)
message (str)
status (int | None)
payload (dict[str, Any] | None)
- pdk.gateway.client.read_token(ignition_home=None)[source]¶
The current token: PDK_GATEWAY_TOKEN, else the file the module rewrites on every startup.
- Parameters:
ignition_home (Path | None)
- Return type:
str | None
- pdk.gateway.client.read_token_file(ignition_home=None)[source]¶
The token the module wrote on its last startup, ignoring PDK_GATEWAY_TOKEN.
- Parameters:
ignition_home (Path | None)
- Return type:
str | None
- pdk.gateway.client.unwrap_arrays(node)[source]¶
Module builds before 2026-10-05 leave qualified arrays as {“$a”: […]} and their quality paths as x.$a[i].
- Parameters:
node (Any)
- Return type:
Any
- class pdk.gateway.client.Gateway(url: 'str' = <factory>, token: 'str | None' = None, timeout: 'float' = 30.0)[source]¶
- Parameters:
url (str)
token (str | None)
timeout (float)
- health(refresh=False)[source]¶
The module’s /health, asked once per instance unless refresh is set.
- Parameters:
refresh (bool)
- Return type:
dict[str, Any]
- resource(project, type_key, path='')[source]¶
A resource with its files and signature, or the listing of a folder.
- Parameters:
project (str)
type_key (str)
path (str)
- Return type:
dict[str, Any]
- put_resource(project, type_key, path, files, signature=None, scope=None)[source]¶
Create (signature None) or modify (the signature you read). A stale signature raises G008.
- Parameters:
project (str)
type_key (str)
path (str)
files (dict[str, str])
signature (str | None)
scope (str | None)
- Return type:
dict[str, Any]
- changes(since=0, limit=200)[source]¶
Sandbox resource changes after sequence since, each with origin pdk or external.
- Parameters:
since (int)
limit (int)
- Return type:
dict[str, Any]
- catalog(full=False)[source]¶
Components registered in the gateway; full adds schemas, events and palette.
- Parameters:
full (bool)
- Return type:
dict[str, Any]
- sessions(project)[source]¶
Client sessions of a project, with their pages and the view instances on each.
- Parameters:
project (str)
- Return type:
dict[str, Any]
- close_session(project, page_id)[source]¶
Close the sandbox session that owns a page, so it does not hold a session slot until it times out.
- Parameters:
project (str)
page_id (str)
- Return type:
dict[str, Any]
- functions()[source]¶
Expression functions ({name, category}) and system.* scripting names registered on the gateway.
- Return type:
dict[str, Any]
- compile_scripts(sources)[source]¶
[{id, code}] compiled (never run) with the gateway’s Jython: [{id, ok, line?, column?, message?}].
- Parameters:
sources (list[dict[str, str]])
- Return type:
list[dict[str, Any]]
- page(project, page_id)[source]¶
The evaluated tree of every view on a page: plain values, non-Good qualities and configured bindings.
- Parameters:
project (str)
page_id (str)
- Return type:
dict[str, Any]
- logs(since=0, limit=200, project=None, view=None)[source]¶
WARN/ERROR gateway log events after since, filtered by project and view (resource path) via the MDC.
- Parameters:
since (int)
limit (int)
project (str | None)
view (str | None)
- Return type:
dict[str, Any]
- tags(path=None, depth=1)[source]¶
Nodes under a tag folder (any provider), flat, with data type, value and quality.
- Parameters:
path (str | None)
depth (int)
- Return type:
dict[str, Any]
- read_tags(paths)[source]¶
Value, quality and timestamp of each tag path (any provider).
- Parameters:
paths (list[str])
- Return type:
dict[str, Any]
- write_tags(writes)[source]¶
[{“path”, “value”, “quality”?}] under PDK/: the write result and the read-back of each.
- Parameters:
writes (list[dict[str, Any]])
- Return type:
dict[str, Any]
- import_tags(path, tags)[source]¶
Tag export JSON (folders and memory tags) into a folder under PDK/, overwriting what is there.
- Parameters:
path (str)
tags (dict[str, Any])
- Return type:
dict[str, Any]
- alarms(path=None)[source]¶
Alarm events of the tags at or under a path (default PDK/).
- Parameters:
path (str | None)
- Return type:
dict[str, Any]
Build, install and restart the pdk-gateway module.
The build runs javac –release 17 from any JDK >= 17 against the jars of the installed gateway, so the module links against exactly the Ignition and Perspective it runs on, without Gradle. Installing and restarting go through the gateway’s own REST API with an Ignition API key that has the Gateway Write permission.
- exception pdk.gateway.module.ModuleError[source]¶
The module cannot be built, installed or restarted.
- class pdk.gateway.module.BuildResult(modl: 'Path', version: 'str', classes: 'int')[source]¶
- Parameters:
modl (Path)
version (str)
classes (int)
- pdk.gateway.module.module_version(now=None)[source]¶
<pdk version>.<yyDDDHHmm>: the build number grows with every build and fits Ignition’s int.
A version between releases (0.2.1.dev3+g1a2b3c4) keeps only its X.Y.Z.
- Parameters:
now (datetime | None)
- Return type:
str
- pdk.gateway.module.find_javac()[source]¶
A javac from a JDK >= 17: JAVA_HOME, then PATH, then the usual install folders.
- Return type:
Path
- pdk.gateway.module.compile_classpath(ignition_home, cache)[source]¶
The installed platform jars plus Perspective’s jars, unpacked from its .modl into cache.
- Parameters:
ignition_home (Path)
cache (Path)
- Return type:
list[Path]
- pdk.gateway.module.argfile_text(args)[source]¶
javac @file syntax: one quoted argument per line (paths already use forward slashes).
- Parameters:
args (Sequence[str])
- Return type:
str
- pdk.gateway.module.build(ignition_home=None, out_dir=BUILD_DIR, now=None)[source]¶
Compile gateway/src/main/java and package an unsigned .modl in out_dir.
- Parameters:
ignition_home (Path | None)
out_dir (Path)
now (datetime | None)
- Return type:
- pdk.gateway.module.install(modl, base=None)[source]¶
Upload and install the module. It loads on the next gateway restart (8.3 has no hot reload).
- Parameters:
modl (Path)
base (str | None)
- Return type:
dict[str, Any]
- pdk.gateway.module.accept_certificate(base=None, key=None)[source]¶
Accept the (empty) certificate of the unsigned module after every upload.
Each upload clears the acceptance; without it the next start stops at commissioning and quarantines the module. Needs -Dignition.allowunsignedmodules=true, otherwise the gateway refuses unsigned modules.
- Parameters:
base (str | None)
key (str | None)
- Return type:
None
- pdk.gateway.module.gateway_state(base, timeout=5)[source]¶
RUNNING, STARTING…, or COMMISSIONING when the gateway waits for someone to accept a new module.
- Parameters:
base (str)
timeout (float)
- Return type:
str | None
- pdk.gateway.module.restart(base=None, timeout=300, poll=2.0)[source]¶
Restart the gateway, wait for RUNNING, then for the module’s /health with its new token.
- Parameters:
base (str | None)
timeout (float)
poll (float)
- Return type:
dict[str, Any]
Pull, deploy, promote and delete views through the pdk-gateway module.
A pulled view lives in work/<project>/<view path>/view.json, next to a .pdk-base.json sidecar with the signature it was read at and its text. Every write sends that signature: if the view changed on the gateway since (a Designer save, another agent), the module answers 409 and nothing is written. The 409 is reported as a diff by JSON pointer — what changed on the gateway since the pull and what you changed — and the gateway version is saved as view.gateway.json to merge from. –resolved then writes over that newer version.
Agent work lives under PDK/ in the sandbox; promote copies PDK/X to X in the same project, signed the same way, or to the target project (-Dpdk.target), which takes finished views only: no PDK/, no preview page. Every deployed view gets a page at /pdk/<view path> in page-config, so it can be opened in a browser; page-config is edited additively with its own signature and re-read on a 409.
- pdk.sync.pull(gateway, path, project=None, *, force=False, root=None)[source]¶
Download a view, or every view below a folder, with its signature. path may be a local copy.
- pdk.sync.deploy(gateway, location, bundle, *, resolved=False, root=None, budget=DEFAULT_BUDGET, _promoting=False)[source]¶
Validate the local view.json and write it with the signature of the pull (create if never pulled).
The target project only gets views through promote, and never a preview page. The stylesheet goes to pdk.stylesheet.
- pdk.sync.compiled(gateway, text, *, strict=False)[source]¶
E009 for the scripts of a view that the gateway’s Jython does not compile. Without strict, nothing when the gateway cannot compile (an older module without the route); with it, that GatewayError propagates.
- Parameters:
gateway (Gateway)
text (str)
strict (bool)
- Return type:
list[Diagnostic]
- pdk.sync.compiler(gateway)[source]¶
For validate’s compiler: the scripts of a view compiled by the gateway (GatewayError if it cannot).
- Parameters:
gateway (Gateway)
- Return type:
Callable[[str], list[Diagnostic]]
- pdk.sync.promote(gateway, location, bundle, *, to=None, to_project=None, resolved=False, root=None, budget=DEFAULT_BUDGET)[source]¶
Copy a deployed sandbox view PDK/X to X (or to) in the same project, with the target’s signature.
With to_project (“target” or its name) the copy goes to the target project instead; there it may not reference PDK/ views, and views it references that the target lacks are reported.
The target’s own sidecar holds the signature it was promoted or pulled at: a target someone edited since gives the usual 409; a target that exists but was never pulled gives a create conflict.
- pdk.sync.delete(gateway, location, *, resolved=False, root=None, budget=DEFAULT_BUDGET)[source]¶
Delete a pulled view on the gateway, only if it is still the version you pulled.
- pdk.sync.page_url(view_path)[source]¶
The preview page of a view: /pdk/<view path>, blanks turned into dashes.
- Parameters:
view_path (str)
- Return type:
str
- pdk.sync.ensure_preview(gateway, project, view_path)[source]¶
Map /pdk/<view path> to the view in page-config; a warning if it cannot.
- Parameters:
gateway (Gateway)
project (str)
view_path (str)
- Return type:
str | None
- pdk.sync.json_diff(a, b, pointer='')[source]¶
(pointer, old, new) for every leaf that differs; MISSING marks an added or removed member.
- Parameters:
a (Any)
b (Any)
pointer (str)
- Return type:
list[tuple[str, Any, Any]]
- pdk.sync.overlapping(theirs, mine, theirs_doc, mine_doc)[source]¶
Pointers both sides changed (one inside the other) to different results.
- Parameters:
theirs (list[tuple[str, Any, Any]])
mine (list[tuple[str, Any, Any]])
theirs_doc (Any)
mine_doc (Any)
- Return type:
list[str]
- pdk.sync.diff_section(title, changes, before, after)[source]¶
One line per change, with the component it falls in (named from the side where it exists).
- Parameters:
title (str)
changes (list[tuple[str, Any, Any]])
before (Any)
after (Any)
- Return type:
str
Pull, deploy and delete the project stylesheet through the pdk-gateway module.
On the gateway it is a singleton resource (type com.inductiveautomation.perspective/stylesheet, path “”, one file stylesheet.css). Locally it lives in work/<project>/_stylesheet/stylesheet.css next to the same .pdk-base.json sidecar views use: every write sends the signature of the pull, and a 409 saves the gateway version as stylesheet.gateway.css and reports a unified diff. It is edited in the sandbox; the target project (-Dpdk.target) takes it only through promote –to-project.
- pdk.stylesheet.is_stylesheet(location)[source]¶
A stylesheet.css, its _stylesheet folder, or any folder whose sidecar says so.
- Parameters:
location (Path)
- Return type:
bool
- pdk.stylesheet.check_css(text)[source]¶
Syntax problems that break a stylesheet: unterminated comment or string, unbalanced braces.
- Parameters:
text (str)
- Return type:
list[str]
- pdk.stylesheet.pull(gateway, project=None, *, force=False, root=None, path=None)[source]¶
Download the stylesheet with its signature. path may be a local copy, which names the project.
- pdk.stylesheet.deploy(gateway, location, *, resolved=False, root=None, budget=DEFAULT_BUDGET, _promoting=False)[source]¶
Check the local stylesheet.css and write it with the signature of the pull (create if never pulled).
- pdk.stylesheet.promote(gateway, location, *, to_project=None, resolved=False, root=None, budget=DEFAULT_BUDGET)[source]¶
Copy the deployed sandbox stylesheet to the target project, with the target’s own signature.
- pdk.stylesheet.delete(gateway, location, *, resolved=False, root=None, budget=DEFAULT_BUDGET)[source]¶
Delete the stylesheet on the gateway, only if it is still the version you pulled.
The module imports Ignition’s own tag export JSON (folders, memory tags, UDT definitions under _types_/PDK/ with memory members and instances of them), writes values with a quality and reads them back, lists the alarm events of those tags, and stores synthetic history points for them. Paths without [provider] hang from PDK/: Sandbox/Setpoint is [default]PDK/Sandbox/Setpoint; _types_/PDK/… is where UDT definitions go. Reads take any path; writes only PDK/ and _types_/PDK/ (G013).
- pdk.tags.full_path(path)[source]¶
Sandbox/Setpoint -> PDK/Sandbox/Setpoint; paths with [provider] or already under PDK/ stay as they are.
- Parameters:
path (str)
- Return type:
str
- pdk.tags.parse_value(raw)[source]¶
A written or expected value: JSON numbers, true/false and null; anything else is a string.
- Parameters:
raw (str)
- Return type:
Any
- pdk.tags.matches(actual, raw)[source]¶
A read value against the expected text: numbers within float precision, else equal as text.
- Parameters:
actual (Any)
raw (str)
- Return type:
bool
- pdk.tags.write(gateway, path, raw, quality=None)[source]¶
Write one tag (raw None keeps its current value, to change only the quality); raises ValueError with the reason when the gateway does not take it.
- Parameters:
gateway (Gateway)
path (str)
raw (str | None)
quality (str | None)
- Return type:
dict[str, Any]
- class pdk.tags.Probe(gateway)[source]¶
Tag reads for expectations, cached for one inspect.
- Parameters:
gateway (Gateway)
- pdk.tags.apply(gateway, file, to=FOLDER)[source]¶
Import a tag export JSON file into to (under PDK/): tags are created, or overwritten with their declared values, and their alarm events acknowledged, which resets them between runs.
- Parameters:
gateway (Gateway)
file (Path)
to (str)
- Return type:
str
- pdk.tags.duration_ms(text)[source]¶
500ms, 30s, 1m, 2h, 1d (a bare number is seconds) in milliseconds.
- Parameters:
text (str)
- Return type:
int
- pdk.tags.history(gateway, path, values, *, every='1m', provider=None, quality=None, end_ms=None)[source]¶
Store values as the history of a tag under PDK/, one every every, the last one now (or at end_ms), in provider (the gateway’s only history provider when not given).
- Parameters:
gateway (Gateway)
path (str)
values (list[str])
every (str)
provider (str | None)
quality (str | None)
end_ms (int | None)
- Return type:
str
The live catalog: what the running gateway’s Perspective registers, against the offline bundle.
pdk schemas sync compares every registered component with the descriptor the bundle extracted from the installed .modl (drift), and writes the components the bundle lacks — third-party modules — to components/live.components.json inside the bundle, where the loader already picks them up.
- class pdk.live.Drift(version: 'str', built_in: 'int' = 0, bundled: 'int' = 0, differing: 'dict[str, list[str]]'=<factory>, missing: 'list[str]' = <factory>, live_only: 'list[dict[str, Any]]'=<factory>, without_schema: 'list[str]' = <factory>, built_in_ids: 'set[str]' = <factory>)[source]¶
- Parameters:
version (str)
built_in (int)
bundled (int)
differing (dict[str, list[str]])
missing (list[str])
live_only (list[dict[str, Any]])
without_schema (list[str])
built_in_ids (set[str])
- pdk.live.offline_descriptors(bundle_dir)[source]¶
id -> (file name, raw descriptor) as extracted from the .modl, without the live file.
- Parameters:
bundle_dir (Path)
- Return type:
dict[str, tuple[str, dict[str, Any]]]
- pdk.live.comparable(descriptor, declared=None)[source]¶
The parts validation relies on, normalized so absent and empty mean the same on both sides.
The registry fills metadata the file leaves out (a derived name or defaultMetaName, category “unknown”) and marks components hidden from the palette (showOnPalette: false) as deprecated: metadata is compared only where the offline descriptor (declared) states it.
- Parameters:
descriptor (dict[str, Any])
declared (dict[str, Any] | None)
- Return type:
dict[str, Any]
- pdk.live.sync(gateway, root=None, budget=DEFAULT_BUDGET)[source]¶
Compare the gateway’s components with the bundle of its Perspective version; write the live-only ones.
- Parameters:
gateway (Gateway)
root (Path | None)
budget (int)
- Return type:
tuple[bool, str]
- pdk.live.changes_text(page)[source]¶
One line per change event, for pdk gateway changes.
- Parameters:
page (dict[str, Any])
- Return type:
str
Eyes¶
Open, inspect and screenshot a deployed view: the eyes’ entry points for the CLI and the MCP server.
The view.json joined with the page is the one the gateway has (what is rendered), not the local copy; when the local copy differs, the report says the edits are not deployed. The gateway log cursor is taken before the page (re)loads, so startup scripts that fail are caught. Bindings start Uncertain until their first value arrives: the gateway tree is read again for a short while until no quality is Uncertain.
- class pdk.eyes.inspect.Target(project: 'str', path: 'str', view: 'dict[str, Any]', note: 'str | None')[source]¶
- Parameters:
project (str)
path (str)
view (dict[str, Any])
note (str | None)
- class pdk.eyes.inspect.Result(ok: 'bool', text: 'str', errors: 'int' = 0)[source]¶
- Parameters:
ok (bool)
text (str)
errors (int)
- pdk.eyes.inspect.resolve(gateway, location)[source]¶
A local view (folder or view.json under work/) or a view path in the sandbox, read from the gateway.
- pdk.eyes.inspect.session_closer(gateway)[source]¶
Eyes’ release: close the sandbox sessions of the browser’s pages on the gateway (best effort, so a module without the route only leaves them to time out).
- Parameters:
gateway (Gateway)
- Return type:
Any
- class pdk.eyes.inspect.Popup(id: 'str', path: 'str', view: 'dict[str, Any]', extracted: 'dict[str, Any]')[source]¶
- Parameters:
id (str)
path (str)
view (dict[str, Any])
extracted (dict[str, Any])
- pdk.eyes.inspect.open_view(eyes, gateway, location, viewport=None)[source]¶
Open the view’s preview in a fresh tab and report whether it renders.
- pdk.eyes.inspect.inspect_view(eyes, gateway, location, viewport=None, *, focus=None, budget=DEFAULT_BUDGET, query=None, expect=None, act=None)[source]¶
Reload the view and report its anomalies: rendering, gateway qualities, logged script errors, console; with act, after performing those steps (pdk.eyes.act); with query, also what was measured of the matching components (measure.describe); with expect, the expectations that fail, as A diagnostics (expect.check). Several viewports (comma-separated, or presets like phone) give one report each, with the budget shared; the sessions of the extra ones are closed afterwards.
- pdk.eyes.inspect.screenshot(eyes, gateway, location, viewport=None, component=None, act=None, compare=None)[source]¶
PNG of the view (opened afresh) or of one component, by name path (root/Panel/Label), DOM path (C.0:1) or, once act has opened it, popup:<id>/root/… With act, the steps (pdk.eyes.act) run before capturing: a step that cannot be read comes back as A005, one that cannot be done as A004, and neither gives a PNG. With compare (the bytes of a PNG taken before), the text ends with the comparison line (see compare_screenshot).
- pdk.eyes.inspect.compare_screenshot(eyes, gateway, location, before, viewport=None, component=None, act=None)[source]¶
screenshot, compared with before (a PNG of the same view, viewport and component): (png, diff_png, text). diff_png is the new PNG with the changed pixels in red; None when nothing changed or the sizes differ.
Chromium side of the eyes: readiness, stability, DOM extraction, console and screenshots.
Ported from perspective-compiler’s runtime harness (open_ready_page, geometry stability, extraction) without the IR, hashes or artifacts. Playwright’s sync API is bound to the thread that started it and refuses to run inside an asyncio loop (the MCP server has one), so every browser call runs on one dedicated worker thread owned by Eyes. One Chromium and one context per viewport stay open between calls: Maker caps concurrent Perspective sessions, and a kept context keeps its session; every open is a new tab, so a new Perspective page whose views start afresh.
- exception pdk.eyes.browser.EyesError(status, message, evidence=None)[source]¶
Opening or reading the page failed; status is one of the constants above.
- Parameters:
status (str)
message (str)
evidence (dict[str, Any] | None)
- class pdk.eyes.browser.Viewport(width: 'int', height: 'int')[source]¶
- Parameters:
width (int)
height (int)
- class pdk.eyes.browser.Opened(url, viewport, page, page_id, readiness_ms, maker_notice, console=<factory>, endless=<factory>)[source]¶
A ready page. Only the worker thread may touch page.
- Parameters:
url (str)
viewport (Viewport)
page (Any)
page_id (str | None)
readiness_ms (float)
maker_notice (bool)
console (list[dict[str, str]])
endless (list[str])
- class pdk.eyes.browser.Eyes(headless=True, idle_s=None, release=None)[source]¶
A long-lived headless Chromium; every method blocks until its work on the browser thread is done.
With idle_s, the browser closes after that long without work: an open page keeps its Perspective session connected (Maker caps them per gateway), and a long-lived server would otherwise hold it forever. The next call starts it again. A browser that goes away only disconnects its sessions, which then hold their slots until they time out: release(page_ids), called before the contexts close with one page of each session, closes them on the gateway.
- Parameters:
headless (bool)
idle_s (float | None)
release (Callable[[list[str]], None] | None)
- open(url, viewport=None, *, fresh=True)[source]¶
Open url in a new tab (replacing the one already open, unless fresh is False) and wait until it is ready and stable.
- extract(opened, root=ROOT_PATH)[source]¶
Boxes, text and overflow of every component under the root (C.0, or P<id>.0 for a popup), after the geometry is stable again.
- Parameters:
opened (Opened)
root (str)
- Return type:
dict[str, Any]
- location(opened)[source]¶
The page’s URL now (a step may have navigated it).
- Parameters:
opened (Opened)
- Return type:
str
- popups(opened)[source]¶
Ids of the popups open on the page, in stacking order.
- Parameters:
opened (Opened)
- Return type:
list[str]
- act(opened, steps, pointers, resolve=None, tags=None)[source]¶
Perform pdk.eyes.act steps on the page, letting it settle after each one.
- Parameters:
opened (Opened)
steps (list[Any])
pointers (dict[str, str])
resolve (Callable[[str], str | None] | None)
tags (Callable[[Any], Any] | None)
- Return type:
Any
- screenshot(opened, component=None)[source]¶
PNG of the viewport, or of one component by its DOM path (e.g. C.0:3:0).
- Parameters:
opened (Opened)
component (str | None)
- Return type:
bytes
- compare(opened, before, after, threshold=32, cell=16)[source]¶
Pixel comparison of two PNGs of the same view, in the page (COMPARE_JS): changed of total pixels differ by more than threshold in one channel, regions boxes them (largest first, at most 20; region_count is the real number) and diff_png is the after image with the changes in red. Sizes that differ come back as size_before and size_after only.
- Parameters:
opened (Opened)
before (bytes)
after (bytes)
threshold (int)
cell (int)
- Return type:
dict[str, Any]
- pdk.eyes.browser.classify_state(state)[source]¶
A terminal failure visible in the page state, or None while it may still become ready.
- Parameters:
state (dict[str, Any])
- Return type:
tuple[str, str] | None
- pdk.eyes.browser.geometry_delta(previous, current)[source]¶
The largest move of any component present in both samples, and its path.
- Parameters:
previous (dict[str, list[float]])
current (dict[str, list[float]])
- Return type:
tuple[float, str | None]
Join of the three views of one component: the local view.json, the DOM and the gateway’s evaluated tree.
All three address components by child index from the root: the view.json pointer /root/children/1/children/0, the gateway address path 0:1:0 and the DOM path <mountPath>.0:1:0 (C.0:1:0 for the routed view). Descendants of embedded views carry a $ in the DOM path, docks a T[/L[/R[/B[ prefix; neither belongs to the inspected view.
- class pdk.eyes.join.Part(node, dom=None, live=None, mount='C', prefix='')[source]¶
One component of the view.json and what was observed of it.
- Parameters:
node (Node)
dom (dict[str, Any] | None)
live (dict[str, Any] | None)
mount (str)
prefix (str)
- class pdk.eyes.join.Joined(parts: 'list[Part]', view: 'dict[str, Any]', live_view: 'dict[str, Any] | None', viewport: 'dict[str, int]', by_dom: 'dict[str, Part]'=<factory>, by_name: 'dict[str, Part]'=<factory>, mismatches: 'list[str]' = <factory>, foreign: 'int' = 0, mount: 'str' = 'C', popups: 'dict[str, str]'=<factory>, url: 'str' = '', routed: 'str | None' = None)[source]¶
- pdk.eyes.join.live_view(page, view_path, mount=MOUNT)[source]¶
The instance of view_path mounted at mount (C routed, P<id> popup), if the gateway reported it.
- Parameters:
page (dict[str, Any] | None)
view_path (str)
mount (str)
- Return type:
dict[str, Any] | None
- pdk.eyes.join.mounted_view(page, mount=MOUNT)[source]¶
The view the gateway lists at mount on the page (C: the routed view), if it reported the page.
- Parameters:
page (dict[str, Any] | None)
mount (str)
- Return type:
str | None
- pdk.eyes.join.mounted_by_design(part)[source]¶
False when the component may legitimately be absent from the DOM.
- Parameters:
part (Part)
- Return type:
bool
Anomaly rules over a joined view: geometry (V), gateway qualities (Q), gateway log (L), browser console (C) and the state of the eyes themselves (X). They produce the validator’s Diagnostic, pointing into the local view.json, so pdk.diagnostics.render budgets and focuses them the same way.
The geometry rules are perspective-compiler’s VIS001/002/003/007 on the joined model, with two changes: text cut with an ellipsis counts (it hides information too), and a collapse is any component with content shrunk to zero inside a parent that has room, not only leaves.
- pdk.eyes.rules.scrolls(dom, axis)[source]¶
The component scrolls along x or y (overflow auto or scroll) instead of clipping or spilling.
- Parameters:
dom (dict[str, Any])
axis (str)
- Return type:
bool
- pdk.eyes.rules.scroll_extent(dom, axis)[source]¶
How far the content runs past the component’s content box along x or y.
- Parameters:
dom (dict[str, Any])
axis (str)
- Return type:
float
- pdk.eyes.rules.text_clipped(joined)[source]¶
V001: visible text of a label, button or link runs past the rectangle that clips it. Along an axis a scroll container scrolls, text past its edge is scrolled out of view, not cut (cut_rect leaves those axes out).
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.component_overflow(joined)[source]¶
V002: a container’s children overflow its content box, along an axis it does not scroll.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.viewport_overflow(joined)[source]¶
V003: the view’s content does not fit the viewport; reported on the deepest component that sticks out.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.collapsed(joined)[source]¶
V007: a component with content is rendered with zero width or height inside a parent that has room.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.not_mounted(joined)[source]¶
V010: a component of the view.json is not in the page (only the topmost missing one is reported).
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.failed_views(joined)[source]¶
V011: an embedded view or repeater shows Perspective’s failed-view placeholder instead of its view.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.unrequested_scroll(joined, declared=None)[source]¶
V014: a container scrolls (its content overflows along an axis with overflow auto or scroll), or shows a scrollbar with nothing to scroll (overflow scroll), and no scrolls expectation declares it. A scroll hides content the way V002 reports, so one the view did not ask for is usually V002 silenced. It is an error when it is plainly not a scrolling area: nested in another scroll along the same axis, or for a sliver of content.
- Parameters:
joined (Joined)
declared (Mapping[str, Set[str]] | None)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.repeater_overflow(joined)[source]¶
V012: a flex repeater’s instances are bigger than the repeater along the axis the instance count does not explain (the cross axis without wrap, the main axis with it), so each one scrolls inside a strip of it.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.contrast_ratio(color, background)[source]¶
WCAG contrast of a text color (its alpha composited over the background) on an opaque background.
- Parameters:
color (str | None)
background (str | None)
- Return type:
float | None
- pdk.eyes.rules.low_contrast(joined)[source]¶
V013: text whose color is too close to what is behind it (WCAG AA: 4.5:1, or 3:1 for large text), on the components whose text is theirs to color (CONTRAST_TYPES), when the view chose those colors (the theme’s defaults are not judged). Disabled controls are exempt, and text over a background image is not judged.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.qualities(joined)[source]¶
Q001/Q002/Q003: a property the gateway evaluated is Bad, Error or Uncertain, on the binding that feeds it.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.overlays(joined)[source]¶
Without the gateway tree: Q001/Q002 from the quality overlay Perspective draws over a bad binding.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.logged(joined, events)[source]¶
L001: the gateway logged a script error for this view, attributed to the component in the MDC.
- Parameters:
joined (Joined)
events (list[dict[str, Any]])
- Return type:
list[Diagnostic]
- pdk.eyes.rules.console(entries)[source]¶
C001: an uncaught exception or a console error in the browser.
- Parameters:
entries (list[dict[str, str]])
- Return type:
list[Diagnostic]
- pdk.eyes.rules.degraded(reasons)[source]¶
X001: part of the eyes is missing, so some anomalies cannot be seen.
- Parameters:
reasons (list[str])
- Return type:
list[Diagnostic]
- pdk.eyes.rules.out_of_sync(joined)[source]¶
X002: the open page does not render this view.json, so nothing else can be trusted.
- Parameters:
joined (Joined)
- Return type:
list[Diagnostic]
- pdk.eyes.rules.evaluate(joined, *, logs=None, console_entries=None, degraded_reasons=None, declared_scrolls=None)[source]¶
Every anomaly of one inspection. A page out of sync with the view.json reports only that, and what happens inside a collapsed component is reported once, as that component’s V007. declared_scrolls maps component names to the axes scrolls expectations ask for (expect.declared_scrolls); V014 leaves those alone.
- Parameters:
joined (Joined)
logs (list[dict[str, Any]] | None)
console_entries (list[dict[str, str]] | None)
degraded_reasons (list[str] | None)
declared_scrolls (Mapping[str, Set[str]] | None)
- Return type:
list[Diagnostic]
What was measured of selected components: one compact line each, so an agent can check a layout without a screenshot.
A query is a comma-separated list of terms: a name path (root/Header matches it and its subtree), a component type (ia.display.label), a pointer prefix (/root/children/0) or * for every component. Values that are at their neutral default (no padding, no border, transparent background, no gap) are left out; coordinates are viewport pixels.
Expectations about a rendered view, checked against what was measured (A001-A003).
One expectation per ; or line, <component> <relation> [args], components by name path (quote names with spaces):
root/Subtitle below root/Title [gap 8] also above, left-of, right-of; gap >= 8, gap 8..16 root/Log/Last inside root/Log within its box, or its content along an axis it scrolls root/Save aligned right root/Form edges: left right top bottom center-x center-y root/Table width 100% width height x y; =, >=, <=, >, <; px or % of the parent’s content root/Pump x 150 x and y from the parent’s inner top-left (coord’s position.x/y) root/Title text “Pump 1” text ~ “Pump” for contains; embedded view: its text; input: its value root/Alarm visible or hidden (rendered with a size, wherever it is scrolled to) root/Save enabled or disabled (the control, or the input inside it) popup detail open or closed; inside an open popup: popup:detail/root/Name text “P-1” root/Log scrolls [y] its content overflows along x/y (either) and it scrolls there root/Log/Last in-view wholly in sight: not scrolled away, clipped, or off the viewport root/Title color #323232 color, bg (hex, rgb(), rgba()), font-size [op] N, font-weight N root/Bar prop value >= 40 a property as the gateway evaluated it: props, or position/custom/meta view prop custom.total = 3 the routed view’s params or custom, from the gateway’s entry for it page /detail the page’s URL path (page ~ /det contains); after a nav action page shows PDK/Detail the view the page shows (a nav action may swap it, URL or not) tag Sandbox/Setpoint = 42 a simulated tag’s value (read back from the gateway), under PDK/ tag Sandbox/Setpoint quality Bad_Stale its quality: a name, or a level (Good, Bad, Uncertain, Error) alarm North/Level active or cleared: alarm events of the tags at or under the path
Lengths compare with a 1px tolerance (sub-pixel layout); edges are viewport pixels of the component’s box, except x and y, which are measured from the parent’s padding box (the viewport for root), like position.x/y in a coord.
- class pdk.eyes.expect.Outcome(total: 'int', diagnostics: 'list[Diagnostic]')[source]¶
- Parameters:
total (int)
diagnostics (list[Diagnostic])
- pdk.eyes.expect.split(text)[source]¶
Expectations separated by ; or newlines, outside quotes.
- Parameters:
text (str)
- Return type:
list[str]
- pdk.eyes.expect.check(joined, text, tags=None)[source]¶
tags reads simulated tags and their alarms (pdk.tags.Probe) for the tag and alarm expectations.
- pdk.eyes.expect.declared_scrolls(text)[source]¶
The components scrolls expectations ask to scroll, with their axes (both without one): those scrolls are by design, so V014 does not report them, whether or not the expectation passes.
- Parameters:
text (str | None)
- Return type:
dict[str, set[str]]
- pdk.eyes.expect.page_location(url)[source]¶
A Perspective page’s path inside its project (/data/perspective/client/<project>/<path>, with the query), decoded; any other URL whole.
- Parameters:
url (str | None)
- Return type:
str
- pdk.eyes.expect.normalize_color(value)[source]¶
Computed-style form: rgb(r, g, b) or rgba(r, g, b, a); hex and opaque rgba are converted.
- Parameters:
value (str)
- Return type:
str
Actions on an open view before it is measured: click, type, choose dropdown options, hover, scroll, slide, press keys, select a table row, wait (F5 part A), and set a simulated tag (part B).
Steps are written like expectations (; or newlines, components by name path):
click root/Save; type root/Name “Pump 2”; row root/Table 1; wait 500; click popup:detail/root/Ok select root/Mode “Auto”; slide root/Speed 40; scroll root/Feed bottom; key root/Name Enter; hover root/Help tag Sandbox/Setpoint 42; tag Sandbox/Message “en marcha”; tag Sandbox/Setpoint quality Bad_Stale
Tag paths without [provider] hang from PDK/; the view gets the new value through its subscription.
They run in the browser in order; after each one the page settles like after opening it. A step that cannot be done stops the sequence and comes back as A004 (with the reason); a step that cannot be read is A005.
- class pdk.eyes.act.Step(verb: 'str', text: 'str', component: 'str | None' = None, dom_path: 'str | None' = None, value: 'str | None' = None, number: 'int' = 0, tag: 'str | None' = None, quality: 'str | None' = None, labels: 'tuple[str, ...]' = (), key: 'str | None' = None, scroll: 'str | int | None' = None, level: 'float | None' = None)[source]¶
- Parameters:
verb (str)
text (str)
component (str | None)
dom_path (str | None)
value (str | None)
number (int)
tag (str | None)
quality (str | None)
labels (tuple[str, ...])
key (str | None)
scroll (str | int | None)
level (float | None)
- class pdk.eyes.act.Outcome(total: 'int', done: 'int', diagnostics: 'list[Diagnostic]')[source]¶
- Parameters:
total (int)
done (int)
diagnostics (list[Diagnostic])
- pdk.eyes.act.parse(text, nodes)[source]¶
Steps for the view’s components (name path -> node); malformed steps come back as A005.
- Parameters:
text (str)
nodes (dict[str, Node])
- Return type:
tuple[list[Step], list[Diagnostic]]
- pdk.eyes.act.run(page, steps, settle, pointers, resolve=None, tags=None)[source]¶
Perform the steps on the worker thread; settle(page) waits for a stable layout after each one, resolve(name) gives the DOM path of a popup:<id>/… component once its popup is open, and tags(step) writes a tag step through the gateway.
Environment¶
Per-machine settings: an optional TOML file whose values fill in the PDK environment variables that are unset.
Everything in PDK reads environment variables; this file only spares setting them for every shell, the MCP server and the hook. Default location ~/.pdk/config.toml, or PDK_CONFIG. Example:
gateway_url = “http://127.0.0.1:8088” ignition_home = “C:/Program Files/Inductive Automation/Ignition” api_key = “…” # or keep it in ~/.pdk/ignition-api-key
- pdk.config.load()[source]¶
The config file, the variables it set (unset ones only), and a problem reading it, if any.
- Return type:
tuple[Path, dict[str, str], str | None]
pdk doctor: check every prerequisite of a working PDK on this machine and say how to fix what is missing.
Ordered from the local install to the gateway; a check whose prerequisite failed is skipped. –fix applies only local, reversible fixes (extract the schema bundle, download Playwright’s Chromium). It never touches the gateway, ignition.conf or anything that needs admin rights or a restart: those are printed for the user to do.