testerkit 0.4.0__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.
- testerkit/__init__.py +68 -0
- testerkit/_docs/_assets/operator-ui/channels/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/dashboard/runs.png +0 -0
- testerkit/_docs/_assets/operator-ui/dashboard/stations.png +0 -0
- testerkit/_docs/_assets/operator-ui/events/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/events/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/chart.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/explore/plot-controls.png +0 -0
- testerkit/_docs/_assets/operator-ui/files/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/launch/form.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/filters.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/pareto.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/ppk.png +0 -0
- testerkit/_docs/_assets/operator-ui/metrics/yield.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-header.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-measurements.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-overview.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/detail-steps.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/stats.png +0 -0
- testerkit/_docs/_assets/operator-ui/results/table.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/designer.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/docs.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/fixtures.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/instruments.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/parts.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/profiles.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/stations.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/tests.png +0 -0
- testerkit/_docs/_assets/operator-ui/tour/uuts.png +0 -0
- testerkit/_docs/concepts/configuration/capabilities.md +447 -0
- testerkit/_docs/concepts/configuration/fixtures.md +335 -0
- testerkit/_docs/concepts/configuration/index.md +14 -0
- testerkit/_docs/concepts/configuration/parts.md +276 -0
- testerkit/_docs/concepts/configuration/stations.md +214 -0
- testerkit/_docs/concepts/data/data-stores.md +199 -0
- testerkit/_docs/concepts/data/event-log.md +185 -0
- testerkit/_docs/concepts/data/event-sourcing.md +76 -0
- testerkit/_docs/concepts/data/flight-streaming.md +58 -0
- testerkit/_docs/concepts/data/index.md +17 -0
- testerkit/_docs/concepts/data/sessions.md +78 -0
- testerkit/_docs/concepts/data/three-verbs.md +260 -0
- testerkit/_docs/concepts/execution/index.md +13 -0
- testerkit/_docs/concepts/execution/outcomes.md +199 -0
- testerkit/_docs/concepts/execution/step-hierarchy.md +170 -0
- testerkit/_docs/concepts/execution/step-manifest.md +115 -0
- testerkit/_docs/concepts/index.md +40 -0
- testerkit/_docs/concepts/overview/ai-integration.md +57 -0
- testerkit/_docs/concepts/overview/architecture.md +357 -0
- testerkit/_docs/concepts/overview/index.md +14 -0
- testerkit/_docs/concepts/overview/platform-vs-framework.md +147 -0
- testerkit/_docs/concepts/overview/pytest.md +53 -0
- testerkit/_docs/concepts/overview/tiers.md +46 -0
- testerkit/_docs/how-to/catalog/datasheet-to-test.md +169 -0
- testerkit/_docs/how-to/catalog/index.md +11 -0
- testerkit/_docs/how-to/configuration/configuring-stations.md +364 -0
- testerkit/_docs/how-to/configuration/custom-drivers.md +484 -0
- testerkit/_docs/how-to/configuration/index.md +13 -0
- testerkit/_docs/how-to/configuration/mock-mode.md +342 -0
- testerkit/_docs/how-to/data/benchmarking.md +120 -0
- testerkit/_docs/how-to/data/capture-an-artifact.md +84 -0
- testerkit/_docs/how-to/data/capture-waveform.md +110 -0
- testerkit/_docs/how-to/data/choosing-a-channel-verb.md +108 -0
- testerkit/_docs/how-to/data/compare-runs.md +120 -0
- testerkit/_docs/how-to/data/export-results.md +104 -0
- testerkit/_docs/how-to/data/find-flaky-tests.md +108 -0
- testerkit/_docs/how-to/data/grafana-dashboards.md +148 -0
- testerkit/_docs/how-to/data/index.md +23 -0
- testerkit/_docs/how-to/data/mcp-debug-failures.md +125 -0
- testerkit/_docs/how-to/data/mcp-query-runs.md +120 -0
- testerkit/_docs/how-to/data/querying-channels.md +117 -0
- testerkit/_docs/how-to/data/querying-events.md +85 -0
- testerkit/_docs/how-to/data/stream-live-channel.md +134 -0
- testerkit/_docs/how-to/execution/custom-operator-ui.md +136 -0
- testerkit/_docs/how-to/execution/index.md +21 -0
- testerkit/_docs/how-to/execution/limits.md +193 -0
- testerkit/_docs/how-to/execution/managing-sessions.md +100 -0
- testerkit/_docs/how-to/execution/multi-uut-testing.md +211 -0
- testerkit/_docs/how-to/execution/operator-prompts.md +151 -0
- testerkit/_docs/how-to/execution/profiles.md +304 -0
- testerkit/_docs/how-to/execution/spec-driven-testing.md +169 -0
- testerkit/_docs/how-to/execution/test-context.md +212 -0
- testerkit/_docs/how-to/execution/traceability.md +252 -0
- testerkit/_docs/how-to/execution/vector-expansion.md +394 -0
- testerkit/_docs/how-to/execution/writing-tests.md +363 -0
- testerkit/_docs/how-to/index.md +59 -0
- testerkit/_docs/how-to/overview/index.md +11 -0
- testerkit/_docs/how-to/overview/mcp-integration.md +168 -0
- testerkit/_docs/how-to/overview/operator-ui-tour.md +234 -0
- testerkit/_docs/integration/data/grafana.md +162 -0
- testerkit/_docs/integration/data/index.md +15 -0
- testerkit/_docs/integration/data/lakehouse-import.md +226 -0
- testerkit/_docs/integration/data/logging.md +131 -0
- testerkit/_docs/integration/data/results-api.md +51 -0
- testerkit/_docs/integration/index.md +20 -0
- testerkit/_docs/integration/runtime/harness.md +281 -0
- testerkit/_docs/integration/runtime/index.md +13 -0
- testerkit/_docs/integration/runtime/instruments.md +284 -0
- testerkit/_docs/integration/runtime/pytest-existing.md +315 -0
- testerkit/_docs/reference/catalog/cookbook.md +389 -0
- testerkit/_docs/reference/catalog/index.md +11 -0
- testerkit/_docs/reference/catalog/schema.md +375 -0
- testerkit/_docs/reference/cli.md +660 -0
- testerkit/_docs/reference/configuration.md +406 -0
- testerkit/_docs/reference/data/channels-schema.md +145 -0
- testerkit/_docs/reference/data/event-types.md +554 -0
- testerkit/_docs/reference/data/events-schema.md +120 -0
- testerkit/_docs/reference/data/files-schema.md +141 -0
- testerkit/_docs/reference/data/index.md +18 -0
- testerkit/_docs/reference/data/models.md +2229 -0
- testerkit/_docs/reference/data/outputs.md +82 -0
- testerkit/_docs/reference/data/parquet-schema.md +529 -0
- testerkit/_docs/reference/data/performance-limits.md +224 -0
- testerkit/_docs/reference/data/query-api.md +247 -0
- testerkit/_docs/reference/index.md +69 -0
- testerkit/_docs/reference/operator-ui/channels/detail.md +82 -0
- testerkit/_docs/reference/operator-ui/channels/list.md +70 -0
- testerkit/_docs/reference/operator-ui/dashboard.md +87 -0
- testerkit/_docs/reference/operator-ui/designer.md +167 -0
- testerkit/_docs/reference/operator-ui/events.md +89 -0
- testerkit/_docs/reference/operator-ui/files.md +74 -0
- testerkit/_docs/reference/operator-ui/fixtures.md +100 -0
- testerkit/_docs/reference/operator-ui/instruments.md +126 -0
- testerkit/_docs/reference/operator-ui/launch.md +79 -0
- testerkit/_docs/reference/operator-ui/live.md +102 -0
- testerkit/_docs/reference/operator-ui/measurements.md +159 -0
- testerkit/_docs/reference/operator-ui/metrics.md +217 -0
- testerkit/_docs/reference/operator-ui/parts.md +98 -0
- testerkit/_docs/reference/operator-ui/profiles.md +103 -0
- testerkit/_docs/reference/operator-ui/results/detail.md +214 -0
- testerkit/_docs/reference/operator-ui/results/list.md +106 -0
- testerkit/_docs/reference/operator-ui/stations.md +98 -0
- testerkit/_docs/reference/operator-ui/tests.md +173 -0
- testerkit/_docs/reference/operator-ui/uuts.md +67 -0
- testerkit/_docs/reference/overview/index.md +11 -0
- testerkit/_docs/reference/overview/pytest-native.md +142 -0
- testerkit/_docs/reference/overview/skills.md +177 -0
- testerkit/_docs/reference/pytest/fixtures.md +342 -0
- testerkit/_docs/reference/pytest/index.md +12 -0
- testerkit/_docs/reference/pytest/markers.md +257 -0
- testerkit/_docs/reference/runtime/api.md +246 -0
- testerkit/_docs/reference/runtime/client.md +280 -0
- testerkit/_docs/reference/runtime/connect.md +140 -0
- testerkit/_docs/reference/runtime/index.md +13 -0
- testerkit/_docs/tutorial/01-first-test.md +113 -0
- testerkit/_docs/tutorial/02-mock-instruments.md +88 -0
- testerkit/_docs/tutorial/03-fixtures.md +169 -0
- testerkit/_docs/tutorial/04-limits.md +139 -0
- testerkit/_docs/tutorial/05-configuration.md +194 -0
- testerkit/_docs/tutorial/06-specifications.md +273 -0
- testerkit/_docs/tutorial/07-real-instruments.md +245 -0
- testerkit/_docs/tutorial/08-capabilities.md +223 -0
- testerkit/_docs/tutorial/09-production.md +374 -0
- testerkit/_docs/tutorial/10-live-monitoring.md +156 -0
- testerkit/_docs/tutorial/11-waveforms-and-evidence.md +169 -0
- testerkit/_docs/tutorial/12-continuous-monitoring.md +145 -0
- testerkit/_docs/tutorial/13-parallel-testing.md +233 -0
- testerkit/_docs/tutorial/index.md +61 -0
- testerkit/_docs/tutorial/quickstart.md +282 -0
- testerkit/analysis/__init__.py +9 -0
- testerkit/analysis/_common.py +24 -0
- testerkit/analysis/measurement_facets.py +524 -0
- testerkit/analysis/measurements_query.py +1458 -0
- testerkit/analysis/metrics.py +389 -0
- testerkit/analysis/runs_query.py +434 -0
- testerkit/analysis/steps_query.py +413 -0
- testerkit/api/__init__.py +6 -0
- testerkit/api/_mime.py +71 -0
- testerkit/api/app.py +1171 -0
- testerkit/api/dialogs/__init__.py +36 -0
- testerkit/api/dialogs/manager.py +617 -0
- testerkit/api/dialogs/models.py +79 -0
- testerkit/api/models.py +80 -0
- testerkit/api/responses.py +205 -0
- testerkit/api/runner.py +222 -0
- testerkit/api/schemas.py +311 -0
- testerkit/benchmark/__init__.py +34 -0
- testerkit/benchmark/concurrency.py +248 -0
- testerkit/benchmark/core.py +232 -0
- testerkit/benchmark/runner.py +628 -0
- testerkit/benchmark/scenario.py +313 -0
- testerkit/benchmark/system.py +191 -0
- testerkit/benchmark/workloads.py +507 -0
- testerkit/catalog/__init__.py +6 -0
- testerkit/catalog/generic/generic_dmm.yaml +61 -0
- testerkit/catalog/generic/generic_eload.yaml +31 -0
- testerkit/catalog/generic/generic_fgen.yaml +56 -0
- testerkit/catalog/generic/generic_oscilloscope.yaml +45 -0
- testerkit/catalog/generic/generic_psu.yaml +77 -0
- testerkit/catalog/generic/generic_smu.yaml +73 -0
- testerkit/channels.py +538 -0
- testerkit/cli/__init__.py +32 -0
- testerkit/cli/_common.py +113 -0
- testerkit/cli/_time.py +179 -0
- testerkit/cli/benchmark_cmd.py +56 -0
- testerkit/cli/catalog_cmd.py +39 -0
- testerkit/cli/daemon.py +157 -0
- testerkit/cli/data_cmd.py +1040 -0
- testerkit/cli/discover_cmd.py +94 -0
- testerkit/cli/docs_cmd.py +100 -0
- testerkit/cli/instrument.py +172 -0
- testerkit/cli/mcp_cmd.py +37 -0
- testerkit/cli/metrics.py +366 -0
- testerkit/cli/project.py +312 -0
- testerkit/cli/root.py +14 -0
- testerkit/cli/runs.py +412 -0
- testerkit/cli/schema_cmd.py +87 -0
- testerkit/cli/serve_cmd.py +50 -0
- testerkit/cli/setup_cmd.py +604 -0
- testerkit/cli/station.py +294 -0
- testerkit/cli/validate.py +112 -0
- testerkit/client.py +463 -0
- testerkit/connect.py +635 -0
- testerkit/data/__init__.py +8 -0
- testerkit/data/_accumulator_pool.py +455 -0
- testerkit/data/_atomic.py +59 -0
- testerkit/data/_collection_indices.py +67 -0
- testerkit/data/_daemon_lifecycle.py +535 -0
- testerkit/data/_duckdb_daemon.py +695 -0
- testerkit/data/_duckdb_flight_server.py +813 -0
- testerkit/data/_event_filters.py +15 -0
- testerkit/data/_event_reader.py +101 -0
- testerkit/data/_flight_errors.py +199 -0
- testerkit/data/_flight_query.py +202 -0
- testerkit/data/_flight_retry.py +148 -0
- testerkit/data/_flight_subscribe.py +67 -0
- testerkit/data/_index_epoch.py +342 -0
- testerkit/data/_ipc_writer.py +185 -0
- testerkit/data/_json_safe.py +63 -0
- testerkit/data/_process.py +18 -0
- testerkit/data/_push_relay.py +124 -0
- testerkit/data/_runs_duckdb_daemon.py +2704 -0
- testerkit/data/_session_reaper.py +110 -0
- testerkit/data/_sql_helpers.py +30 -0
- testerkit/data/_store.py +24 -0
- testerkit/data/backends/__init__.py +7 -0
- testerkit/data/backends/_event_accumulator.py +957 -0
- testerkit/data/backends/_row_helpers.py +1172 -0
- testerkit/data/backends/parquet.py +1143 -0
- testerkit/data/backends/protocol.py +35 -0
- testerkit/data/channels/__init__.py +8 -0
- testerkit/data/channels/_flight_daemon.py +75 -0
- testerkit/data/channels/client.py +242 -0
- testerkit/data/channels/flight_manager.py +88 -0
- testerkit/data/channels/index.py +664 -0
- testerkit/data/channels/models.py +418 -0
- testerkit/data/channels/server.py +162 -0
- testerkit/data/channels/store.py +1481 -0
- testerkit/data/data_dir.py +61 -0
- testerkit/data/duckdb_manager.py +79 -0
- testerkit/data/event_log.py +384 -0
- testerkit/data/event_store.py +768 -0
- testerkit/data/events.py +1025 -0
- testerkit/data/exporters/__init__.py +6 -0
- testerkit/data/exporters/_helpers.py +40 -0
- testerkit/data/exporters/csv_exporter.py +168 -0
- testerkit/data/exporters/hdf5.py +234 -0
- testerkit/data/exporters/json_exporter.py +229 -0
- testerkit/data/exporters/mdf4.py +201 -0
- testerkit/data/exporters/stdf.py +255 -0
- testerkit/data/exporters/tdms.py +292 -0
- testerkit/data/files/__init__.py +71 -0
- testerkit/data/files/_backend.py +207 -0
- testerkit/data/files/_catalog_daemon.py +84 -0
- testerkit/data/files/catalog.py +218 -0
- testerkit/data/files/catalog_manager.py +325 -0
- testerkit/data/files/models.py +60 -0
- testerkit/data/files/serializers.py +414 -0
- testerkit/data/files/store.py +463 -0
- testerkit/data/files/streaming.py +783 -0
- testerkit/data/models.py +604 -0
- testerkit/data/ref.py +152 -0
- testerkit/data/retention.py +297 -0
- testerkit/data/run_store.py +367 -0
- testerkit/data/runs_duckdb_manager.py +103 -0
- testerkit/data/schema_dispatch.py +191 -0
- testerkit/data/schema_migrate.py +76 -0
- testerkit/data/schema_versions.py +85 -0
- testerkit/data/schemas.py +288 -0
- testerkit/data/subscribers/__init__.py +48 -0
- testerkit/data/subscribers/_base.py +20 -0
- testerkit/data/subscribers/_output_file.py +25 -0
- testerkit/data/subscribers/replay.py +55 -0
- testerkit/environment.py +84 -0
- testerkit/execution/__init__.py +11 -0
- testerkit/execution/_git.py +190 -0
- testerkit/execution/_state.py +740 -0
- testerkit/execution/accessors.py +54 -0
- testerkit/execution/audit.py +51 -0
- testerkit/execution/cascade.py +85 -0
- testerkit/execution/connections.py +491 -0
- testerkit/execution/harness.py +1931 -0
- testerkit/execution/instrument_events.py +42 -0
- testerkit/execution/limits.py +154 -0
- testerkit/execution/metadata.py +102 -0
- testerkit/execution/mocks.py +63 -0
- testerkit/execution/profiles.py +616 -0
- testerkit/execution/run_scope.py +1331 -0
- testerkit/execution/session_scope.py +196 -0
- testerkit/execution/sidecar.py +280 -0
- testerkit/execution/site_runner.py +705 -0
- testerkit/execution/sites.py +183 -0
- testerkit/execution/sync.py +294 -0
- testerkit/execution/uut_provider.py +231 -0
- testerkit/execution/vectors.py +205 -0
- testerkit/execution/verify.py +396 -0
- testerkit/expand.py +75 -0
- testerkit/files.py +235 -0
- testerkit/fixtures/__init__.py +6 -0
- testerkit/fixtures/manager.py +357 -0
- testerkit/grafana/__init__.py +1 -0
- testerkit/grafana/bootstrap.py +61 -0
- testerkit/grafana/cli.py +314 -0
- testerkit/grafana/dashboards/asset_utilization.json +92 -0
- testerkit/grafana/dashboards/channel_explorer.json +77 -0
- testerkit/grafana/dashboards/event_log.json +92 -0
- testerkit/grafana/dashboards/failure_pareto.json +104 -0
- testerkit/grafana/dashboards/measurement_distribution.json +97 -0
- testerkit/grafana/dashboards/measurement_trend.json +161 -0
- testerkit/grafana/dashboards/station_comparison.json +95 -0
- testerkit/grafana/dashboards/test_duration.json +104 -0
- testerkit/grafana/dashboards/unit_traceability.json +73 -0
- testerkit/grafana/dashboards/yield_overview.json +189 -0
- testerkit/grafana/provisioning/dashboards.yaml.j2 +12 -0
- testerkit/grafana/provisioning/datasources.yaml.j2 +17 -0
- testerkit/grafana/server.py +282 -0
- testerkit/init.py +724 -0
- testerkit/instruments/__init__.py +10 -0
- testerkit/instruments/base.py +82 -0
- testerkit/instruments/discovery/__init__.py +62 -0
- testerkit/instruments/discovery/_base.py +174 -0
- testerkit/instruments/discovery/lxi.py +152 -0
- testerkit/instruments/discovery/ni.py +95 -0
- testerkit/instruments/discovery/serial.py +73 -0
- testerkit/instruments/discovery/visa.py +98 -0
- testerkit/instruments/lifecycle.py +213 -0
- testerkit/instruments/loader.py +79 -0
- testerkit/instruments/locks.py +178 -0
- testerkit/instruments/mocks.py +230 -0
- testerkit/instruments/observer.py +270 -0
- testerkit/instruments/observers/__init__.py +36 -0
- testerkit/instruments/observers/_base.py +68 -0
- testerkit/instruments/observers/daqmx.py +70 -0
- testerkit/instruments/observers/descriptor.py +69 -0
- testerkit/instruments/observers/generic.py +92 -0
- testerkit/instruments/observers/lantz.py +31 -0
- testerkit/instruments/observers/modbus.py +96 -0
- testerkit/instruments/observers/motion.py +68 -0
- testerkit/instruments/observers/ni_modular.py +73 -0
- testerkit/instruments/observers/ophyd.py +80 -0
- testerkit/instruments/observers/pymeasure.py +120 -0
- testerkit/instruments/observers/qcodes.py +66 -0
- testerkit/instruments/observers/scpi.py +88 -0
- testerkit/instruments/observers/tektronix.py +57 -0
- testerkit/instruments/observers/visa.py +103 -0
- testerkit/instruments/pool.py +459 -0
- testerkit/instruments/proxy.py +59 -0
- testerkit/instruments/route_manager.py +329 -0
- testerkit/instruments/routed_proxy.py +88 -0
- testerkit/instruments/server.py +508 -0
- testerkit/instruments/switch.py +43 -0
- testerkit/instruments/visa.py +294 -0
- testerkit/matching/__init__.py +6 -0
- testerkit/matching/service.py +969 -0
- testerkit/mcp/__init__.py +6 -0
- testerkit/mcp/server.py +681 -0
- testerkit/mcp/tools.py +1868 -0
- testerkit/models/__init__.py +18 -0
- testerkit/models/capability.py +598 -0
- testerkit/models/catalog.py +76 -0
- testerkit/models/data_options.py +153 -0
- testerkit/models/enums.py +334 -0
- testerkit/models/instrument.py +152 -0
- testerkit/models/instrument_asset.py +25 -0
- testerkit/models/part.py +271 -0
- testerkit/models/part_manifest.py +96 -0
- testerkit/models/project.py +114 -0
- testerkit/models/station.py +140 -0
- testerkit/models/test_config.py +748 -0
- testerkit/ontology/__init__.py +39 -0
- testerkit/ontology/schema.py +188 -0
- testerkit/ontology/testerkit.yaml +1420 -0
- testerkit/parts/__init__.py +8 -0
- testerkit/parts/context.py +181 -0
- testerkit/parts/folder.py +251 -0
- testerkit/parts/loader.py +27 -0
- testerkit/prompts/__init__.py +17 -0
- testerkit/prompts/core.py +133 -0
- testerkit/pytest_plugin/__init__.py +1287 -0
- testerkit/pytest_plugin/autouse.py +369 -0
- testerkit/pytest_plugin/helpers.py +354 -0
- testerkit/pytest_plugin/hooks.py +1982 -0
- testerkit/pytest_plugin/markers.py +203 -0
- testerkit/pytest_plugin/retry.py +37 -0
- testerkit/pytest_plugin/sweeps.py +117 -0
- testerkit/queries.py +78 -0
- testerkit/reports/__init__.py +6 -0
- testerkit/reports/core.py +492 -0
- testerkit/reports/datasheet.py +831 -0
- testerkit/reports/templates/datasheet.html +383 -0
- testerkit/reports/templates/default.html +153 -0
- testerkit/sbom.py +142 -0
- testerkit/schema_export.py +101 -0
- testerkit/signals.py +63 -0
- testerkit/skills/README.md +58 -0
- testerkit/skills/templates/project-instructions.md +125 -0
- testerkit/skills/testerkit-analysis/SKILL.md +87 -0
- testerkit/skills/testerkit-capture/SKILL.md +114 -0
- testerkit/skills/testerkit-data/SKILL.md +138 -0
- testerkit/skills/testerkit-datasheets/SKILL.md +100 -0
- testerkit/skills/testerkit-datasheets/agents/scaffold-writer.md +115 -0
- testerkit/skills/testerkit-datasheets/agents/section-extractor.md +98 -0
- testerkit/skills/testerkit-datasheets/agents/section-reviewer.md +153 -0
- testerkit/skills/testerkit-datasheets/agents/section-splitter.md +89 -0
- testerkit/skills/testerkit-datasheets/agents/section-writer.md +226 -0
- testerkit/skills/testerkit-datasheets/references/catalog-pipeline.md +271 -0
- testerkit/skills/testerkit-datasheets/references/process-queue.md +42 -0
- testerkit/skills/testerkit-datasheets/references/scaffold.md +156 -0
- testerkit/skills/testerkit-datasheets/references/test-pipeline.md +312 -0
- testerkit/skills/testerkit-debug/SKILL.md +127 -0
- testerkit/skills/testerkit-interactive/SKILL.md +188 -0
- testerkit/skills/testerkit-interactive/references/live-ui-patterns.md +102 -0
- testerkit/skills/testerkit-mocks/SKILL.md +128 -0
- testerkit/skills/testerkit-parts/SKILL.md +126 -0
- testerkit/skills/testerkit-profiles/SKILL.md +139 -0
- testerkit/skills/testerkit-sites/SKILL.md +140 -0
- testerkit/skills/testerkit-stations/SKILL.md +146 -0
- testerkit/skills/testerkit-tests/SKILL.md +136 -0
- testerkit/store.py +1639 -0
- testerkit/ui/__init__.py +71 -0
- testerkit/ui/_asgi.py +162 -0
- testerkit/ui/app.py +32 -0
- testerkit/ui/components/__init__.py +13 -0
- testerkit/ui/components/artifact_viewer.py +250 -0
- testerkit/ui/components/channel_values.py +159 -0
- testerkit/ui/components/event_timeline.py +222 -0
- testerkit/ui/components/execution_gantt.py +210 -0
- testerkit/ui/components/file_streams.py +147 -0
- testerkit/ui/components/instrument_activity.py +146 -0
- testerkit/ui/components/session_table.py +118 -0
- testerkit/ui/pages/__init__.py +25 -0
- testerkit/ui/pages/channels/__init__.py +4 -0
- testerkit/ui/pages/channels/detail.py +932 -0
- testerkit/ui/pages/channels/list.py +515 -0
- testerkit/ui/pages/dashboard.py +236 -0
- testerkit/ui/pages/designer/__init__.py +5 -0
- testerkit/ui/pages/designer/graph.py +635 -0
- testerkit/ui/pages/designer/matching.py +663 -0
- testerkit/ui/pages/designer/page.py +593 -0
- testerkit/ui/pages/designer/properties.py +591 -0
- testerkit/ui/pages/designer/state.py +468 -0
- testerkit/ui/pages/docs/__init__.py +6 -0
- testerkit/ui/pages/docs/index.py +95 -0
- testerkit/ui/pages/docs/page.py +698 -0
- testerkit/ui/pages/events/__init__.py +3 -0
- testerkit/ui/pages/events/list.py +263 -0
- testerkit/ui/pages/explore.py +1108 -0
- testerkit/ui/pages/files/__init__.py +4 -0
- testerkit/ui/pages/files/detail.py +426 -0
- testerkit/ui/pages/files/list.py +447 -0
- testerkit/ui/pages/fixtures/__init__.py +8 -0
- testerkit/ui/pages/fixtures/detail.py +280 -0
- testerkit/ui/pages/fixtures/edit.py +370 -0
- testerkit/ui/pages/fixtures/list.py +180 -0
- testerkit/ui/pages/fixtures/new.py +195 -0
- testerkit/ui/pages/instruments/__init__.py +4 -0
- testerkit/ui/pages/instruments/detail.py +320 -0
- testerkit/ui/pages/instruments/edit.py +482 -0
- testerkit/ui/pages/instruments/list.py +249 -0
- testerkit/ui/pages/instruments/new.py +200 -0
- testerkit/ui/pages/launch.py +261 -0
- testerkit/ui/pages/live.py +124 -0
- testerkit/ui/pages/metrics_page.py +1229 -0
- testerkit/ui/pages/parts/__init__.py +11 -0
- testerkit/ui/pages/parts/detail.py +202 -0
- testerkit/ui/pages/parts/edit.py +310 -0
- testerkit/ui/pages/parts/list.py +160 -0
- testerkit/ui/pages/parts/new.py +170 -0
- testerkit/ui/pages/parts/requirements.py +153 -0
- testerkit/ui/pages/parts/stations.py +199 -0
- testerkit/ui/pages/profiles/__init__.py +4 -0
- testerkit/ui/pages/profiles/detail.py +113 -0
- testerkit/ui/pages/profiles/list.py +86 -0
- testerkit/ui/pages/results/__init__.py +4 -0
- testerkit/ui/pages/results/detail.py +631 -0
- testerkit/ui/pages/results/list.py +230 -0
- testerkit/ui/pages/stations/__init__.py +4 -0
- testerkit/ui/pages/stations/detail.py +256 -0
- testerkit/ui/pages/stations/edit.py +273 -0
- testerkit/ui/pages/stations/list.py +180 -0
- testerkit/ui/pages/stations/new.py +311 -0
- testerkit/ui/pages/tests/__init__.py +4 -0
- testerkit/ui/pages/tests/detail.py +152 -0
- testerkit/ui/pages/tests/list.py +194 -0
- testerkit/ui/pages/uuts/__init__.py +4 -0
- testerkit/ui/pages/uuts/list.py +97 -0
- testerkit/ui/shared/__init__.py +8 -0
- testerkit/ui/shared/components.py +1826 -0
- testerkit/ui/shared/dialogs.py +105 -0
- testerkit/ui/shared/event_binding.py +194 -0
- testerkit/ui/shared/layout.py +314 -0
- testerkit/ui/shared/services.py +1719 -0
- testerkit/ui/shared/timestamps.py +21 -0
- testerkit/ui/static/branding/favicon-16.png +0 -0
- testerkit/ui/static/branding/favicon-32.png +0 -0
- testerkit/ui/static/branding/favicon-48.png +0 -0
- testerkit/ui/static/branding/testerkit-icon-256.png +0 -0
- testerkit/ui/static/branding/testerkit-mark.svg +1 -0
- testerkit/ui/static/branding/testerkit-wordmark.svg +1 -0
- testerkit/ui/static/global.css +465 -0
- testerkit/utils/__init__.py +17 -0
- testerkit/utils/enum_meta.py +800 -0
- testerkit/utils/paths.py +75 -0
- testerkit/utils/ranges.py +257 -0
- testerkit/validation.py +82 -0
- testerkit/verbs.py +142 -0
- testerkit-0.4.0.dist-info/METADATA +250 -0
- testerkit-0.4.0.dist-info/RECORD +521 -0
- testerkit-0.4.0.dist-info/WHEEL +4 -0
- testerkit-0.4.0.dist-info/entry_points.txt +5 -0
- testerkit-0.4.0.dist-info/licenses/LICENSE +191 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Opt-in **migrate** sink — rewrite an owned durable file forward to the current
|
|
2
|
+
schema, in place, via the same adapter the reader uses.
|
|
3
|
+
|
|
4
|
+
The re-index sink (the whitelist-dispatch reader at each store's ingest boundary)
|
|
5
|
+
is the *mandatory floor*: it projects any readable version forward at query time
|
|
6
|
+
and never touches the file. Migrate is the *optional* housekeeping layer on top —
|
|
7
|
+
"migration is re-index that also persists the adapter output" (§4 of
|
|
8
|
+
``schema-versioning-migration.md``). It exists so an operator can retire old-major
|
|
9
|
+
files (and their adapters) once they're rewritten, not because reading requires
|
|
10
|
+
it. Never a substitute for read-time projection: files in the wild are unreachable.
|
|
11
|
+
|
|
12
|
+
Both content kinds the adapters span are covered here:
|
|
13
|
+
- ``migrate_parquet_file`` — Arrow/parquet (runs). Events/channels are the same
|
|
14
|
+
Arrow pattern over IPC; wired when a real adapter first needs them (YAGNI).
|
|
15
|
+
- ``migrate_sidecar_file`` — the FileStore JSON sidecar (Pydantic model).
|
|
16
|
+
|
|
17
|
+
Each is idempotent (a current-version file is a no-op) and raises
|
|
18
|
+
:class:`~testerkit.data.schema_dispatch.SchemaVersionRefused` for a file the current
|
|
19
|
+
build cannot read (absent/pre-1.0 or unknown) — the caller decides whether to
|
|
20
|
+
skip or surface it, exactly as the re-index sink does.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import pyarrow.parquet as pq
|
|
29
|
+
|
|
30
|
+
from testerkit.data._atomic import atomic_write_table, atomic_write_text
|
|
31
|
+
from testerkit.data.files.models import FileArtifactMetadata
|
|
32
|
+
from testerkit.data.schema_dispatch import dispatch, stamp_from_arrow_metadata
|
|
33
|
+
from testerkit.data.schema_versions import CURRENT_SCHEMA_VERSION, SchemaStore
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def migrate_parquet_file(store: SchemaStore, path: Path | str) -> bool:
|
|
37
|
+
"""Rewrite a parquet artifact forward to the current schema, atomically.
|
|
38
|
+
|
|
39
|
+
Reads the footer stamp, applies the ``source -> current`` adapter, re-stamps
|
|
40
|
+
the result to the current version, and atomically replaces the file. Returns
|
|
41
|
+
``True`` if it migrated, ``False`` if already current (no-op). Raises
|
|
42
|
+
:class:`SchemaVersionRefused` for an unreadable version.
|
|
43
|
+
"""
|
|
44
|
+
path = Path(path)
|
|
45
|
+
parquet_file = pq.ParquetFile(str(path))
|
|
46
|
+
stamp = stamp_from_arrow_metadata(parquet_file.schema_arrow.metadata)
|
|
47
|
+
current = CURRENT_SCHEMA_VERSION[store]
|
|
48
|
+
if stamp == current:
|
|
49
|
+
return False
|
|
50
|
+
adapter = dispatch(store, stamp)
|
|
51
|
+
migrated = adapter(parquet_file.read()).replace_schema_metadata(
|
|
52
|
+
{b"schema_version": current.encode()}
|
|
53
|
+
)
|
|
54
|
+
atomic_write_table(migrated, path)
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def migrate_sidecar_file(path: Path | str) -> bool:
|
|
59
|
+
"""Rewrite a FileStore ``.meta.json`` sidecar forward to the current schema.
|
|
60
|
+
|
|
61
|
+
Reads the raw JSON (so an absent stamp is detectable), applies the
|
|
62
|
+
``source -> current`` adapter to the ``FileArtifactMetadata`` model, re-stamps
|
|
63
|
+
to the current version, and atomically replaces the sidecar. The blob is never
|
|
64
|
+
touched (it is opaque user payload). Returns ``True`` if migrated, ``False`` if
|
|
65
|
+
already current. Raises :class:`SchemaVersionRefused` for an unreadable version.
|
|
66
|
+
"""
|
|
67
|
+
path = Path(path)
|
|
68
|
+
raw = json.loads(path.read_text())
|
|
69
|
+
current = CURRENT_SCHEMA_VERSION[SchemaStore.FILES]
|
|
70
|
+
if raw.get("schema_version") == current:
|
|
71
|
+
return False
|
|
72
|
+
adapter = dispatch(SchemaStore.FILES, raw.get("schema_version"))
|
|
73
|
+
migrated = adapter(FileArtifactMetadata.model_validate(raw))
|
|
74
|
+
migrated = migrated.model_copy(update={"schema_version": current})
|
|
75
|
+
atomic_write_text(migrated.model_dump_json(), path)
|
|
76
|
+
return True
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Central registry of durable-artifact schema versions.
|
|
2
|
+
|
|
3
|
+
The single home for *which durable stores exist and which schema versions
|
|
4
|
+
each can read*. Every durable artifact a TesterKit daemon writes carries a
|
|
5
|
+
schema-version stamp in its file metadata; the stamp is the **migration
|
|
6
|
+
key**. At each store's ``durable-artifact -> rows`` read boundary a
|
|
7
|
+
whitelist-dispatch reader matches the stamp against
|
|
8
|
+
:data:`KNOWN_SCHEMA_VERSIONS` and picks the adapter that projects that
|
|
9
|
+
version forward to the current shape. See
|
|
10
|
+
``docs/_internal/explorations/schema-versioning-migration.md`` for the full
|
|
11
|
+
strategy (read-time adaptation, coexist-always + optional-migrate).
|
|
12
|
+
|
|
13
|
+
Versioning is SemVer and **decoupled from the testerkit package version** — the
|
|
14
|
+
stores diverge on their own lines, so schema ``0.1`` at package ``0.3.0`` is
|
|
15
|
+
deliberate, not a mismatch.
|
|
16
|
+
|
|
17
|
+
The breaking unit is the **epoch = leftmost-significant SemVer component**:
|
|
18
|
+
|
|
19
|
+
- **Pre-1.0** — the **MINOR** is the epoch. ``0.1 -> 0.2`` is a breaking
|
|
20
|
+
reshape (a new epoch); there is no additive tier yet. Each 0.x epoch is a
|
|
21
|
+
clean break that regenerates or read-time-adapts prior-epoch artifacts —
|
|
22
|
+
deliberately rehearsing the same epoch -> quarantine/adapter path we'll bet
|
|
23
|
+
on at the first real ``2.0``, so the apparatus is proven before 1.0.
|
|
24
|
+
- **Post-1.0** — the **MAJOR** is the epoch. ``1.0 -> 1.1`` becomes additive
|
|
25
|
+
(``union_by_name`` null-fills old files, ``ALTER TABLE ADD COLUMN IF NOT
|
|
26
|
+
EXISTS`` extends the projection); ``1.x -> 2.0`` is the breaking epoch,
|
|
27
|
+
needing a per-version read-time adapter + a frozen reference doc for the
|
|
28
|
+
outgoing epoch.
|
|
29
|
+
|
|
30
|
+
All stores start at ``"0.1"`` — a distinct pre-1.0 schema line, **not frozen**.
|
|
31
|
+
1.0 is graduated to later, once the schema design and this apparatus have real
|
|
32
|
+
mileage (see ``docs/_internal/explorations/pre-1.0-epoch-strategy.md``).
|
|
33
|
+
Unstamped artifacts are unsupported by design (regenerate).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from enum import StrEnum
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SchemaStore(StrEnum):
|
|
42
|
+
"""A durable-artifact store that stamps and dispatches on a schema version.
|
|
43
|
+
|
|
44
|
+
Events carries **two** coordinates (§3 of the strategy doc): the storage
|
|
45
|
+
``ENVELOPE`` (the ``_IPC_SCHEMA`` column shape) and the payload
|
|
46
|
+
``EVENT_CATALOG`` (the event models). They version independently.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
RUNS = "runs"
|
|
50
|
+
"""Runs parquet — content fused into the columnar schema (footer metadata)."""
|
|
51
|
+
EVENTS_ENVELOPE = "events_envelope"
|
|
52
|
+
"""Event WAL Arrow IPC envelope — ``event_log._IPC_SCHEMA``."""
|
|
53
|
+
EVENT_CATALOG = "event_catalog"
|
|
54
|
+
"""Event payload catalog — the event models in ``data.events``."""
|
|
55
|
+
CHANNELS = "channels"
|
|
56
|
+
"""Channel ``.arrow`` skeleton, value typed by ``value_type``."""
|
|
57
|
+
FILES = "files"
|
|
58
|
+
"""FileStore sidecar (``FileArtifactMetadata``); the blob is opaque."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# The stamp a freshly written artifact carries, per store. This is the one
|
|
62
|
+
# home for the current version — every store's public constant aliases the
|
|
63
|
+
# matching entry here (do not hardcode the string at the write site).
|
|
64
|
+
CURRENT_SCHEMA_VERSION: dict[SchemaStore, str] = {
|
|
65
|
+
SchemaStore.RUNS: "0.1",
|
|
66
|
+
SchemaStore.EVENTS_ENVELOPE: "0.1",
|
|
67
|
+
SchemaStore.EVENT_CATALOG: "0.1",
|
|
68
|
+
SchemaStore.CHANNELS: "0.1",
|
|
69
|
+
SchemaStore.FILES: "0.1",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Older epochs a store still ships a read-time adapter for. Empty today
|
|
73
|
+
# (0.1-only). Add an entry the *same commit* its ``vN -> current`` adapter
|
|
74
|
+
# lands, so the whitelist reader starts accepting that version exactly when it
|
|
75
|
+
# can transform it.
|
|
76
|
+
_LEGACY_READABLE: dict[SchemaStore, frozenset[str]] = {store: frozenset() for store in SchemaStore}
|
|
77
|
+
|
|
78
|
+
# Versions each store's reader will dispatch. Current ∪ legacy-readable, so the
|
|
79
|
+
# current version is always accepted by construction. Anything not in this set
|
|
80
|
+
# is refused at read time ("unsupported schema version"); an absent stamp is
|
|
81
|
+
# refused as unstamped/pre-baseline ("regenerate").
|
|
82
|
+
KNOWN_SCHEMA_VERSIONS: dict[SchemaStore, frozenset[str]] = {
|
|
83
|
+
store: frozenset({CURRENT_SCHEMA_VERSION[store]}) | _LEGACY_READABLE[store]
|
|
84
|
+
for store in SchemaStore
|
|
85
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Canonical Arrow schema contract shared across all backends.
|
|
2
|
+
|
|
3
|
+
Defines the measurement schema, type inference for dynamic columns,
|
|
4
|
+
and helpers for constructing validated Arrow tables. Any backend
|
|
5
|
+
(Parquet, Flight, NATS) imports from here rather than defining its own
|
|
6
|
+
schema.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pyarrow as pa
|
|
13
|
+
|
|
14
|
+
from testerkit.data.backends._row_helpers import (
|
|
15
|
+
INSTRUMENT_STRUCT_FIELDS,
|
|
16
|
+
LANE_FIELDS,
|
|
17
|
+
MEASUREMENT_STRUCT_FIELDS,
|
|
18
|
+
)
|
|
19
|
+
from testerkit.data.schema_versions import CURRENT_SCHEMA_VERSION, SchemaStore
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"RUN_ROW_SCHEMA",
|
|
23
|
+
"SCHEMA_VERSION",
|
|
24
|
+
"INSTRUMENT_STRUCT_FIELDS",
|
|
25
|
+
"_INSTRUMENT_LIST",
|
|
26
|
+
"_INSTRUMENT_STRUCT",
|
|
27
|
+
"_LANE_LIST",
|
|
28
|
+
"_MEASUREMENT_LIST",
|
|
29
|
+
"_MEASUREMENT_STRUCT",
|
|
30
|
+
"_SCHEMA_DICT",
|
|
31
|
+
"_build_write_schema",
|
|
32
|
+
"table_from_rows",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Runs parquet footer stamp. Sourced from the central registry (one home);
|
|
36
|
+
# see ``testerkit.data.schema_versions`` for the SemVer / migration contract.
|
|
37
|
+
SCHEMA_VERSION = CURRENT_SCHEMA_VERSION[SchemaStore.RUNS]
|
|
38
|
+
|
|
39
|
+
# EAV lane struct — the nested at-rest representation of one input / output
|
|
40
|
+
# entry. ``value_type`` selects which ``value_*`` lane holds the value. Field
|
|
41
|
+
# names must match ``_row_helpers.LANE_FIELDS`` / the encoder (guarded below).
|
|
42
|
+
_LANE_STRUCT = pa.struct(
|
|
43
|
+
[
|
|
44
|
+
("name", pa.string()),
|
|
45
|
+
("value_type", pa.string()),
|
|
46
|
+
("value_int", pa.int64()),
|
|
47
|
+
("value_double", pa.float64()),
|
|
48
|
+
("value_bool", pa.bool_()),
|
|
49
|
+
("value_text", pa.string()),
|
|
50
|
+
("value_timestamp", pa.timestamp("us", tz="UTC")),
|
|
51
|
+
("value_json", pa.string()),
|
|
52
|
+
("unit", pa.string()),
|
|
53
|
+
("uut_pin", pa.string()),
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
assert [f.name for f in _LANE_STRUCT] == list(LANE_FIELDS), (
|
|
57
|
+
"schemas._LANE_STRUCT drifted from _row_helpers.LANE_FIELDS"
|
|
58
|
+
)
|
|
59
|
+
_LANE_LIST = pa.list_(_LANE_STRUCT)
|
|
60
|
+
|
|
61
|
+
# Nested measurement struct — the at-rest representation of one measurement,
|
|
62
|
+
# carried in the vector row's ``measurements`` LIST. Field names must match
|
|
63
|
+
# ``_row_helpers.MEASUREMENT_STRUCT_FIELDS`` (guarded below). The daemon
|
|
64
|
+
# UNNESTs these into the flat measurement fact at ingest.
|
|
65
|
+
_MEASUREMENT_STRUCT = pa.struct(
|
|
66
|
+
[
|
|
67
|
+
("name", pa.string()),
|
|
68
|
+
("value", pa.float64()),
|
|
69
|
+
("unit", pa.string()),
|
|
70
|
+
("outcome", pa.string()),
|
|
71
|
+
("timestamp", pa.timestamp("us", tz="UTC")),
|
|
72
|
+
("limit_low", pa.float64()),
|
|
73
|
+
("limit_high", pa.float64()),
|
|
74
|
+
("limit_nominal", pa.float64()),
|
|
75
|
+
("limit_comparator", pa.string()),
|
|
76
|
+
("characteristic_id", pa.string()),
|
|
77
|
+
("spec_ref", pa.string()),
|
|
78
|
+
("uut_pin", pa.string()),
|
|
79
|
+
("fixture_connection", pa.string()),
|
|
80
|
+
("instrument_name", pa.string()),
|
|
81
|
+
("instrument_resource", pa.string()),
|
|
82
|
+
("instrument_channel", pa.string()),
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
assert [f.name for f in _MEASUREMENT_STRUCT] == list(MEASUREMENT_STRUCT_FIELDS), (
|
|
86
|
+
"schemas._MEASUREMENT_STRUCT drifted from _row_helpers.MEASUREMENT_STRUCT_FIELDS"
|
|
87
|
+
)
|
|
88
|
+
_MEASUREMENT_LIST = pa.list_(_MEASUREMENT_STRUCT)
|
|
89
|
+
|
|
90
|
+
# Instrument inventory struct — the at-rest representation of one instrument
|
|
91
|
+
# connected during a run. Carried as a ``LIST<STRUCT>`` on every row
|
|
92
|
+
# (dense, self-describing). The daemon UNNESTs these into the flat
|
|
93
|
+
# ``instruments_materialized`` table for queries. Field names must match
|
|
94
|
+
# ``_row_helpers.INSTRUMENT_STRUCT_FIELDS`` (guarded below).
|
|
95
|
+
_INSTRUMENT_STRUCT = pa.struct(
|
|
96
|
+
[
|
|
97
|
+
("name", pa.string()),
|
|
98
|
+
("id", pa.string()),
|
|
99
|
+
("driver", pa.string()),
|
|
100
|
+
("resource", pa.string()),
|
|
101
|
+
("protocol", pa.string()),
|
|
102
|
+
("manufacturer", pa.string()),
|
|
103
|
+
("model", pa.string()),
|
|
104
|
+
("serial_number", pa.string()),
|
|
105
|
+
("firmware", pa.string()),
|
|
106
|
+
("cal_due", pa.string()),
|
|
107
|
+
("cal_last", pa.string()),
|
|
108
|
+
("cal_certificate", pa.string()),
|
|
109
|
+
("cal_lab", pa.string()),
|
|
110
|
+
("mocked", pa.bool_()),
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
assert [f.name for f in _INSTRUMENT_STRUCT] == list(INSTRUMENT_STRUCT_FIELDS), (
|
|
114
|
+
"schemas._INSTRUMENT_STRUCT drifted from _row_helpers.INSTRUMENT_STRUCT_FIELDS"
|
|
115
|
+
)
|
|
116
|
+
_INSTRUMENT_LIST = pa.list_(_INSTRUMENT_STRUCT)
|
|
117
|
+
|
|
118
|
+
# Canonical row schema for the unified per-run parquet — a chronological
|
|
119
|
+
# telling of the run. Every row carries an explicit ``record_type``
|
|
120
|
+
# discriminator with one of three values:
|
|
121
|
+
# * ``record_type = 'run'`` — one row per run; run / UUT / station /
|
|
122
|
+
# environment context. Step / vector columns are NULL.
|
|
123
|
+
# * ``record_type = 'step'`` — one step-execution, keyed ``(step_path,
|
|
124
|
+
# step_retry, vector_outer_index)``; carries code identity + timing +
|
|
125
|
+
# rolled-up outcome, and its OWN ``inputs``/``outputs``/``measurements``
|
|
126
|
+
# (a non-looping step can have zero vector rows — the step row carries
|
|
127
|
+
# its own data, no synthesized scope vector). ``vector_index`` is always
|
|
128
|
+
# NULL on this row kind.
|
|
129
|
+
# * ``record_type = 'vector'`` — one condition-point execution carrier: a
|
|
130
|
+
# swept step's own sweep variant, or an in-body ``vectors`` loop
|
|
131
|
+
# iteration. Holds the ``inputs``/``outputs`` lanes and the nested
|
|
132
|
+
# ``measurements`` list for that execution; ``vector_index`` is its own
|
|
133
|
+
# 0..N position.
|
|
134
|
+
#
|
|
135
|
+
# ``inputs`` / ``outputs`` are nested ``LIST<STRUCT<lanes>>`` columns (see
|
|
136
|
+
# ``_LANE_STRUCT``), not wide ``in_*``/``out_*`` columns; the DuckDB daemon
|
|
137
|
+
# UNNESTs them into the honestly-named ``inputs``/``outputs`` EAV tables (one
|
|
138
|
+
# per role, no ``role`` column) for queries. ``measurements`` is a nested
|
|
139
|
+
# ``LIST<STRUCT>`` on the vector row; the daemon UNNESTs it into the flat
|
|
140
|
+
# measurement fact for queries.
|
|
141
|
+
RUN_ROW_SCHEMA = pa.schema(
|
|
142
|
+
[
|
|
143
|
+
# Discriminator — 'run', 'step', or 'vector'
|
|
144
|
+
("record_type", pa.string()),
|
|
145
|
+
# Identity & timing
|
|
146
|
+
("session_id", pa.string()),
|
|
147
|
+
("run_id", pa.string()),
|
|
148
|
+
("site_index", pa.int64()),
|
|
149
|
+
("site_name", pa.string()),
|
|
150
|
+
("run_started_at", pa.timestamp("us", tz="UTC")),
|
|
151
|
+
("run_ended_at", pa.timestamp("us", tz="UTC")),
|
|
152
|
+
("step_name", pa.string()),
|
|
153
|
+
("step_index", pa.int64()),
|
|
154
|
+
("step_path", pa.string()),
|
|
155
|
+
("step_started_at", pa.timestamp("us", tz="UTC")),
|
|
156
|
+
("step_ended_at", pa.timestamp("us", tz="UTC")),
|
|
157
|
+
("step_node_id", pa.string()),
|
|
158
|
+
("step_module", pa.string()),
|
|
159
|
+
("step_file", pa.string()),
|
|
160
|
+
("step_class", pa.string()),
|
|
161
|
+
("step_function", pa.string()),
|
|
162
|
+
("step_markers", pa.string()),
|
|
163
|
+
# step_retry: 0-based outer (item) retry — pytest-rerunfailures rerun
|
|
164
|
+
# count of this step. Distinct from vector_retry (the inner per-vector
|
|
165
|
+
# retry). Both stamp every execution so a rerun is a distinct row.
|
|
166
|
+
("step_retry", pa.int64()),
|
|
167
|
+
("vector_index", pa.int64()),
|
|
168
|
+
("vector_outer_index", pa.int64()),
|
|
169
|
+
("vector_retry", pa.int64()),
|
|
170
|
+
("vector_started_at", pa.timestamp("us", tz="UTC")),
|
|
171
|
+
("vector_ended_at", pa.timestamp("us", tz="UTC")),
|
|
172
|
+
# Who
|
|
173
|
+
("operator_id", pa.string()),
|
|
174
|
+
("operator_name", pa.string()),
|
|
175
|
+
# UUT
|
|
176
|
+
("uut_serial_number", pa.string()),
|
|
177
|
+
("uut_part_number", pa.string()),
|
|
178
|
+
("uut_revision", pa.string()),
|
|
179
|
+
("uut_lot_number", pa.string()),
|
|
180
|
+
# Part
|
|
181
|
+
("part_id", pa.string()),
|
|
182
|
+
("part_name", pa.string()),
|
|
183
|
+
("part_revision", pa.string()),
|
|
184
|
+
# Station
|
|
185
|
+
("station_id", pa.string()),
|
|
186
|
+
("station_name", pa.string()),
|
|
187
|
+
("station_type", pa.string()),
|
|
188
|
+
("station_location", pa.string()),
|
|
189
|
+
("station_hostname", pa.string()),
|
|
190
|
+
# Fixture
|
|
191
|
+
("fixture_id", pa.string()),
|
|
192
|
+
# Test context
|
|
193
|
+
("test_phase", pa.string()),
|
|
194
|
+
("project_name", pa.string()),
|
|
195
|
+
("git_commit", pa.string()),
|
|
196
|
+
("git_branch", pa.string()),
|
|
197
|
+
("git_remote", pa.string()),
|
|
198
|
+
# Rollup
|
|
199
|
+
("step_outcome", pa.string()),
|
|
200
|
+
("vector_outcome", pa.string()),
|
|
201
|
+
("run_outcome", pa.string()),
|
|
202
|
+
# Environment traceability
|
|
203
|
+
("python_version", pa.string()),
|
|
204
|
+
("testerkit_version", pa.string()),
|
|
205
|
+
("env_fingerprint", pa.string()),
|
|
206
|
+
# Dynamic attributes — nested EAV lanes (see _row_helpers). Names are
|
|
207
|
+
# values inside the structs, so there is no column explosion.
|
|
208
|
+
("inputs", _LANE_LIST),
|
|
209
|
+
("outputs", _LANE_LIST),
|
|
210
|
+
# Nested measurements on the vector row; the daemon UNNESTs these into
|
|
211
|
+
# the flat measurement fact at ingest.
|
|
212
|
+
("measurements", _MEASUREMENT_LIST),
|
|
213
|
+
# Instrument inventory on every row (dense, self-describing); the daemon
|
|
214
|
+
# UNNESTs these into the flat ``instruments_materialized`` table.
|
|
215
|
+
("instruments", _INSTRUMENT_LIST),
|
|
216
|
+
],
|
|
217
|
+
# Stamp the schema itself with its version (like events' ``_IPC_SCHEMA``), so
|
|
218
|
+
# every table built with ``schema=RUN_ROW_SCHEMA`` carries the stamp by
|
|
219
|
+
# construction — the real write path overrides this metadata with the fuller
|
|
220
|
+
# ``_build_parquet_metadata`` dict (still schema_version), and fixtures that
|
|
221
|
+
# write directly are stamped automatically instead of silently unstamped.
|
|
222
|
+
metadata={b"schema_version": SCHEMA_VERSION.encode()},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
_SCHEMA_DICT = {f.name: f.type for f in RUN_ROW_SCHEMA}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _infer_type_from_value(value: Any) -> pa.DataType:
|
|
229
|
+
"""Infer Arrow type from a Python value. None → string (safe default)."""
|
|
230
|
+
if value is None:
|
|
231
|
+
return pa.string()
|
|
232
|
+
if isinstance(value, bool):
|
|
233
|
+
return pa.bool_()
|
|
234
|
+
if isinstance(value, (int, float)):
|
|
235
|
+
return pa.float64()
|
|
236
|
+
if isinstance(value, datetime):
|
|
237
|
+
return pa.timestamp("us", tz="UTC")
|
|
238
|
+
if isinstance(value, list):
|
|
239
|
+
return pa.list_(pa.string())
|
|
240
|
+
return pa.string()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _build_write_schema(rows: list[dict[str, Any]]) -> pa.Schema:
|
|
244
|
+
"""Build complete Arrow schema: fixed canonical + dynamic columns.
|
|
245
|
+
|
|
246
|
+
Fixed columns (including the nested ``inputs``/``outputs`` lanes and
|
|
247
|
+
the ``instruments`` struct list) use ``RUN_ROW_SCHEMA`` types. Any other
|
|
248
|
+
stray column is inferred from its first non-None value. Passed to
|
|
249
|
+
``pa.Table.from_pylist()`` so Arrow validates at construction time.
|
|
250
|
+
|
|
251
|
+
Single pass over rows to collect keys and first non-None values.
|
|
252
|
+
"""
|
|
253
|
+
first_values: dict[str, Any] = {}
|
|
254
|
+
all_keys: set[str] = set()
|
|
255
|
+
for row in rows:
|
|
256
|
+
for k, v in row.items():
|
|
257
|
+
all_keys.add(k)
|
|
258
|
+
if k not in first_values and v is not None:
|
|
259
|
+
first_values[k] = v
|
|
260
|
+
|
|
261
|
+
fields: list[pa.Field] = []
|
|
262
|
+
used: set[str] = set()
|
|
263
|
+
|
|
264
|
+
# Fixed columns first, in canonical order
|
|
265
|
+
for field in RUN_ROW_SCHEMA:
|
|
266
|
+
if field.name in all_keys:
|
|
267
|
+
fields.append(field)
|
|
268
|
+
used.add(field.name)
|
|
269
|
+
|
|
270
|
+
# Remaining dynamic columns inferred from values
|
|
271
|
+
for key in sorted(all_keys - used):
|
|
272
|
+
fields.append(pa.field(key, _infer_type_from_value(first_values.get(key))))
|
|
273
|
+
|
|
274
|
+
# Carry the version stamp (as RUN_ROW_SCHEMA does), so tables built from this
|
|
275
|
+
# dynamic schema are stamped by construction. The real write path overrides
|
|
276
|
+
# with the fuller _build_parquet_metadata dict; direct writers are covered.
|
|
277
|
+
return pa.schema(fields, metadata={b"schema_version": SCHEMA_VERSION.encode()})
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def table_from_rows(rows: list[dict[str, Any]], schema: pa.Schema) -> pa.Table:
|
|
281
|
+
"""Build a PyArrow Table from row dicts against ``schema``.
|
|
282
|
+
|
|
283
|
+
Wraps ``pa.Table.from_pylist`` so a build failure carries row context.
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
return pa.Table.from_pylist(rows, schema=schema)
|
|
287
|
+
except (pa.ArrowInvalid, pa.ArrowTypeError, pa.ArrowNotImplementedError) as exc:
|
|
288
|
+
raise pa.ArrowInvalid(f"Cannot build measurement table from rows: {exc}") from exc
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Internal event-subscriber registry — used by the ``testerkit export``
|
|
2
|
+
CLI replay path (csv/json/stdf/hdf5/tdms/mdf4 converters in
|
|
3
|
+
:mod:`testerkit.data.exporters`). The canonical run materializer
|
|
4
|
+
(parquet + DuckDB index) is owned by the runs daemon and does NOT
|
|
5
|
+
go through this registry.
|
|
6
|
+
|
|
7
|
+
Not a public extension protocol: third-party packages should not register
|
|
8
|
+
formats via entry points or any other mechanism. The set of supported
|
|
9
|
+
formats is fixed by the package and ships through ``testerkit export``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import testerkit.data.exporters.csv_exporter # noqa: F401
|
|
13
|
+
import testerkit.data.exporters.json_exporter # noqa: F401
|
|
14
|
+
from testerkit.data.event_log import EventSubscriber
|
|
15
|
+
from testerkit.data.subscribers._base import get_subscriber_class, list_subscribers
|
|
16
|
+
from testerkit.data.subscribers._output_file import OutputFile
|
|
17
|
+
from testerkit.data.subscribers.replay import replay_to_subscriber
|
|
18
|
+
|
|
19
|
+
# Optional industry-format subscribers — registered via ``__init_subclass__``
|
|
20
|
+
# at import time when their respective extras are installed
|
|
21
|
+
# (``testerkit[stdf]``, ``[hdf5]``, ``[tdms]``, ``[mdf4]``). When the
|
|
22
|
+
# extra isn't installed the import fails; swallow so the rest of the
|
|
23
|
+
# package still loads. The CLI surfaces a "format not registered" error
|
|
24
|
+
# pointing at the right extra.
|
|
25
|
+
try:
|
|
26
|
+
import testerkit.data.exporters.stdf # noqa: F401
|
|
27
|
+
except ImportError:
|
|
28
|
+
pass
|
|
29
|
+
try:
|
|
30
|
+
import testerkit.data.exporters.hdf5 # noqa: F401
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
33
|
+
try:
|
|
34
|
+
import testerkit.data.exporters.tdms # noqa: F401
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
37
|
+
try:
|
|
38
|
+
import testerkit.data.exporters.mdf4 # noqa: F401
|
|
39
|
+
except ImportError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"EventSubscriber",
|
|
44
|
+
"OutputFile",
|
|
45
|
+
"get_subscriber_class",
|
|
46
|
+
"list_subscribers",
|
|
47
|
+
"replay_to_subscriber",
|
|
48
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Subscriber lookup functions.
|
|
2
|
+
|
|
3
|
+
Registration is automatic via ``EventSubscriber.__init_subclass__``.
|
|
4
|
+
Built-in subscribers are imported in ``__init__.py``.
|
|
5
|
+
Third-party plugins are loaded via entry points.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from testerkit.data.event_log import EventSubscriber
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_subscriber_class(format_name: str) -> type[EventSubscriber] | None:
|
|
14
|
+
"""Look up a subscriber class by format name."""
|
|
15
|
+
return EventSubscriber._registry.get(format_name)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_subscribers() -> list[str]:
|
|
19
|
+
"""Return sorted list of all registered subscriber format names."""
|
|
20
|
+
return sorted(EventSubscriber._registry)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""OutputFile descriptor — metadata about a file produced by a subscriber.
|
|
2
|
+
|
|
3
|
+
Transports use ``format`` to decide how to ship the file. The ``on_output``
|
|
4
|
+
callback receives an ``OutputFile`` after each successful write.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class OutputFile:
|
|
15
|
+
"""Describes a file produced by a subscriber.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
path: Absolute path to the written file.
|
|
19
|
+
format: Format name (``"parquet"``, ``"csv"``, ``"stdf"``, etc.).
|
|
20
|
+
run_id: Run ID associated with the file, if known.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
path: Path
|
|
24
|
+
format: str
|
|
25
|
+
run_id: str | None = None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Replay stored events through a subscriber — the post-hoc bridge.
|
|
2
|
+
|
|
3
|
+
This replaces the old ``reconstruct_test_run → exporter.export()`` chain.
|
|
4
|
+
Any subscriber can be fed stored events to produce its output format
|
|
5
|
+
after the fact, without needing a live test session.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from testerkit.data.subscribers.replay import replay_to_subscriber
|
|
10
|
+
from testerkit.data.exporters.json_exporter import JsonSubscriber
|
|
11
|
+
|
|
12
|
+
sub = JsonSubscriber(output_dir=Path("out"))
|
|
13
|
+
replay_to_subscriber(sub, event_dicts)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import warnings
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from pydantic import TypeAdapter, ValidationError
|
|
22
|
+
|
|
23
|
+
from testerkit.data.event_log import EventSubscriber
|
|
24
|
+
from testerkit.data.events import Event
|
|
25
|
+
|
|
26
|
+
_event_adapter: TypeAdapter[Any] = TypeAdapter(Event)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def replay_to_subscriber(
|
|
30
|
+
subscriber: EventSubscriber,
|
|
31
|
+
events: list[dict[str, Any]],
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Deserialize event dicts and feed them through a subscriber.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
subscriber: An ``EventSubscriber`` instance (has ``open``,
|
|
37
|
+
``on_event``, ``close``, and ``event_types``).
|
|
38
|
+
events: Raw event dicts, e.g. from ``EventLog.events()``
|
|
39
|
+
or ``EventStore.events(session_id=...)``.
|
|
40
|
+
"""
|
|
41
|
+
subscriber.open()
|
|
42
|
+
try:
|
|
43
|
+
for raw in events:
|
|
44
|
+
try:
|
|
45
|
+
event = _event_adapter.validate_python(raw)
|
|
46
|
+
except ValidationError:
|
|
47
|
+
warnings.warn(
|
|
48
|
+
f"Skipping invalid event: {raw.get('event_type', '?')}",
|
|
49
|
+
stacklevel=2,
|
|
50
|
+
)
|
|
51
|
+
continue
|
|
52
|
+
if type(event) in subscriber.event_types:
|
|
53
|
+
subscriber.on_event(event)
|
|
54
|
+
finally:
|
|
55
|
+
subscriber.close()
|
testerkit/environment.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Environment snapshot for software traceability.
|
|
2
|
+
|
|
3
|
+
Captures the Python runtime environment (version, OS, top-level dependencies)
|
|
4
|
+
at test start. Full package resolution is tracked by ``uv.lock`` in git;
|
|
5
|
+
we store only the hash here for correlation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import logging
|
|
12
|
+
import platform
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EnvironmentSnapshot(BaseModel):
|
|
21
|
+
"""Snapshot of the software environment at test time."""
|
|
22
|
+
|
|
23
|
+
python_version: str
|
|
24
|
+
os_name: str
|
|
25
|
+
os_version: str
|
|
26
|
+
platform_machine: str
|
|
27
|
+
testerkit_version: str
|
|
28
|
+
dependencies: list[str]
|
|
29
|
+
lockfile_hash: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_top_level_deps() -> list[str]:
|
|
33
|
+
"""Read direct dependencies from pyproject.toml [project.dependencies]."""
|
|
34
|
+
pyproject = Path("pyproject.toml")
|
|
35
|
+
if not pyproject.exists():
|
|
36
|
+
return []
|
|
37
|
+
try:
|
|
38
|
+
import tomllib
|
|
39
|
+
except ModuleNotFoundError:
|
|
40
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
data = tomllib.loads(pyproject.read_text())
|
|
44
|
+
return sorted(data.get("project", {}).get("dependencies", []))
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
logger.debug("Could not parse pyproject.toml: %s", exc)
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _hash_file(path: Path) -> str | None:
|
|
51
|
+
"""SHA-256 prefix of a file, or None if missing/unreadable."""
|
|
52
|
+
try:
|
|
53
|
+
if path.exists():
|
|
54
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
logger.debug("Could not read %s: %s", path, exc)
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def capture_environment() -> EnvironmentSnapshot:
|
|
61
|
+
"""Capture current environment snapshot.
|
|
62
|
+
|
|
63
|
+
Stores top-level dependencies from pyproject.toml (the ones you chose),
|
|
64
|
+
not the full transitive dependency tree. ``lockfile_hash`` covers the
|
|
65
|
+
exact resolved versions; ``pyproject_hash`` covers the intent.
|
|
66
|
+
"""
|
|
67
|
+
from testerkit import __version__
|
|
68
|
+
|
|
69
|
+
# Hash lockfile (uv.lock preferred, fallback to others)
|
|
70
|
+
lockfile_hash = None
|
|
71
|
+
for name in ("uv.lock", "poetry.lock", "Pipfile.lock", "requirements.txt"):
|
|
72
|
+
lockfile_hash = _hash_file(Path(name))
|
|
73
|
+
if lockfile_hash:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
return EnvironmentSnapshot(
|
|
77
|
+
python_version=platform.python_version(),
|
|
78
|
+
os_name=platform.system(),
|
|
79
|
+
os_version=platform.release(),
|
|
80
|
+
platform_machine=platform.machine(),
|
|
81
|
+
testerkit_version=__version__,
|
|
82
|
+
dependencies=_read_top_level_deps(),
|
|
83
|
+
lockfile_hash=lockfile_hash,
|
|
84
|
+
)
|