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,414 @@
|
|
|
1
|
+
"""Serialization registry for FileStore artifacts (build items 12 + 13).
|
|
2
|
+
|
|
3
|
+
A single source of truth for "how does this value land on disk?"
|
|
4
|
+
Both :meth:`FileStore.write` and the legacy ``save_ref_to_dir`` helper
|
|
5
|
+
use this registry so the dispatch logic lives in one place and the
|
|
6
|
+
MIME / extension convention is the same regardless of which file
|
|
7
|
+
layout the caller writes to.
|
|
8
|
+
|
|
9
|
+
Each registered handler carries:
|
|
10
|
+
|
|
11
|
+
- ``extension``: file extension including the dot (e.g. ``".npz"``)
|
|
12
|
+
- ``mime``: MIME type per the TesterKit convention table (item 13)
|
|
13
|
+
- ``write(value, dest)``: callable that writes ``value`` to ``dest``
|
|
14
|
+
|
|
15
|
+
Built-in handlers (in priority order — first match wins):
|
|
16
|
+
|
|
17
|
+
- ``Path`` → copied via :func:`shutil.copy`; extension preserved
|
|
18
|
+
from source (default ``.bin``); MIME defaults to
|
|
19
|
+
``application/octet-stream`` (the user is presumed to know the
|
|
20
|
+
payload's real MIME and can override via ``attributes``)
|
|
21
|
+
- ``Waveform`` → ``.npz`` via ``np.savez``; MIME
|
|
22
|
+
``application/x-numpy-npz``. Falls back to ``.json`` + Pydantic
|
|
23
|
+
dump when numpy isn't importable.
|
|
24
|
+
- ``bytes`` → ``.bin``; MIME ``application/octet-stream``
|
|
25
|
+
- Pydantic ``BaseModel`` (anything with ``model_dump_json``) →
|
|
26
|
+
``.json``; MIME ``application/json``
|
|
27
|
+
- numpy ``ndarray`` (anything with ``tolist`` + ``dtype``) →
|
|
28
|
+
``.npy`` via ``np.save``; MIME ``application/x-numpy-npy``.
|
|
29
|
+
JSON fallback when numpy is unavailable.
|
|
30
|
+
|
|
31
|
+
Opportunistic handlers (registered only when the library is
|
|
32
|
+
importable):
|
|
33
|
+
|
|
34
|
+
- ``PIL.Image.Image`` → ``.png``; MIME ``image/png``
|
|
35
|
+
- ``pandas.DataFrame`` → ``.parquet``; MIME
|
|
36
|
+
``application/vnd.apache.parquet``
|
|
37
|
+
|
|
38
|
+
Custom types may either:
|
|
39
|
+
|
|
40
|
+
- expose a ``testerkit_serialize(dest: Path) -> Path`` method (the
|
|
41
|
+
serializer protocol — preferred when the object knows its own
|
|
42
|
+
format), OR
|
|
43
|
+
- be registered explicitly via :func:`register_serializer`.
|
|
44
|
+
|
|
45
|
+
Pickle is the **last-resort fallback** for any value the registry
|
|
46
|
+
can't match. It emits a ``RuntimeWarning`` naming the type so
|
|
47
|
+
callers see what they should be registering a handler for; the
|
|
48
|
+
warning is what nudges the codebase toward typed serializers
|
|
49
|
+
rather than silent pickle bloat.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import importlib.util as _ilu
|
|
55
|
+
import json
|
|
56
|
+
import pickle
|
|
57
|
+
import shutil
|
|
58
|
+
import warnings
|
|
59
|
+
from collections.abc import Callable
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
from typing import Any, NamedTuple
|
|
62
|
+
|
|
63
|
+
from testerkit.data.models import Waveform, XYData
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
HAS_NUMPY = _ilu.find_spec("numpy") is not None
|
|
67
|
+
except Exception: # pragma: no cover - defensive
|
|
68
|
+
HAS_NUMPY = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Serializer(NamedTuple):
|
|
72
|
+
"""One entry in the registry: how a value type lands on disk."""
|
|
73
|
+
|
|
74
|
+
extension: str
|
|
75
|
+
mime: str
|
|
76
|
+
write: Callable[[Any, Path], None]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# The registry is a list of (predicate, serializer) pairs. Predicate is a
|
|
80
|
+
# callable so we can use ``isinstance`` checks (typical case) or arbitrary
|
|
81
|
+
# duck-typing (numpy-array detection without importing numpy at module
|
|
82
|
+
# load time). First match wins; user registrations are inserted at the
|
|
83
|
+
# front so they shadow built-ins.
|
|
84
|
+
_Registry = list[tuple[Callable[[Any], bool], Serializer]]
|
|
85
|
+
_registry: _Registry = []
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------- #
|
|
89
|
+
# Built-in writers #
|
|
90
|
+
# --------------------------------------------------------------------- #
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _write_path(value: Any, dest: Path) -> None:
|
|
94
|
+
shutil.copy(value, dest)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _write_bytes(value: Any, dest: Path) -> None:
|
|
98
|
+
dest.write_bytes(value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _write_basemodel_json(value: Any, dest: Path) -> None:
|
|
102
|
+
dest.write_text(value.model_dump_json())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _write_waveform(value: Waveform, dest: Path) -> None:
|
|
106
|
+
if HAS_NUMPY:
|
|
107
|
+
import numpy as np
|
|
108
|
+
|
|
109
|
+
# t0 is a datetime (or None). np.savez can't store datetimes
|
|
110
|
+
# directly — serialize as an ISO-8601 string when set, empty
|
|
111
|
+
# string when None. Readers must round-trip through
|
|
112
|
+
# ``datetime.fromisoformat``.
|
|
113
|
+
t0_iso = value.t0.isoformat() if value.t0 is not None else ""
|
|
114
|
+
np.savez(dest, Y=value.Y, t0=t0_iso, dt=value.dt, **value.attributes)
|
|
115
|
+
else:
|
|
116
|
+
dest.write_text(value.model_dump_json())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _write_xydata(value: XYData, dest: Path) -> None:
|
|
120
|
+
"""Item 15: pack paired x/y arrays into a single ``.npz``.
|
|
121
|
+
|
|
122
|
+
Optional unit / names land as scalar string entries in the same
|
|
123
|
+
archive so a reader (UI plot, materializer ref-deref) can
|
|
124
|
+
reconstruct axis labels without a sidecar lookup. Only set keys
|
|
125
|
+
are written — readers should treat absent keys as None.
|
|
126
|
+
"""
|
|
127
|
+
if HAS_NUMPY:
|
|
128
|
+
import numpy as np
|
|
129
|
+
|
|
130
|
+
kwargs: dict[str, Any] = {"x": value.x, "y": value.y}
|
|
131
|
+
if value.x_unit is not None:
|
|
132
|
+
kwargs["x_unit"] = value.x_unit
|
|
133
|
+
if value.y_unit is not None:
|
|
134
|
+
kwargs["y_unit"] = value.y_unit
|
|
135
|
+
if value.x_name is not None:
|
|
136
|
+
kwargs["x_name"] = value.x_name
|
|
137
|
+
if value.y_name is not None:
|
|
138
|
+
kwargs["y_name"] = value.y_name
|
|
139
|
+
np.savez(dest, **kwargs)
|
|
140
|
+
else:
|
|
141
|
+
dest.write_text(value.model_dump_json())
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _write_ndarray(value: Any, dest: Path) -> None:
|
|
145
|
+
if HAS_NUMPY:
|
|
146
|
+
import numpy as np
|
|
147
|
+
|
|
148
|
+
np.save(dest, value)
|
|
149
|
+
else:
|
|
150
|
+
dest.write_text(json.dumps(value.tolist()))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _write_pil_image(value: Any, dest: Path) -> None:
|
|
154
|
+
value.save(dest, format="PNG")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _write_pandas_dataframe(value: Any, dest: Path) -> None:
|
|
158
|
+
value.to_parquet(dest)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _write_arrow_table(value: Any, dest: Path) -> None:
|
|
162
|
+
"""Write a pyarrow Table as an Arrow IPC stream file.
|
|
163
|
+
|
|
164
|
+
Used by the materializer's channel-data preservation flow
|
|
165
|
+
(item 1d) — preserves a channel's Arrow rows verbatim under
|
|
166
|
+
FileStore.
|
|
167
|
+
"""
|
|
168
|
+
import pyarrow.ipc as ipc
|
|
169
|
+
|
|
170
|
+
writer = ipc.new_stream(dest, value.schema)
|
|
171
|
+
writer.write_table(value)
|
|
172
|
+
writer.close()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _write_pickle(value: Any, dest: Path) -> None:
|
|
176
|
+
with open(dest, "wb") as f:
|
|
177
|
+
pickle.dump(value, f)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --------------------------------------------------------------------- #
|
|
181
|
+
# Built-in registration #
|
|
182
|
+
# --------------------------------------------------------------------- #
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _waveform_ext_and_mime() -> tuple[str, str]:
|
|
186
|
+
"""Waveform extension + MIME depends on numpy availability."""
|
|
187
|
+
if HAS_NUMPY:
|
|
188
|
+
return ".npz", "application/x-numpy-npz"
|
|
189
|
+
return ".json", "application/json"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _ndarray_ext_and_mime() -> tuple[str, str]:
|
|
193
|
+
if HAS_NUMPY:
|
|
194
|
+
return ".npy", "application/x-numpy-npy"
|
|
195
|
+
return ".json", "application/json"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _is_pil_image(value: Any) -> bool:
|
|
199
|
+
# Duck-type without importing PIL at module load.
|
|
200
|
+
if not hasattr(value, "save"):
|
|
201
|
+
return False
|
|
202
|
+
mod = type(value).__module__
|
|
203
|
+
return mod.startswith("PIL.") or mod == "PIL"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _is_pandas_dataframe(value: Any) -> bool:
|
|
207
|
+
if not hasattr(value, "to_parquet"):
|
|
208
|
+
return False
|
|
209
|
+
mod = type(value).__module__
|
|
210
|
+
return mod.startswith("pandas.")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _is_ndarray(value: Any) -> bool:
|
|
214
|
+
"""True for numpy-array-like values without importing numpy."""
|
|
215
|
+
return hasattr(value, "tolist") and hasattr(value, "dtype")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_basemodel(value: Any) -> bool:
|
|
219
|
+
"""Pydantic BaseModel ducktype — any model with model_dump_json."""
|
|
220
|
+
return hasattr(value, "model_dump_json")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _is_arrow_table(value: Any) -> bool:
|
|
224
|
+
"""True for pyarrow Table-shaped values (item 1d)."""
|
|
225
|
+
# Duck-type rather than ``isinstance(value, pa.Table)`` so the
|
|
226
|
+
# check is cheap when pyarrow isn't already loaded by the call
|
|
227
|
+
# site. The materializer's preservation path always has pyarrow
|
|
228
|
+
# imported, but the registry itself shouldn't be transitively
|
|
229
|
+
# dependent.
|
|
230
|
+
mod = type(value).__module__
|
|
231
|
+
return mod.startswith("pyarrow.") and type(value).__name__ == "Table"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _register_builtins() -> None:
|
|
235
|
+
"""Register the built-in handlers in priority order.
|
|
236
|
+
|
|
237
|
+
The order matters because :func:`find_serializer` returns the
|
|
238
|
+
first match — types that are subclasses of more general types
|
|
239
|
+
must come first.
|
|
240
|
+
|
|
241
|
+
Predicate order:
|
|
242
|
+
|
|
243
|
+
1. ``Path`` — distinct from bytes; copy-with-suffix
|
|
244
|
+
2. ``Waveform`` — distinct from BaseModel (also a BaseModel
|
|
245
|
+
subclass; needs to land as .npz, not .json)
|
|
246
|
+
3. ``XYData`` — distinct from BaseModel (item 15; lands as
|
|
247
|
+
.npz paired arrays, not .json)
|
|
248
|
+
4. ``bytes`` — raw payload
|
|
249
|
+
5. pyarrow ``Table``— item 1d; used by the materializer's
|
|
250
|
+
channel-data preservation flow
|
|
251
|
+
6. PIL ``Image`` — opportunistic, before ndarray
|
|
252
|
+
(PIL.Image quacks like an array)
|
|
253
|
+
7. pandas DataFrame — opportunistic
|
|
254
|
+
8. Pydantic — any model_dump_json-capable object
|
|
255
|
+
9. numpy ``ndarray``— covers any tolist + dtype object
|
|
256
|
+
"""
|
|
257
|
+
# Type-specific dispatch lives at the bottom of each predicate.
|
|
258
|
+
waveform_ext, waveform_mime = _waveform_ext_and_mime()
|
|
259
|
+
ndarray_ext, ndarray_mime = _ndarray_ext_and_mime()
|
|
260
|
+
|
|
261
|
+
builtins: list[tuple[Callable[[Any], bool], Serializer]] = [
|
|
262
|
+
(
|
|
263
|
+
lambda v: isinstance(v, Path),
|
|
264
|
+
Serializer(extension=".bin", mime="application/octet-stream", write=_write_path),
|
|
265
|
+
),
|
|
266
|
+
(
|
|
267
|
+
lambda v: isinstance(v, Waveform),
|
|
268
|
+
Serializer(extension=waveform_ext, mime=waveform_mime, write=_write_waveform),
|
|
269
|
+
),
|
|
270
|
+
(
|
|
271
|
+
lambda v: isinstance(v, XYData),
|
|
272
|
+
# Item 15: paired x/y arrays + optional unit/name keys → .npz.
|
|
273
|
+
# MIME shares the numpy-npz convention (item 13).
|
|
274
|
+
Serializer(extension=waveform_ext, mime=waveform_mime, write=_write_xydata),
|
|
275
|
+
),
|
|
276
|
+
(
|
|
277
|
+
lambda v: isinstance(v, bytes),
|
|
278
|
+
Serializer(extension=".bin", mime="application/octet-stream", write=_write_bytes),
|
|
279
|
+
),
|
|
280
|
+
(
|
|
281
|
+
_is_arrow_table,
|
|
282
|
+
# Item 1d: pyarrow.Table → ``.arrow`` IPC stream.
|
|
283
|
+
# Used by the materializer to preserve channel data
|
|
284
|
+
# before retention pruning.
|
|
285
|
+
Serializer(
|
|
286
|
+
extension=".arrow",
|
|
287
|
+
mime="application/vnd.apache.arrow.stream",
|
|
288
|
+
write=_write_arrow_table,
|
|
289
|
+
),
|
|
290
|
+
),
|
|
291
|
+
(
|
|
292
|
+
_is_pil_image,
|
|
293
|
+
Serializer(extension=".png", mime="image/png", write=_write_pil_image),
|
|
294
|
+
),
|
|
295
|
+
(
|
|
296
|
+
_is_pandas_dataframe,
|
|
297
|
+
Serializer(
|
|
298
|
+
extension=".parquet",
|
|
299
|
+
mime="application/vnd.apache.parquet",
|
|
300
|
+
write=_write_pandas_dataframe,
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
(
|
|
304
|
+
_is_basemodel,
|
|
305
|
+
Serializer(extension=".json", mime="application/json", write=_write_basemodel_json),
|
|
306
|
+
),
|
|
307
|
+
(
|
|
308
|
+
_is_ndarray,
|
|
309
|
+
Serializer(extension=ndarray_ext, mime=ndarray_mime, write=_write_ndarray),
|
|
310
|
+
),
|
|
311
|
+
]
|
|
312
|
+
_registry.extend(builtins)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
_register_builtins()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# --------------------------------------------------------------------- #
|
|
319
|
+
# Pickle fallback #
|
|
320
|
+
# --------------------------------------------------------------------- #
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
PICKLE_FALLBACK = Serializer(
|
|
324
|
+
extension=".pkl",
|
|
325
|
+
mime="application/x-python-pickle",
|
|
326
|
+
write=_write_pickle,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# --------------------------------------------------------------------- #
|
|
331
|
+
# Public API #
|
|
332
|
+
# --------------------------------------------------------------------- #
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def register_serializer(
|
|
336
|
+
predicate: Callable[[Any], bool] | type,
|
|
337
|
+
*,
|
|
338
|
+
extension: str,
|
|
339
|
+
mime: str,
|
|
340
|
+
write: Callable[[Any, Path], None],
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Register a custom serializer.
|
|
343
|
+
|
|
344
|
+
User registrations are inserted at index 0 so they shadow the
|
|
345
|
+
built-in handlers — call this to override how a built-in type
|
|
346
|
+
serializes, or to add a new type entirely.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
predicate: Either a Python ``type`` (matched via
|
|
350
|
+
``isinstance``) or a callable ``(value) -> bool`` that
|
|
351
|
+
decides whether this serializer applies. Use the
|
|
352
|
+
callable form when the type isn't importable at module
|
|
353
|
+
load time (e.g. optional dependencies).
|
|
354
|
+
extension: File extension including the dot (e.g. ``".npz"``).
|
|
355
|
+
mime: MIME type per the TesterKit convention table (item 13).
|
|
356
|
+
write: Callable ``(value, dest)`` that writes ``value`` to
|
|
357
|
+
the destination ``Path``.
|
|
358
|
+
"""
|
|
359
|
+
if isinstance(predicate, type):
|
|
360
|
+
cls = predicate
|
|
361
|
+
match: Callable[[Any], bool] = lambda v, _cls=cls: isinstance(v, _cls) # noqa: E731
|
|
362
|
+
else:
|
|
363
|
+
match = predicate
|
|
364
|
+
_registry.insert(0, (match, Serializer(extension=extension, mime=mime, write=write)))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def find_serializer(value: Any) -> Serializer:
|
|
368
|
+
"""Return the serializer for ``value``; fall back to pickle with a warning.
|
|
369
|
+
|
|
370
|
+
Lookup order:
|
|
371
|
+
|
|
372
|
+
1. The ``testerkit_serialize`` protocol — if the value exposes
|
|
373
|
+
``testerkit_serialize(dest: Path) -> Path``, use it. The object
|
|
374
|
+
owns its file format; the extension is read from the object's
|
|
375
|
+
``testerkit_extension`` attribute (default ``".bin"``) and the
|
|
376
|
+
MIME from ``testerkit_mime`` (default ``application/octet-stream``).
|
|
377
|
+
2. The registry — first-match across user registrations + built-ins.
|
|
378
|
+
3. Pickle fallback with ``RuntimeWarning`` naming the type.
|
|
379
|
+
"""
|
|
380
|
+
if _has_testerkit_serialize_protocol(value):
|
|
381
|
+
extension = getattr(value, "testerkit_extension", ".bin")
|
|
382
|
+
mime = getattr(value, "testerkit_mime", "application/octet-stream")
|
|
383
|
+
return Serializer(extension=extension, mime=mime, write=_write_protocol)
|
|
384
|
+
|
|
385
|
+
for predicate, serializer in _registry:
|
|
386
|
+
if predicate(value):
|
|
387
|
+
return serializer
|
|
388
|
+
|
|
389
|
+
warnings.warn(
|
|
390
|
+
f"FileStore: no registered serializer for {type(value).__name__}; "
|
|
391
|
+
"falling back to pickle. Register a handler via "
|
|
392
|
+
"`from testerkit.data.files import register_serializer` so the artifact "
|
|
393
|
+
"lands as a typed file instead.",
|
|
394
|
+
RuntimeWarning,
|
|
395
|
+
stacklevel=3,
|
|
396
|
+
)
|
|
397
|
+
return PICKLE_FALLBACK
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _has_testerkit_serialize_protocol(value: Any) -> bool:
|
|
401
|
+
"""True for objects that know how to write themselves."""
|
|
402
|
+
return callable(getattr(value, "testerkit_serialize", None))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _write_protocol(value: Any, dest: Path) -> None:
|
|
406
|
+
"""Adapter for the ``testerkit_serialize`` protocol."""
|
|
407
|
+
value.testerkit_serialize(dest)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _reset_registry_for_tests() -> None:
|
|
411
|
+
"""Reset the registry to built-ins-only. Tests that register a
|
|
412
|
+
custom serializer use this in teardown to avoid leaking state."""
|
|
413
|
+
_registry.clear()
|
|
414
|
+
_register_builtins()
|