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,524 @@
|
|
|
1
|
+
"""Filter facets and field-reference types for the parametric viewer.
|
|
2
|
+
|
|
3
|
+
The ``/explore`` page lets users scope cross-run measurement queries by
|
|
4
|
+
filtering on columns of the ``measurements`` view. This module is the
|
|
5
|
+
single source of truth for *which columns are facetable* and *what kind
|
|
6
|
+
of facet each column wants* — closed enum sets vs open string sets vs
|
|
7
|
+
date ranges.
|
|
8
|
+
|
|
9
|
+
Closed sets (``Outcome``, ``Comparator``) come straight from the
|
|
10
|
+
Pydantic models in ``testerkit.data.models`` / ``testerkit.models.enums`` —
|
|
11
|
+
no DB query needed; the universe is known at import time. Open sets
|
|
12
|
+
(``part_id``, ``station_id``, ``uut_serial_number``, ``step_name``,
|
|
13
|
+
``measurement_name``, ``test_phase``) require a ``SELECT DISTINCT``
|
|
14
|
+
against the current filter set so the dropdowns reflect what the user
|
|
15
|
+
can actually pick from given their other selections.
|
|
16
|
+
|
|
17
|
+
The Pydantic types here also describe every analytics result row that
|
|
18
|
+
crosses a boundary — ``ParametricRow`` / ``HistogramRow`` /
|
|
19
|
+
``FacetOption`` / ``SummaryCounts`` / ``FilterSet``. No
|
|
20
|
+
``dict[str, Any]`` enters or exits ``MeasurementsQuery`` with these in
|
|
21
|
+
place.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from datetime import date, datetime
|
|
27
|
+
from enum import StrEnum
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel, Field, model_validator
|
|
30
|
+
|
|
31
|
+
from testerkit.data.models import Outcome
|
|
32
|
+
from testerkit.models.enums import Comparator
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Field identity — role + name + optional value_type
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FieldRole(StrEnum):
|
|
40
|
+
"""Which role a recorded field plays in a measurement vector."""
|
|
41
|
+
|
|
42
|
+
INPUT = "input"
|
|
43
|
+
OUTPUT = "output"
|
|
44
|
+
MEASUREMENT = "measurement"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FieldRef(BaseModel):
|
|
48
|
+
"""Reference to a named field, identified by (role, name).
|
|
49
|
+
|
|
50
|
+
Use the classmethod constructors for everyday code::
|
|
51
|
+
|
|
52
|
+
FieldRef.measurement("v_rail")
|
|
53
|
+
FieldRef.output("v_rail")
|
|
54
|
+
FieldRef.input("vin")
|
|
55
|
+
|
|
56
|
+
The plain constructor also works and is used at wire boundaries::
|
|
57
|
+
|
|
58
|
+
FieldRef(role=FieldRole.OUTPUT, name="v_rail")
|
|
59
|
+
FieldRef(role="output", name="v_rail") # FieldRole coerces from str
|
|
60
|
+
|
|
61
|
+
``value_type`` is an open string (not an enum) — it reflects the
|
|
62
|
+
stored tag (e.g. ``"scalar:float"``, ``"scalar:int"``) and is only
|
|
63
|
+
required when a (role, name) pair has mixed value_types in scope.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
role: FieldRole
|
|
67
|
+
name: str
|
|
68
|
+
value_type: str | None = None
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def input(cls, name: str, value_type: str | None = None) -> FieldRef:
|
|
72
|
+
return cls(role=FieldRole.INPUT, name=name, value_type=value_type)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def output(cls, name: str, value_type: str | None = None) -> FieldRef:
|
|
76
|
+
return cls(role=FieldRole.OUTPUT, name=name, value_type=value_type)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def measurement(cls, name: str, value_type: str | None = None) -> FieldRef:
|
|
80
|
+
return cls(role=FieldRole.MEASUREMENT, name=name, value_type=value_type)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# describe_columns() result models
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FixedColumnDescriptor(BaseModel):
|
|
89
|
+
"""One plottable fixed column from the measurements view."""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
column_type: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DynamicFieldDescriptor(BaseModel):
|
|
96
|
+
"""One role-keyed field discovered in the catalog."""
|
|
97
|
+
|
|
98
|
+
role: FieldRole
|
|
99
|
+
name: str
|
|
100
|
+
value_types: list[str]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ColumnSchema(BaseModel):
|
|
104
|
+
"""Return type of ``MeasurementsQuery.describe_columns()``."""
|
|
105
|
+
|
|
106
|
+
fixed: list[FixedColumnDescriptor]
|
|
107
|
+
fields: list[DynamicFieldDescriptor]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FacetKind(StrEnum):
|
|
111
|
+
"""How a facet's options are sourced and displayed in the UI."""
|
|
112
|
+
|
|
113
|
+
ENUM = "enum"
|
|
114
|
+
STRING = "string"
|
|
115
|
+
DATE = "date"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class FacetSpec(BaseModel):
|
|
119
|
+
"""Self-describing definition of one filter facet.
|
|
120
|
+
|
|
121
|
+
``column`` is the facet's storage column — the ``FilterSet`` dict key,
|
|
122
|
+
and the value that's filtered and enumerated. Facets name the real
|
|
123
|
+
storage column (read follows storage), so the same key means the same
|
|
124
|
+
thing on every page. ``param`` gives an optional friendlier URL query
|
|
125
|
+
key (e.g. ``?part=`` for ``uut_part_number``); empty ``param`` → URL
|
|
126
|
+
key is ``column``. ``expr`` lets a facet filter/enumerate on a SQL
|
|
127
|
+
expression instead of the bare column (e.g. ``test_phase`` labels its
|
|
128
|
+
NULLs ``'unknown'``); empty ``expr`` → ``column`` directly.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
column: str
|
|
132
|
+
kind: FacetKind
|
|
133
|
+
enum_class: type[StrEnum] | None = None
|
|
134
|
+
label: str
|
|
135
|
+
description: str = ""
|
|
136
|
+
param: str = ""
|
|
137
|
+
expr: str = ""
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def url_key(self) -> str:
|
|
141
|
+
"""The query-string key this facet reads/writes (``param`` or ``column``)."""
|
|
142
|
+
return self.param or self.column
|
|
143
|
+
|
|
144
|
+
@model_validator(mode="after")
|
|
145
|
+
def _enum_class_required_for_enum_kind(self) -> FacetSpec:
|
|
146
|
+
if self.kind is FacetKind.ENUM and self.enum_class is None:
|
|
147
|
+
raise ValueError(f"{self.column}: ENUM facet requires enum_class")
|
|
148
|
+
if self.kind is not FacetKind.ENUM and self.enum_class is not None:
|
|
149
|
+
raise ValueError(f"{self.column}: enum_class only valid for ENUM facets")
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class FacetOption(BaseModel):
|
|
154
|
+
"""One pickable value within a facet, with the row count it covers."""
|
|
155
|
+
|
|
156
|
+
value: str
|
|
157
|
+
count: int
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SummaryCounts(BaseModel):
|
|
161
|
+
"""Cardinality stats for the cardinality badge — single query result."""
|
|
162
|
+
|
|
163
|
+
total_rows: int
|
|
164
|
+
distinct_runs: int
|
|
165
|
+
distinct_measurements: int
|
|
166
|
+
distinct_parts: int
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ParametricRow(BaseModel):
|
|
170
|
+
"""One long-format row from a scatter / line / bar parametric query.
|
|
171
|
+
|
|
172
|
+
``x`` widens to accept datetime / date because measurements view
|
|
173
|
+
columns include timestamps; the chart layer coerces these to
|
|
174
|
+
epoch ms for ECharts ``time`` axes. ``group`` is always coerced to
|
|
175
|
+
str so numeric EAV fields used as group_by axes render as labels.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
x: float | str | datetime | date | None = None
|
|
179
|
+
y: float
|
|
180
|
+
group: str = ""
|
|
181
|
+
|
|
182
|
+
@model_validator(mode="before")
|
|
183
|
+
@classmethod
|
|
184
|
+
def _coerce_group(cls, data: object) -> object:
|
|
185
|
+
if isinstance(data, dict) and "group" in data and data["group"] is not None:
|
|
186
|
+
data = dict(data)
|
|
187
|
+
data["group"] = str(data["group"])
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class HistogramRow(BaseModel):
|
|
192
|
+
"""One bin in a histogram result. ``x`` is the bin midpoint."""
|
|
193
|
+
|
|
194
|
+
bin: int
|
|
195
|
+
x: float
|
|
196
|
+
y: int
|
|
197
|
+
group: str = ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class YieldRow(BaseModel):
|
|
201
|
+
"""One row from :meth:`MeasurementsQuery.yield_summary` or
|
|
202
|
+
:meth:`MeasurementsQuery.yield_overall`."""
|
|
203
|
+
|
|
204
|
+
part: str
|
|
205
|
+
station: str
|
|
206
|
+
phase: str
|
|
207
|
+
period: object # date from DuckDB — typed as object to accept date/str
|
|
208
|
+
total_runs: int
|
|
209
|
+
passed: int
|
|
210
|
+
failed: int
|
|
211
|
+
errored: int
|
|
212
|
+
unique_serials: int
|
|
213
|
+
first_pass_total: int
|
|
214
|
+
first_pass_passed: int
|
|
215
|
+
final_passed: int
|
|
216
|
+
avg_duration_s: float | None = None
|
|
217
|
+
p95_duration_s: float | None = None
|
|
218
|
+
min_duration_s: float | None = None
|
|
219
|
+
max_duration_s: float | None = None
|
|
220
|
+
# Quality metrics — rty from step records; dpmo from measurement records; dppm from runs.
|
|
221
|
+
# rty is None when no step records exist in the matching scope.
|
|
222
|
+
rty: float | None = None # Rolled Throughput Yield — product of per-step FPY
|
|
223
|
+
dpmo: float | None = None # Defects Per Million Opportunities (measurement-level)
|
|
224
|
+
dppm: float | None = None # Defective Parts Per Million (run-level)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class ParetoRow(BaseModel):
|
|
228
|
+
"""One row from :meth:`MeasurementsQuery.pareto`.
|
|
229
|
+
|
|
230
|
+
Represents one (part, station, step, measurement) failure bucket.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
part: str
|
|
234
|
+
station: str
|
|
235
|
+
step_name: str | None = None
|
|
236
|
+
measurement_name: str | None = None
|
|
237
|
+
total_count: int
|
|
238
|
+
fail_count: int
|
|
239
|
+
fail_rate: float | None = None
|
|
240
|
+
|
|
241
|
+
def to_bucket_dict(self) -> dict[str, object]:
|
|
242
|
+
"""Normalize to the shared failure-pareto display shape."""
|
|
243
|
+
return {
|
|
244
|
+
"bucket": f"{self.step_name or ''}: {self.measurement_name or ''}",
|
|
245
|
+
"failed_count": self.fail_count,
|
|
246
|
+
"total": self.total_count,
|
|
247
|
+
"fail_rate_pct": self.fail_rate,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class PpkRow(BaseModel):
|
|
252
|
+
"""One row from :meth:`MeasurementsQuery.ppk` — one homogeneous population:
|
|
253
|
+
one (part, station, measurement_name, characteristic_id, uut_pin) sharing a
|
|
254
|
+
single spec limit pair. Splitting on characteristic / pin / limits keeps Ppk
|
|
255
|
+
over a single distribution instead of pooling differing specs under a shared
|
|
256
|
+
name."""
|
|
257
|
+
|
|
258
|
+
part: str
|
|
259
|
+
station: str
|
|
260
|
+
measurement_name: str
|
|
261
|
+
characteristic_id: str | None = None
|
|
262
|
+
uut_pin: str | None = None
|
|
263
|
+
n: int
|
|
264
|
+
mean: float | None = None
|
|
265
|
+
sigma: float | None = None
|
|
266
|
+
lsl: float | None = None
|
|
267
|
+
usl: float | None = None
|
|
268
|
+
pp: float | None = None
|
|
269
|
+
ppk: float | None = None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TrendRow(BaseModel):
|
|
273
|
+
"""One row from :meth:`MeasurementsQuery.trend` — one (part, station, phase, period)."""
|
|
274
|
+
|
|
275
|
+
part: str
|
|
276
|
+
station: str
|
|
277
|
+
phase: str
|
|
278
|
+
period: object # date from DuckDB — typed as object to accept date/str
|
|
279
|
+
total: int
|
|
280
|
+
passed: int
|
|
281
|
+
yield_pct: float | None = None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class RetestRow(BaseModel):
|
|
285
|
+
"""One row from :meth:`MeasurementsQuery.retest` — one (part, station, phase, period)."""
|
|
286
|
+
|
|
287
|
+
part: str
|
|
288
|
+
station: str
|
|
289
|
+
phase: str
|
|
290
|
+
period: object # date from DuckDB — typed as object to accept date/str
|
|
291
|
+
total_serials: int
|
|
292
|
+
retested_count: int
|
|
293
|
+
retest_rate: float | None = None
|
|
294
|
+
avg_retries: float | None = None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TimeLossRow(BaseModel):
|
|
298
|
+
"""One row from :meth:`MeasurementsQuery.time_loss` — one (part, station, phase, period)."""
|
|
299
|
+
|
|
300
|
+
part: str
|
|
301
|
+
station: str
|
|
302
|
+
phase: str
|
|
303
|
+
period: object # date from DuckDB — typed as object to accept date/str
|
|
304
|
+
total_time_s: float | None = None
|
|
305
|
+
pass_time_s: float | None = None
|
|
306
|
+
fail_time_s: float | None = None
|
|
307
|
+
error_time_s: float | None = None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class LimitBandRow(BaseModel):
|
|
311
|
+
"""One point of a measurement's limit envelope, keyed by the chart's X.
|
|
312
|
+
|
|
313
|
+
The chart layer draws ``low`` and ``high`` as step lines against the
|
|
314
|
+
same X axis as the data — a staircase when limits are condition-indexed,
|
|
315
|
+
a flat band when they don't vary.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
x: float | str | datetime | date | None = None
|
|
319
|
+
low: float | None = None
|
|
320
|
+
high: float | None = None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class FilterSet(BaseModel):
|
|
324
|
+
"""Current filter state — URL-shareable, validated against ``MEASUREMENT_FACETS``.
|
|
325
|
+
|
|
326
|
+
Splits enum / string filters because they take different SQL
|
|
327
|
+
treatment downstream: enum filters can be passed straight through
|
|
328
|
+
(their values are the enum.value strings) while string filters
|
|
329
|
+
need cross-filtering when populating their own DISTINCT options.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
string_filters: dict[str, list[str]] = Field(default_factory=dict)
|
|
333
|
+
enum_filters: dict[str, list[str]] = Field(default_factory=dict)
|
|
334
|
+
since: date | None = None
|
|
335
|
+
until: date | None = None
|
|
336
|
+
|
|
337
|
+
@model_validator(mode="after")
|
|
338
|
+
def _validate_against_registry(self) -> FilterSet:
|
|
339
|
+
for col in self.string_filters:
|
|
340
|
+
spec = _spec_by_column(col)
|
|
341
|
+
if spec is None or spec.kind is not FacetKind.STRING:
|
|
342
|
+
raise ValueError(f"unknown or non-string facet column: {col}")
|
|
343
|
+
for col in self.enum_filters:
|
|
344
|
+
spec = _spec_by_column(col)
|
|
345
|
+
if spec is None or spec.kind is not FacetKind.ENUM:
|
|
346
|
+
raise ValueError(f"unknown or non-enum facet column: {col}")
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def is_empty(self) -> bool:
|
|
350
|
+
return (
|
|
351
|
+
not self.string_filters
|
|
352
|
+
and not self.enum_filters
|
|
353
|
+
and self.since is None
|
|
354
|
+
and self.until is None
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def to_url_params(self) -> list[tuple[str, str]]:
|
|
358
|
+
"""Encode as repeated query keys for ``urlencode([(k, v), ...])``.
|
|
359
|
+
|
|
360
|
+
Each filter's URL key is its facet's ``url_key`` (the operator-
|
|
361
|
+
facing ``param`` when set, else the column).
|
|
362
|
+
"""
|
|
363
|
+
params: list[tuple[str, str]] = []
|
|
364
|
+
for filters in (self.string_filters, self.enum_filters):
|
|
365
|
+
for col, values in filters.items():
|
|
366
|
+
spec = _spec_by_column(col)
|
|
367
|
+
key = spec.url_key if spec is not None else col
|
|
368
|
+
for v in values:
|
|
369
|
+
params.append((key, v))
|
|
370
|
+
if self.since is not None:
|
|
371
|
+
params.append(("since", self.since.isoformat()))
|
|
372
|
+
if self.until is not None:
|
|
373
|
+
params.append(("until", self.until.isoformat()))
|
|
374
|
+
return params
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def from_url_params(cls, params: dict[str, list[str]]) -> FilterSet:
|
|
378
|
+
"""Decode a query-string dict — unknown columns are dropped silently.
|
|
379
|
+
|
|
380
|
+
We drop rather than raise so a stale URL gracefully degrades
|
|
381
|
+
to a partially-empty filter rather than a 500. Callers can
|
|
382
|
+
distinguish "user typed garbage" from "schema changed under us"
|
|
383
|
+
by comparing the input keys to ``MEASUREMENT_FACETS``.
|
|
384
|
+
"""
|
|
385
|
+
string_filters: dict[str, list[str]] = {}
|
|
386
|
+
enum_filters: dict[str, list[str]] = {}
|
|
387
|
+
since: date | None = None
|
|
388
|
+
until: date | None = None
|
|
389
|
+
for key, values in params.items():
|
|
390
|
+
if key == "since" and values:
|
|
391
|
+
try:
|
|
392
|
+
since = date.fromisoformat(values[0])
|
|
393
|
+
except ValueError:
|
|
394
|
+
pass
|
|
395
|
+
continue
|
|
396
|
+
if key == "until" and values:
|
|
397
|
+
try:
|
|
398
|
+
until = date.fromisoformat(values[0])
|
|
399
|
+
except ValueError:
|
|
400
|
+
pass
|
|
401
|
+
continue
|
|
402
|
+
spec = _spec_by_url_key(key)
|
|
403
|
+
if spec is None:
|
|
404
|
+
continue
|
|
405
|
+
if spec.kind is FacetKind.STRING:
|
|
406
|
+
string_filters[spec.column] = list(values)
|
|
407
|
+
elif spec.kind is FacetKind.ENUM:
|
|
408
|
+
assert spec.enum_class is not None
|
|
409
|
+
allowed = {m.value for m in spec.enum_class.__members__.values()}
|
|
410
|
+
enum_filters[spec.column] = [v for v in values if v in allowed]
|
|
411
|
+
return cls(
|
|
412
|
+
string_filters=string_filters,
|
|
413
|
+
enum_filters=enum_filters,
|
|
414
|
+
since=since,
|
|
415
|
+
until=until,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
# The registry — single source of truth for facetable measurement columns.
|
|
421
|
+
# ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
MEASUREMENT_FACETS: list[FacetSpec] = [
|
|
424
|
+
FacetSpec(
|
|
425
|
+
column="run_outcome",
|
|
426
|
+
kind=FacetKind.ENUM,
|
|
427
|
+
enum_class=Outcome,
|
|
428
|
+
label="Run outcome",
|
|
429
|
+
description="Did the run pass overall?",
|
|
430
|
+
),
|
|
431
|
+
FacetSpec(
|
|
432
|
+
column="measurement_outcome",
|
|
433
|
+
kind=FacetKind.ENUM,
|
|
434
|
+
enum_class=Outcome,
|
|
435
|
+
label="Measurement outcome",
|
|
436
|
+
description="Did this measurement meet its limit?",
|
|
437
|
+
),
|
|
438
|
+
FacetSpec(
|
|
439
|
+
column="limit_comparator",
|
|
440
|
+
kind=FacetKind.ENUM,
|
|
441
|
+
enum_class=Comparator,
|
|
442
|
+
label="Limit comparator",
|
|
443
|
+
description="How the measurement is checked against its limits",
|
|
444
|
+
),
|
|
445
|
+
FacetSpec(
|
|
446
|
+
column="uut_part_number",
|
|
447
|
+
param="part",
|
|
448
|
+
kind=FacetKind.STRING,
|
|
449
|
+
label="Part",
|
|
450
|
+
description="Manufacturer part number of the UUT (scanned, or from the part config)",
|
|
451
|
+
),
|
|
452
|
+
FacetSpec(
|
|
453
|
+
column="station_hostname",
|
|
454
|
+
param="station",
|
|
455
|
+
kind=FacetKind.STRING,
|
|
456
|
+
label="Station",
|
|
457
|
+
description="Hostname of the station that ran the test",
|
|
458
|
+
),
|
|
459
|
+
FacetSpec(
|
|
460
|
+
column="test_phase",
|
|
461
|
+
param="phase",
|
|
462
|
+
kind=FacetKind.STRING,
|
|
463
|
+
label="Test phase",
|
|
464
|
+
description="Production / qual / development",
|
|
465
|
+
expr="COALESCE(test_phase, 'unknown')",
|
|
466
|
+
),
|
|
467
|
+
FacetSpec(
|
|
468
|
+
column="step_name",
|
|
469
|
+
kind=FacetKind.STRING,
|
|
470
|
+
label="Step",
|
|
471
|
+
description="Test step name",
|
|
472
|
+
),
|
|
473
|
+
FacetSpec(
|
|
474
|
+
column="measurement_name",
|
|
475
|
+
kind=FacetKind.STRING,
|
|
476
|
+
label="Measurement",
|
|
477
|
+
description="Named measurement (e.g. vout)",
|
|
478
|
+
),
|
|
479
|
+
FacetSpec(
|
|
480
|
+
column="uut_serial_number",
|
|
481
|
+
kind=FacetKind.STRING,
|
|
482
|
+
label="UUT serial",
|
|
483
|
+
description="Specific unit",
|
|
484
|
+
),
|
|
485
|
+
FacetSpec(
|
|
486
|
+
column="run_started_at",
|
|
487
|
+
kind=FacetKind.DATE,
|
|
488
|
+
label="Date range",
|
|
489
|
+
description="When the run started",
|
|
490
|
+
),
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
_BY_COLUMN: dict[str, FacetSpec] = {f.column: f for f in MEASUREMENT_FACETS}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _spec_by_column(column: str) -> FacetSpec | None:
|
|
498
|
+
"""Look up a facet by column name, or ``None`` if not in the registry."""
|
|
499
|
+
return _BY_COLUMN.get(column)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
_BY_URL_KEY: dict[str, FacetSpec] = {f.url_key: f for f in MEASUREMENT_FACETS}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _spec_by_url_key(key: str) -> FacetSpec | None:
|
|
506
|
+
"""Look up a facet by its URL query key (``param`` or ``column``)."""
|
|
507
|
+
return _BY_URL_KEY.get(key)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def facet_sql_expr(column: str) -> str | None:
|
|
511
|
+
"""The SQL expression a facet filters / enumerates on.
|
|
512
|
+
|
|
513
|
+
Returns the facet's ``expr`` (e.g. the operator-facing ``COALESCE``
|
|
514
|
+
for Part / Station / Test phase) when it has one, else ``None`` — in
|
|
515
|
+
which case callers quote ``column`` as a plain identifier. Lives here
|
|
516
|
+
so the column→expression mapping has one source of truth.
|
|
517
|
+
"""
|
|
518
|
+
spec = _spec_by_column(column)
|
|
519
|
+
return spec.expr if (spec is not None and spec.expr) else None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def facets_by_kind(kind: FacetKind) -> list[FacetSpec]:
|
|
523
|
+
"""Return every facet of the given kind, in registry order."""
|
|
524
|
+
return [f for f in MEASUREMENT_FACETS if f.kind is kind]
|