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.
Files changed (86) hide show
  1. benchweave_sdk/__init__.py +8 -0
  2. benchweave_sdk/cli.py +361 -0
  3. benchweave_sdk/conformance.py +63 -0
  4. benchweave_sdk/console.py +57 -0
  5. benchweave_sdk/fixtures.py +289 -0
  6. benchweave_sdk/interfaces.py +51 -0
  7. benchweave_sdk/packaging.py +61 -0
  8. benchweave_sdk/presentation.py +404 -0
  9. benchweave_sdk/preview_assets/inventory.json +21 -0
  10. benchweave_sdk/preview_assets/site/assets/index-BqP_aR5c.css +1 -0
  11. benchweave_sdk/preview_assets/site/assets/index-D0qT06Ho.js +28 -0
  12. benchweave_sdk/preview_assets/site/index.html +14 -0
  13. benchweave_sdk/preview_models.py +103 -0
  14. benchweave_sdk/preview_server.py +305 -0
  15. benchweave_sdk/preview_tui.py +69 -0
  16. benchweave_sdk/py.typed +1 -0
  17. benchweave_sdk/scaffold.py +430 -0
  18. benchweave_sdk/standards/execution/_GENERATED.txt +12 -0
  19. benchweave_sdk/standards/execution/execution-v1.0.0/bench.schema.json +476 -0
  20. benchweave_sdk/standards/execution/execution-v1.0.0/commissioning.schema.json +277 -0
  21. benchweave_sdk/standards/execution/execution-v1.0.0/examples/bench.json +158 -0
  22. benchweave_sdk/standards/execution/execution-v1.0.0/examples/commissioning.json +158 -0
  23. benchweave_sdk/standards/execution/execution-v1.0.0/examples/procedure.json +101 -0
  24. benchweave_sdk/standards/execution/execution-v1.0.0/examples/run-binding.json +38 -0
  25. benchweave_sdk/standards/execution/execution-v1.0.0/examples/run-record.json +25 -0
  26. benchweave_sdk/standards/execution/execution-v1.0.0/examples/safety-policy.json +110 -0
  27. benchweave_sdk/standards/execution/execution-v1.0.0/procedure.schema.json +498 -0
  28. benchweave_sdk/standards/execution/execution-v1.0.0/run-binding.schema.json +176 -0
  29. benchweave_sdk/standards/execution/execution-v1.0.0/run-record.schema.json +321 -0
  30. benchweave_sdk/standards/execution/execution-v1.0.0/safety-policy.schema.json +511 -0
  31. benchweave_sdk/standards/interface/_GENERATED.txt +6 -0
  32. benchweave_sdk/standards/interface/interface-v1.1.1/examples/mcp-start-exchange.json +63 -0
  33. benchweave_sdk/standards/interface/interface-v1.1.1/examples/operation-vectors.json +393 -0
  34. benchweave_sdk/standards/interface/interface-v1.1.1/interface.schema.json +1974 -0
  35. benchweave_sdk/standards/interface/interface-v1.1.1/mcp-tools.json +25151 -0
  36. benchweave_sdk/standards/interface/interface-v1.1.1/openapi.json +3584 -0
  37. benchweave_sdk/standards/interface/interface-v1.1.1/operation-catalog.json +1448 -0
  38. benchweave_sdk/standards/otdp/_GENERATED.txt +24 -0
  39. benchweave_sdk/standards/otdp/otdp-v0.3.0/device-profile-catalog.json +4798 -0
  40. benchweave_sdk/standards/otdp/otdp-v0.3.0/device-profile-catalog.schema.json +110 -0
  41. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-action-vectors.json +2678 -0
  42. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-daq.json +177 -0
  43. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-dc_psu.json +159 -0
  44. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-dmm.json +144 -0
  45. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-electronic_load.json +159 -0
  46. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-embedded_controller.json +186 -0
  47. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-function_generator.json +155 -0
  48. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-logic_analyser.json +188 -0
  49. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-oscilloscope.json +177 -0
  50. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-smu.json +214 -0
  51. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-spectrum_analyser.json +177 -0
  52. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-switch_matrix.json +154 -0
  53. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/class-vna.json +177 -0
  54. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/measurement-vectors.json +1089 -0
  55. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-can.json +108 -0
  56. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-capture.json +114 -0
  57. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-controller.json +150 -0
  58. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-psu.json +187 -0
  59. benchweave_sdk/standards/otdp/otdp-v0.3.0/examples/reference-vectors.json +259 -0
  60. benchweave_sdk/standards/otdp/otdp-v0.3.0/otdp-device-descriptor.schema.json +3003 -0
  61. benchweave_sdk/standards/otdp/otdp-v0.3.0/otdp-measurement.schema.json +787 -0
  62. benchweave_sdk/standards/otdp/otdp-v0.3.0/otdp-runtime.schema.json +1238 -0
  63. benchweave_sdk/standards/plugin-ui/_GENERATED.txt +5 -0
  64. benchweave_sdk/standards/plugin-ui/contracts.py +573 -0
  65. benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/binding-catalogue.schema.json +6 -0
  66. benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/configuration-preset.schema.json +6 -0
  67. benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/presentation-envelope.schema.json +6 -0
  68. benchweave_sdk/standards/plugin-ui/plugin-ui-v0.1.0/ui-manifest.schema.json +69 -0
  69. benchweave_sdk/standards/plugin-ui-preview/_GENERATED.txt +2 -0
  70. benchweave_sdk/standards/plugin-ui-preview/plugin-ui-preview-v1/fixture.schema.json +94 -0
  71. benchweave_sdk/standards/plugin-ui-preview/plugin-ui-preview-v1/preview-document.schema.json +209 -0
  72. benchweave_sdk/standards/registry/_GENERATED.txt +6 -0
  73. benchweave_sdk/standards/registry/registry-v1.0.0/examples/package-lock.json +32 -0
  74. benchweave_sdk/standards/registry/registry-v1.0.0/examples/release-manifest.json +170 -0
  75. benchweave_sdk/standards/registry/registry-v1.0.0/examples/release-status.json +18 -0
  76. benchweave_sdk/standards/registry/registry-v1.0.0/package-lock.schema.json +116 -0
  77. benchweave_sdk/standards/registry/registry-v1.0.0/release-manifest.schema.json +721 -0
  78. benchweave_sdk/standards/registry/registry-v1.0.0/release-status.schema.json +190 -0
  79. benchweave_sdk/standards_sync.py +270 -0
  80. benchweave_sdk/testing.py +106 -0
  81. benchweave_sdk/validation.py +102 -0
  82. benchweave_sdk-0.0.1.dist-info/METADATA +192 -0
  83. benchweave_sdk-0.0.1.dist-info/RECORD +86 -0
  84. benchweave_sdk-0.0.1.dist-info/WHEEL +4 -0
  85. benchweave_sdk-0.0.1.dist-info/entry_points.txt +2 -0
  86. 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)