benchweave-sdk 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchweave_sdk/__init__.py +8 -0
- benchweave_sdk/cli.py +361 -0
- benchweave_sdk/conformance.py +63 -0
- benchweave_sdk/console.py +57 -0
- benchweave_sdk/fixtures.py +289 -0
- benchweave_sdk/interfaces.py +51 -0
- benchweave_sdk/packaging.py +61 -0
- benchweave_sdk/presentation.py +404 -0
- benchweave_sdk/preview_assets/inventory.json +21 -0
- benchweave_sdk/preview_assets/site/assets/index-BqP_aR5c.css +1 -0
- benchweave_sdk/preview_assets/site/assets/index-D0qT06Ho.js +28 -0
- benchweave_sdk/preview_assets/site/index.html +14 -0
- benchweave_sdk/preview_models.py +103 -0
- benchweave_sdk/preview_server.py +305 -0
- benchweave_sdk/preview_tui.py +69 -0
- benchweave_sdk/py.typed +1 -0
- benchweave_sdk/scaffold.py +430 -0
- benchweave_sdk/standards/execution/_GENERATED.txt +12 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/bench.schema.json +476 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/commissioning.schema.json +277 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/examples/bench.json +158 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/examples/commissioning.json +158 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/examples/procedure.json +101 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/examples/run-binding.json +38 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/examples/run-record.json +25 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/examples/safety-policy.json +110 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/procedure.schema.json +498 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/run-binding.schema.json +176 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/run-record.schema.json +321 -0
- benchweave_sdk/standards/execution/execution-v1.0.0/safety-policy.schema.json +511 -0
- benchweave_sdk/standards/interface/_GENERATED.txt +6 -0
- benchweave_sdk/standards/interface/interface-v1.1.1/examples/mcp-start-exchange.json +63 -0
- benchweave_sdk/standards/interface/interface-v1.1.1/examples/operation-vectors.json +393 -0
- benchweave_sdk/standards/interface/interface-v1.1.1/interface.schema.json +1974 -0
- benchweave_sdk/standards/interface/interface-v1.1.1/mcp-tools.json +25151 -0
- benchweave_sdk/standards/interface/interface-v1.1.1/openapi.json +3584 -0
- benchweave_sdk/standards/interface/interface-v1.1.1/operation-catalog.json +1448 -0
- benchweave_sdk/standards/otdp/_GENERATED.txt +24 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/device-profile-catalog.json +4798 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/device-profile-catalog.schema.json +110 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-action-vectors.json +2678 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-daq.json +177 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-dc_psu.json +159 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-dmm.json +144 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-electronic_load.json +159 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-embedded_controller.json +186 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-function_generator.json +155 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-logic_analyser.json +188 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-oscilloscope.json +177 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-smu.json +214 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-spectrum_analyser.json +177 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-switch_matrix.json +154 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-vna.json +177 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/measurement-vectors.json +1089 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-can.json +108 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-capture.json +114 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-controller.json +150 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-psu.json +187 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-vectors.json +259 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/otdp-device-descriptor.schema.json +3003 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/otdp-measurement.schema.json +787 -0
- benchweave_sdk/standards/otdp/otdp-v0.3.0/otdp-runtime.schema.json +1238 -0
- benchweave_sdk/standards/plugin-ui/_GENERATED.txt +5 -0
- benchweave_sdk/standards/plugin-ui/contracts.py +573 -0
- benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/binding-catalogue.schema.json +6 -0
- benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/configuration-preset.schema.json +6 -0
- benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/presentation-envelope.schema.json +6 -0
- benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/ui-manifest.schema.json +69 -0
- benchweave_sdk/standards/plugin-ui-preview/_GENERATED.txt +2 -0
- benchweave_sdk/standards/plugin-ui-preview/plugin-ui-preview-v1/fixture.schema.json +94 -0
- benchweave_sdk/standards/plugin-ui-preview/plugin-ui-preview-v1/preview-document.schema.json +209 -0
- benchweave_sdk/standards/registry/_GENERATED.txt +6 -0
- benchweave_sdk/standards/registry/registry-v1.0.0/examples/package-lock.json +32 -0
- benchweave_sdk/standards/registry/registry-v1.0.0/examples/release-manifest.json +170 -0
- benchweave_sdk/standards/registry/registry-v1.0.0/examples/release-status.json +18 -0
- benchweave_sdk/standards/registry/registry-v1.0.0/package-lock.schema.json +116 -0
- benchweave_sdk/standards/registry/registry-v1.0.0/release-manifest.schema.json +721 -0
- benchweave_sdk/standards/registry/registry-v1.0.0/release-status.schema.json +190 -0
- benchweave_sdk/standards_sync.py +270 -0
- benchweave_sdk/testing.py +106 -0
- benchweave_sdk/validation.py +102 -0
- benchweave_sdk-0.0.1.dist-info/METADATA +192 -0
- benchweave_sdk-0.0.1.dist-info/RECORD +86 -0
- benchweave_sdk-0.0.1.dist-info/WHEEL +4 -0
- benchweave_sdk-0.0.1.dist-info/entry_points.txt +2 -0
- benchweave_sdk-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Public authoring surface for OTDP 0.3.0 / adapter API 1.1."""
|
|
2
|
+
|
|
3
|
+
from .interfaces import Adapter, HostServices, OperationContext
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
OTDP_VERSION = "0.3.0"
|
|
7
|
+
ADAPTER_API_VERSION = "1.1"
|
|
8
|
+
__all__ = ["Adapter", "HostServices", "OperationContext"]
|
benchweave_sdk/cli.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Software-only SDK commands. No registry publication or hardware access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import sys
|
|
7
|
+
import webbrowser
|
|
8
|
+
from collections.abc import Callable, Sequence
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .console import ConsoleOutput
|
|
17
|
+
from .packaging import inventory
|
|
18
|
+
from .scaffold import create_project
|
|
19
|
+
from .validation import validate_descriptor
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _domain_errors[**P, R](function: Callable[P, R]) -> Callable[P, R]:
|
|
23
|
+
@wraps(function)
|
|
24
|
+
def guarded(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
25
|
+
try:
|
|
26
|
+
return function(*args, **kwargs)
|
|
27
|
+
except (ValueError, OSError, RuntimeError) as exc:
|
|
28
|
+
raise click.ClickException(str(exc)) from exc
|
|
29
|
+
|
|
30
|
+
return guarded
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _presentation_options[R](function: Callable[..., R]) -> Callable[..., R]:
|
|
34
|
+
options = (
|
|
35
|
+
click.option("--descriptor", required=True, type=click.Path(path_type=Path)),
|
|
36
|
+
click.option(
|
|
37
|
+
"--resources",
|
|
38
|
+
required=True,
|
|
39
|
+
type=click.Path(path_type=Path),
|
|
40
|
+
help="Package resource root",
|
|
41
|
+
),
|
|
42
|
+
click.option("--catalogue", required=True, type=click.Path(path_type=Path)),
|
|
43
|
+
click.option("--firmware"),
|
|
44
|
+
click.option("--feature", multiple=True),
|
|
45
|
+
click.option("--panel", multiple=True),
|
|
46
|
+
)
|
|
47
|
+
decorated = function
|
|
48
|
+
for option in reversed(options):
|
|
49
|
+
decorated = option(decorated)
|
|
50
|
+
return decorated
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.group()
|
|
54
|
+
@click.version_option(__version__)
|
|
55
|
+
def cli() -> None:
|
|
56
|
+
"""Software-only SDK commands; no publication or hardware access."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cli.command("new")
|
|
60
|
+
@click.argument("directory", type=click.Path(path_type=Path))
|
|
61
|
+
@click.option("--package", "package_name", default="example_plugin", show_default=True)
|
|
62
|
+
@click.option("--with-ui", is_flag=True, help="Add optional read-only UI resources")
|
|
63
|
+
@_domain_errors
|
|
64
|
+
def new_command(directory: Path, package_name: str, with_ui: bool) -> None:
|
|
65
|
+
"""Create a synthetic external plugin project."""
|
|
66
|
+
create_project(directory, package_name)
|
|
67
|
+
if with_ui:
|
|
68
|
+
from .presentation import create_ui_resources
|
|
69
|
+
|
|
70
|
+
create_ui_resources(directory, package_name)
|
|
71
|
+
ConsoleOutput().message(
|
|
72
|
+
f"Created synthetic plugin at {directory}; review before hardware or publication.",
|
|
73
|
+
style="green",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cli.command("check")
|
|
78
|
+
@click.argument("descriptor", type=click.Path(path_type=Path))
|
|
79
|
+
@_domain_errors
|
|
80
|
+
def check_command(descriptor: Path) -> None:
|
|
81
|
+
"""Run offline descriptor schema and basic semantic checks."""
|
|
82
|
+
import json
|
|
83
|
+
|
|
84
|
+
from .presentation import read_file
|
|
85
|
+
|
|
86
|
+
validate_descriptor(json.loads(read_file(descriptor)))
|
|
87
|
+
ConsoleOutput().message(
|
|
88
|
+
"Descriptor schema and basic S01/S02 checks passed; "
|
|
89
|
+
"full conformance and hardware evidence remain separate.",
|
|
90
|
+
style="green",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@cli.command("inventory")
|
|
95
|
+
@click.argument("directory", type=click.Path(path_type=Path))
|
|
96
|
+
@_domain_errors
|
|
97
|
+
def inventory_command(directory: Path) -> None:
|
|
98
|
+
"""Print hashes for a prepared bundle; not a release manifest."""
|
|
99
|
+
ConsoleOutput().document(inventory(directory))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _render_report(report: object) -> None:
|
|
103
|
+
output = ConsoleOutput()
|
|
104
|
+
findings = [(row.code, row.path, row.message) for row in report.findings] # type: ignore[attr-defined]
|
|
105
|
+
findings.extend(("panel_unavailable", str(page), "") for page in report.unavailable_pages) # type: ignore[attr-defined]
|
|
106
|
+
if findings:
|
|
107
|
+
output.findings(findings)
|
|
108
|
+
if not report.valid: # type: ignore[attr-defined]
|
|
109
|
+
raise click.ClickException("Presentation validation failed.")
|
|
110
|
+
output.message(
|
|
111
|
+
"Offline presentation checks passed; not admission or approval to apply settings.",
|
|
112
|
+
style="green",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@cli.command("check-ui")
|
|
117
|
+
@click.argument("envelope", type=click.Path(path_type=Path))
|
|
118
|
+
@_presentation_options
|
|
119
|
+
@_domain_errors
|
|
120
|
+
def check_ui_command(
|
|
121
|
+
envelope: Path,
|
|
122
|
+
descriptor: Path,
|
|
123
|
+
resources: Path,
|
|
124
|
+
catalogue: Path,
|
|
125
|
+
firmware: str | None,
|
|
126
|
+
feature: tuple[str, ...],
|
|
127
|
+
panel: tuple[str, ...],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Validate a presentation candidate offline."""
|
|
130
|
+
from .presentation import check_ui
|
|
131
|
+
|
|
132
|
+
report = check_ui(
|
|
133
|
+
envelope,
|
|
134
|
+
descriptor,
|
|
135
|
+
resources,
|
|
136
|
+
catalogue,
|
|
137
|
+
firmware=firmware,
|
|
138
|
+
features=frozenset(feature),
|
|
139
|
+
panels=frozenset(panel),
|
|
140
|
+
)
|
|
141
|
+
_render_report(report)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@cli.command("check-preset")
|
|
145
|
+
@click.argument("preset", type=click.Path(path_type=Path))
|
|
146
|
+
@click.option("--descriptor", required=True, type=click.Path(path_type=Path))
|
|
147
|
+
@click.option("--settings-schema", required=True, type=click.Path(path_type=Path))
|
|
148
|
+
@click.option("--firmware", required=True)
|
|
149
|
+
@_domain_errors
|
|
150
|
+
def check_preset_command(
|
|
151
|
+
preset: Path, descriptor: Path, settings_schema: Path, firmware: str
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Validate complete settings offline."""
|
|
154
|
+
from .presentation import read_file, validate_preset
|
|
155
|
+
|
|
156
|
+
report = validate_preset(
|
|
157
|
+
read_file(preset),
|
|
158
|
+
descriptor_raw=read_file(descriptor),
|
|
159
|
+
settings_schema_raw=read_file(settings_schema),
|
|
160
|
+
firmware=firmware,
|
|
161
|
+
)
|
|
162
|
+
_render_report(report)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@cli.command("sync-standards")
|
|
166
|
+
@click.argument("bundle", required=False, type=click.Path(path_type=Path))
|
|
167
|
+
@click.option("--check", "check_only", is_flag=True, help="Verify the vendored tree only")
|
|
168
|
+
@_domain_errors
|
|
169
|
+
def sync_standards_command(bundle: Path | None, check_only: bool) -> None:
|
|
170
|
+
"""Import a standards bundle into the SDK's vendored tree and lock.
|
|
171
|
+
|
|
172
|
+
With --check and no bundle, verify the committed lock and vendored tree
|
|
173
|
+
alone; no main-project export is read.
|
|
174
|
+
"""
|
|
175
|
+
from .standards_sync import sync
|
|
176
|
+
|
|
177
|
+
report = sync(
|
|
178
|
+
bundle,
|
|
179
|
+
sdk_root=Path(__file__).resolve().parents[2],
|
|
180
|
+
check_only=check_only,
|
|
181
|
+
)
|
|
182
|
+
summary = ", ".join(
|
|
183
|
+
f"{label}: {len(rows)}"
|
|
184
|
+
for label, rows in (
|
|
185
|
+
("added", report.added),
|
|
186
|
+
("changed", report.changed),
|
|
187
|
+
("deprecated", report.deprecated),
|
|
188
|
+
("removed", report.removed),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
if check_only and any(
|
|
192
|
+
(report.added, report.changed, report.deprecated, report.removed)
|
|
193
|
+
):
|
|
194
|
+
# A non-empty report in check mode is drift awaiting sync, not success.
|
|
195
|
+
raise click.ClickException(
|
|
196
|
+
f"standards drift detected ({summary}); re-run sync-standards to update"
|
|
197
|
+
)
|
|
198
|
+
if bundle is None:
|
|
199
|
+
ConsoleOutput().message(
|
|
200
|
+
"Standards verified (committed lock and vendored tree agree).",
|
|
201
|
+
style="green",
|
|
202
|
+
)
|
|
203
|
+
return
|
|
204
|
+
ConsoleOutput().message(
|
|
205
|
+
f"Standards {'verified' if check_only else 'synced'} ({summary}).",
|
|
206
|
+
style="green",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _renderer_origin(renderer_url: str | None) -> str | None:
|
|
211
|
+
if renderer_url is None:
|
|
212
|
+
return None
|
|
213
|
+
parsed = urlsplit(renderer_url)
|
|
214
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
215
|
+
raise ValueError(f"preview_renderer_url_invalid: {renderer_url}")
|
|
216
|
+
host = parsed.hostname or ""
|
|
217
|
+
try:
|
|
218
|
+
loopback = ipaddress.ip_address(host).is_loopback
|
|
219
|
+
except ValueError:
|
|
220
|
+
loopback = host == "localhost"
|
|
221
|
+
if not loopback:
|
|
222
|
+
# Author-editable plugin docs can suggest command lines; a non-loopback
|
|
223
|
+
# renderer would get CORS-trusted API access without the bundled
|
|
224
|
+
# renderer's simulation labelling. Keep trust on the operator's machine.
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"preview_renderer_origin_not_local: {renderer_url} must name a loopback "
|
|
227
|
+
"host; serve third-party renderers locally"
|
|
228
|
+
)
|
|
229
|
+
return f"{parsed.scheme}://{parsed.netloc}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _renderer_target(renderer_url: str | None, api_base: str) -> str:
|
|
233
|
+
if renderer_url is None:
|
|
234
|
+
return api_base
|
|
235
|
+
parsed = urlsplit(renderer_url)
|
|
236
|
+
query = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
|
237
|
+
query["apiBase"] = api_base
|
|
238
|
+
return urlunsplit(parsed._replace(query=urlencode(query)))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _run_preview(
|
|
242
|
+
*,
|
|
243
|
+
envelope: Path,
|
|
244
|
+
descriptor: Path,
|
|
245
|
+
resources: Path,
|
|
246
|
+
catalogue: Path,
|
|
247
|
+
fixtures: Path | None,
|
|
248
|
+
firmware: str | None,
|
|
249
|
+
feature: tuple[str, ...],
|
|
250
|
+
panel: tuple[str, ...],
|
|
251
|
+
renderer_url: str | None,
|
|
252
|
+
host: str,
|
|
253
|
+
port: int,
|
|
254
|
+
allow_network: bool,
|
|
255
|
+
no_open: bool,
|
|
256
|
+
) -> None:
|
|
257
|
+
from .fixtures import build_preview_model
|
|
258
|
+
from .presentation import load_validated_preview_inputs
|
|
259
|
+
from .preview_server import PreviewServer, bundled_assets, validate_listener
|
|
260
|
+
|
|
261
|
+
validate_listener(host, allow_network)
|
|
262
|
+
renderer_origin = _renderer_origin(renderer_url)
|
|
263
|
+
if fixtures is not None and not fixtures.is_dir():
|
|
264
|
+
raise ValueError(f"preview_fixtures_directory_expected: {fixtures}")
|
|
265
|
+
candidate = load_validated_preview_inputs(
|
|
266
|
+
envelope,
|
|
267
|
+
descriptor,
|
|
268
|
+
resources,
|
|
269
|
+
catalogue,
|
|
270
|
+
firmware=firmware,
|
|
271
|
+
features=frozenset(feature),
|
|
272
|
+
panels=frozenset(panel),
|
|
273
|
+
)
|
|
274
|
+
if fixtures is not None:
|
|
275
|
+
candidate = type(candidate)(
|
|
276
|
+
envelope=candidate.envelope,
|
|
277
|
+
manifest=candidate.manifest,
|
|
278
|
+
binding_catalogue=candidate.binding_catalogue,
|
|
279
|
+
resource_root=fixtures.parent,
|
|
280
|
+
)
|
|
281
|
+
model = build_preview_model(candidate)
|
|
282
|
+
server = PreviewServer(
|
|
283
|
+
model,
|
|
284
|
+
bundled_assets(),
|
|
285
|
+
host=host,
|
|
286
|
+
port=port,
|
|
287
|
+
allow_network=allow_network,
|
|
288
|
+
allowed_origin=renderer_origin,
|
|
289
|
+
)
|
|
290
|
+
try:
|
|
291
|
+
address = server.start()
|
|
292
|
+
target_url = _renderer_target(renderer_url, address.url)
|
|
293
|
+
if renderer_url is not None:
|
|
294
|
+
ConsoleOutput().message(
|
|
295
|
+
"Custom renderer: a developer-supplied page is display, not the bundled "
|
|
296
|
+
"BenchWeave renderer; all data remains simulated.",
|
|
297
|
+
style="yellow",
|
|
298
|
+
)
|
|
299
|
+
if no_open or not sys.stdout.isatty():
|
|
300
|
+
ConsoleOutput().preview_ready(
|
|
301
|
+
target_url,
|
|
302
|
+
scenarios=len(model.scenarios),
|
|
303
|
+
renderer_version=model.renderer_version,
|
|
304
|
+
)
|
|
305
|
+
try:
|
|
306
|
+
opened = False if no_open else webbrowser.open(target_url)
|
|
307
|
+
except webbrowser.Error:
|
|
308
|
+
opened = False
|
|
309
|
+
if not no_open and not opened:
|
|
310
|
+
ConsoleOutput().message(f"Browser did not open; use {target_url}", style="yellow")
|
|
311
|
+
server.wait()
|
|
312
|
+
return
|
|
313
|
+
from .preview_tui import PreviewStatusApp
|
|
314
|
+
|
|
315
|
+
PreviewStatusApp(
|
|
316
|
+
url=target_url,
|
|
317
|
+
renderer_version=model.renderer_version,
|
|
318
|
+
scenarios=len(model.scenarios),
|
|
319
|
+
open_browser=webbrowser.open,
|
|
320
|
+
shutdown=server.shutdown,
|
|
321
|
+
open_on_mount=True,
|
|
322
|
+
).run()
|
|
323
|
+
except KeyboardInterrupt:
|
|
324
|
+
return
|
|
325
|
+
finally:
|
|
326
|
+
server.shutdown()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@cli.command("preview-ui")
|
|
330
|
+
@click.argument("envelope", type=click.Path(path_type=Path))
|
|
331
|
+
@_presentation_options
|
|
332
|
+
@click.option("--fixtures", type=click.Path(path_type=Path))
|
|
333
|
+
@click.option("--renderer-url")
|
|
334
|
+
@click.option("--host", default="127.0.0.1", show_default=True)
|
|
335
|
+
@click.option("--port", default=0, type=click.IntRange(0, 65535), show_default=True)
|
|
336
|
+
@click.option("--allow-network", is_flag=True)
|
|
337
|
+
@click.option("--no-open", is_flag=True)
|
|
338
|
+
@_domain_errors
|
|
339
|
+
def preview_ui_command(**options: object) -> None:
|
|
340
|
+
"""Preview simulated presentation states on a local renderer."""
|
|
341
|
+
_run_preview(**options) # type: ignore[arg-type]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def main(args: Sequence[str] | None = None) -> int:
|
|
345
|
+
"""Run the Click group and return a process-compatible exit code."""
|
|
346
|
+
try:
|
|
347
|
+
cli.main(
|
|
348
|
+
args=list(args) if args is not None else None,
|
|
349
|
+
prog_name="benchweave-sdk",
|
|
350
|
+
standalone_mode=False,
|
|
351
|
+
)
|
|
352
|
+
except click.ClickException as exc:
|
|
353
|
+
exc.show()
|
|
354
|
+
return exc.exit_code
|
|
355
|
+
except click.exceptions.Exit as exc:
|
|
356
|
+
return exc.exit_code
|
|
357
|
+
return 0
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Reusable checks, not full OTDP certification or hardware qualification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .interfaces import Adapter, OperationContext
|
|
11
|
+
from .testing import MockContext, MockHost
|
|
12
|
+
from .validation import validate_descriptor, validate_request, validate_result
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _check_timeout(timeout: float) -> None:
|
|
16
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
17
|
+
raise ValueError("Conformance timeout must be finite and positive")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def check_operation(
|
|
21
|
+
adapter: Adapter,
|
|
22
|
+
request: dict[str, Any],
|
|
23
|
+
context: OperationContext,
|
|
24
|
+
*,
|
|
25
|
+
timeout: float = 1.0,
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
"""Check one response, with a wall-clock limit for cooperative async code."""
|
|
28
|
+
_check_timeout(timeout)
|
|
29
|
+
validate_request(request)
|
|
30
|
+
if request["operation_id"] != context.operation_id:
|
|
31
|
+
raise ValueError("Request/context correlation mismatch")
|
|
32
|
+
async with asyncio.timeout(timeout):
|
|
33
|
+
result = await adapter.execute(request, context)
|
|
34
|
+
validate_result(result, request)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def check_lifecycle(
|
|
39
|
+
factory: Callable[[], Adapter], descriptor: dict[str, Any], *, timeout: float = 1.0
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Check quiet lifecycle and bounded cleanup; import I/O needs separate tests.
|
|
42
|
+
|
|
43
|
+
Each await has a wall-clock timeout. Run untrusted code in an isolated process:
|
|
44
|
+
blocking Python or code swallowing cancellation cannot be stopped by asyncio.
|
|
45
|
+
"""
|
|
46
|
+
_check_timeout(timeout)
|
|
47
|
+
validate_descriptor(descriptor)
|
|
48
|
+
host = MockHost([])
|
|
49
|
+
context = MockContext("lifecycle", deadline_monotonic=timeout)
|
|
50
|
+
adapter = factory()
|
|
51
|
+
try:
|
|
52
|
+
async with asyncio.timeout(timeout):
|
|
53
|
+
await adapter.open(descriptor, host, context)
|
|
54
|
+
assert not host.transfers, "Open performed a device transaction"
|
|
55
|
+
async with asyncio.timeout(timeout):
|
|
56
|
+
assert await adapter.next_event("quiet", context) is None
|
|
57
|
+
finally:
|
|
58
|
+
async with asyncio.timeout(timeout):
|
|
59
|
+
await adapter.close(context)
|
|
60
|
+
async with asyncio.timeout(timeout):
|
|
61
|
+
await adapter.close(context)
|
|
62
|
+
host.assert_complete()
|
|
63
|
+
assert not host.transfers, "Lifecycle performed a device transaction"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Rich-backed SDK output with stable plain text for automation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TextIO
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConsoleOutput:
|
|
15
|
+
"""Render restrained terminal output without changing semantic messages."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *, file: TextIO | None = None, terminal: bool | None = None) -> None:
|
|
18
|
+
target = file or sys.stdout
|
|
19
|
+
self.terminal = target.isatty() if terminal is None else terminal
|
|
20
|
+
self.console = Console(
|
|
21
|
+
file=target,
|
|
22
|
+
force_terminal=self.terminal,
|
|
23
|
+
no_color=not self.terminal,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def message(self, text: str, *, style: str | None = None) -> None:
|
|
27
|
+
self.console.print(text, style=style if self.terminal else None, markup=False)
|
|
28
|
+
|
|
29
|
+
def document(self, value: object) -> None:
|
|
30
|
+
import json
|
|
31
|
+
|
|
32
|
+
self.message(json.dumps(value, indent=2))
|
|
33
|
+
|
|
34
|
+
def findings(self, rows: list[tuple[str, str, str]]) -> None:
|
|
35
|
+
if not self.terminal:
|
|
36
|
+
for code, path, message in rows:
|
|
37
|
+
self.message(f"{code}: {path}: {message}")
|
|
38
|
+
return
|
|
39
|
+
table = Table(title="Presentation findings", box=None)
|
|
40
|
+
table.add_column("Code", style="yellow")
|
|
41
|
+
table.add_column("Path")
|
|
42
|
+
table.add_column("Message")
|
|
43
|
+
for row in rows:
|
|
44
|
+
# Text cells opt out of Rich markup: findings carry author-influenced
|
|
45
|
+
# strings that must never become clickable links or restyled output.
|
|
46
|
+
table.add_row(Text(row[0]), Text(row[1]), Text(row[2]))
|
|
47
|
+
self.console.print(table)
|
|
48
|
+
|
|
49
|
+
def preview_ready(self, url: str, *, scenarios: int, renderer_version: str) -> None:
|
|
50
|
+
text = (
|
|
51
|
+
f"SIMULATED PRESENTATION DATA: {url}\n"
|
|
52
|
+
f"Renderer {renderer_version} · {scenarios} scenarios"
|
|
53
|
+
)
|
|
54
|
+
if self.terminal:
|
|
55
|
+
self.console.print(Panel(text, title="BenchWeave SDK preview", border_style="cyan"))
|
|
56
|
+
else:
|
|
57
|
+
self.message(text)
|