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,218 @@
|
|
|
1
|
+
"""Files catalog — warm DuckDB index over FileStore sidecar metadata.
|
|
2
|
+
|
|
3
|
+
The catalog daemon owns this index so the UI / API / materializer query
|
|
4
|
+
it instead of walking the date-partitioned tree per call (req 2). It is
|
|
5
|
+
a derived cache: the on-disk blobs and their ``.meta.json`` sidecars are
|
|
6
|
+
the durable truth, so the catalog is built in-memory and rebuilt by
|
|
7
|
+
scanning sidecars on every daemon start, then kept warm by a do_put from
|
|
8
|
+
``FileStore.write`` after each artifact lands durably.
|
|
9
|
+
|
|
10
|
+
Mirrors the channels Opt-1 model (daemon indexes producer files; live
|
|
11
|
+
writes push; restart rebuilds from disk). Per the plan, the low-level
|
|
12
|
+
``FileStore`` walk stays as a fallback until Phase E removes it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import duckdb
|
|
23
|
+
import pyarrow as pa
|
|
24
|
+
|
|
25
|
+
from testerkit.data.files.models import FileArtifactMetadata
|
|
26
|
+
from testerkit.data.schema_dispatch import SchemaVersionRefused, dispatch, report_schema_refusal
|
|
27
|
+
from testerkit.data.schema_versions import SchemaStore
|
|
28
|
+
|
|
29
|
+
_SIDECAR_SUFFIX = ".meta.json"
|
|
30
|
+
|
|
31
|
+
CATALOG_DDL = """
|
|
32
|
+
CREATE TABLE IF NOT EXISTS file_catalog (
|
|
33
|
+
uri VARCHAR PRIMARY KEY,
|
|
34
|
+
session_id VARCHAR,
|
|
35
|
+
run_id VARCHAR,
|
|
36
|
+
name VARCHAR,
|
|
37
|
+
path VARCHAR,
|
|
38
|
+
mime VARCHAR,
|
|
39
|
+
extension VARCHAR,
|
|
40
|
+
size_bytes BIGINT,
|
|
41
|
+
instrument_role VARCHAR,
|
|
42
|
+
resource VARCHAR,
|
|
43
|
+
created_at TIMESTAMPTZ,
|
|
44
|
+
attributes VARCHAR
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# ``uri`` is the natural key (``file://{date}/{session}/{name}`` is 1:1 with a
|
|
49
|
+
# blob), so the catalog is its own ingest ledger: the startup scan skips
|
|
50
|
+
# any sidecar whose uri is already present (incremental — no rebuild from
|
|
51
|
+
# every sidecar), and the live ``do_put`` from ``FileStore.write`` upserts.
|
|
52
|
+
# A file rewritten under the same uri refreshes its row instead of
|
|
53
|
+
# duplicating it (the old plain INSERT double-counted).
|
|
54
|
+
_CATALOG_COLUMNS = (
|
|
55
|
+
"uri",
|
|
56
|
+
"session_id",
|
|
57
|
+
"run_id",
|
|
58
|
+
"name",
|
|
59
|
+
"path",
|
|
60
|
+
"mime",
|
|
61
|
+
"extension",
|
|
62
|
+
"size_bytes",
|
|
63
|
+
"instrument_role",
|
|
64
|
+
"resource",
|
|
65
|
+
"created_at",
|
|
66
|
+
"attributes",
|
|
67
|
+
)
|
|
68
|
+
# Column-explicit (not ``SELECT *``) so a catalog upgraded via ALTER (which
|
|
69
|
+
# appends run_id at the end) still aligns with the source table by name.
|
|
70
|
+
_INSERT_COLS = ", ".join(_CATALOG_COLUMNS)
|
|
71
|
+
_UPSERT_SQL = (
|
|
72
|
+
f"INSERT INTO file_catalog ({_INSERT_COLS}) SELECT {_INSERT_COLS} FROM {{src}} "
|
|
73
|
+
"ON CONFLICT (uri) DO UPDATE SET "
|
|
74
|
+
+ ", ".join(f"{c}=excluded.{c}" for c in _CATALOG_COLUMNS if c != "uri")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
CATALOG_ARROW_SCHEMA = pa.schema(
|
|
78
|
+
[
|
|
79
|
+
("uri", pa.utf8()),
|
|
80
|
+
("session_id", pa.utf8()),
|
|
81
|
+
("run_id", pa.utf8()),
|
|
82
|
+
("name", pa.utf8()),
|
|
83
|
+
("path", pa.utf8()),
|
|
84
|
+
("mime", pa.utf8()),
|
|
85
|
+
("extension", pa.utf8()),
|
|
86
|
+
("size_bytes", pa.int64()),
|
|
87
|
+
("instrument_role", pa.utf8()),
|
|
88
|
+
("resource", pa.utf8()),
|
|
89
|
+
("created_at", pa.timestamp("us", tz="UTC")),
|
|
90
|
+
("attributes", pa.utf8()),
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Ephemeral live-stream frames. NOT a durable event (the committed design
|
|
96
|
+
# keeps file events lifecycle-only — FileStarted / FileEnded — to
|
|
97
|
+
# avoid flooding the EventStore at kHz/30 fps rates). Frames ride a
|
|
98
|
+
# fan-out-only Flight db so live consumers get the new bytes push-style, no
|
|
99
|
+
# poll. The frame carries the chunk PAYLOAD (like channels push the sample
|
|
100
|
+
# value) so a live consumer never has to read a still-growing object — which
|
|
101
|
+
# an object-store backend cannot serve anyway. ``payload`` is nullable:
|
|
102
|
+
# raw/jsonl fill it; format sinks (tdms/h5) leave it null and a subscriber
|
|
103
|
+
# rejoins via a library reload at the next boundary (byte-drop can't decode
|
|
104
|
+
# mid-container). ``byte_offset``/``length`` stay for ordering + at-rest seek.
|
|
105
|
+
FRAMES_DB = "file_frames"
|
|
106
|
+
|
|
107
|
+
FRAME_ARROW_SCHEMA = pa.schema(
|
|
108
|
+
[
|
|
109
|
+
("file_id", pa.utf8()),
|
|
110
|
+
("uri", pa.utf8()),
|
|
111
|
+
("byte_offset", pa.int64()),
|
|
112
|
+
("length", pa.int64()),
|
|
113
|
+
("payload", pa.binary()),
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ensure_schema(conn: duckdb.DuckDBPyConnection) -> None:
|
|
119
|
+
"""Idempotently align the on-disk catalog schema (additive open)."""
|
|
120
|
+
conn.execute(CATALOG_DDL)
|
|
121
|
+
# Additive upgrade for catalogs created before run_id existed.
|
|
122
|
+
conn.execute("ALTER TABLE file_catalog ADD COLUMN IF NOT EXISTS run_id VARCHAR")
|
|
123
|
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_file_catalog_created ON file_catalog(created_at)")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def upsert_rows(conn: duckdb.DuckDBPyConnection, table: pa.Table) -> None:
|
|
127
|
+
"""Upsert catalog rows by ``uri`` (idempotent for scan + live do_put)."""
|
|
128
|
+
if table.num_rows == 0:
|
|
129
|
+
return
|
|
130
|
+
conn.register("_upsert", table)
|
|
131
|
+
conn.execute(_UPSERT_SQL.format(src="_upsert"))
|
|
132
|
+
conn.unregister("_upsert")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def catalog_row(
|
|
136
|
+
*,
|
|
137
|
+
uri: str,
|
|
138
|
+
session_id: str,
|
|
139
|
+
name: str,
|
|
140
|
+
key: str,
|
|
141
|
+
meta: FileArtifactMetadata,
|
|
142
|
+
created_at: datetime,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Build one catalog row from sidecar metadata + backend key.
|
|
145
|
+
|
|
146
|
+
``key`` is the backend-relative physical locator
|
|
147
|
+
(``{date}/{session_id}/{filename}``) — NOT an absolute path — so the
|
|
148
|
+
catalog stays backend-agnostic and a resolve hands back a key the
|
|
149
|
+
blob backend (local or remote) can read.
|
|
150
|
+
"""
|
|
151
|
+
return {
|
|
152
|
+
"uri": uri,
|
|
153
|
+
"session_id": session_id,
|
|
154
|
+
"run_id": meta.run_id,
|
|
155
|
+
"name": name,
|
|
156
|
+
"path": key,
|
|
157
|
+
"mime": meta.mime,
|
|
158
|
+
"extension": meta.extension,
|
|
159
|
+
"size_bytes": meta.size_bytes,
|
|
160
|
+
"instrument_role": meta.instrument_role,
|
|
161
|
+
"resource": meta.resource,
|
|
162
|
+
"created_at": created_at,
|
|
163
|
+
"attributes": json.dumps(meta.attributes),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def scan_sidecars(conn: duckdb.DuckDBPyConnection, files_dir: Path) -> int:
|
|
168
|
+
"""Fold sidecars not yet cataloged into the (persistent) catalog.
|
|
169
|
+
|
|
170
|
+
Layout: ``{files_dir}/{date}/{session_id}/{filename}`` with a
|
|
171
|
+
``{filename}.meta.json`` sidecar alongside. **Incremental**: a sidecar
|
|
172
|
+
whose uri is already in the catalog is skipped, so a daemon restart
|
|
173
|
+
reads only new sidecars rather than rebuilding from every one. Returns
|
|
174
|
+
the count of newly-ingested rows.
|
|
175
|
+
"""
|
|
176
|
+
known = {row[0] for row in conn.execute("SELECT uri FROM file_catalog").fetchall()}
|
|
177
|
+
rows: list[dict[str, Any]] = []
|
|
178
|
+
for sidecar in files_dir.glob(f"*/*/*{_SIDECAR_SUFFIX}"):
|
|
179
|
+
blob = sidecar.with_name(sidecar.name[: -len(_SIDECAR_SUFFIX)])
|
|
180
|
+
if not blob.exists():
|
|
181
|
+
continue
|
|
182
|
+
session_id = blob.parent.name
|
|
183
|
+
name = blob.name
|
|
184
|
+
# URI carries the full backend key incl. date ({date}/{session_id}/{name}),
|
|
185
|
+
# matching FileStore.write so _resolve_key strips the right key on rescan.
|
|
186
|
+
uri = f"file://{blob.parent.parent.name}/{session_id}/{name}"
|
|
187
|
+
if uri in known:
|
|
188
|
+
continue
|
|
189
|
+
try:
|
|
190
|
+
# Read the raw dict first so an ABSENT ``schema_version`` is
|
|
191
|
+
# detectable — the Pydantic default would otherwise mask a pre-1.0
|
|
192
|
+
# sidecar (no key) as the current version. Dispatch, then validate.
|
|
193
|
+
raw = json.loads(sidecar.read_text())
|
|
194
|
+
adapter = dispatch(SchemaStore.FILES, raw.get("schema_version"))
|
|
195
|
+
meta = adapter(FileArtifactMetadata.model_validate(raw))
|
|
196
|
+
created_at = datetime.fromtimestamp(blob.stat().st_mtime, tz=UTC)
|
|
197
|
+
except (OSError, ValueError):
|
|
198
|
+
continue
|
|
199
|
+
except SchemaVersionRefused as exc:
|
|
200
|
+
# Surface it (permanent → warn-once; deferrable → debug), then skip
|
|
201
|
+
# WITHOUT cataloging so a deferrable (newer) sidecar is re-read next
|
|
202
|
+
# scan and a newer daemon ingests it (#43). Presence-only catalog →
|
|
203
|
+
# an absent/pre-1.0 sidecar is re-read too; the warning is deduped.
|
|
204
|
+
report_schema_refusal(exc, str(sidecar))
|
|
205
|
+
continue
|
|
206
|
+
rows.append(
|
|
207
|
+
catalog_row(
|
|
208
|
+
uri=uri,
|
|
209
|
+
session_id=session_id,
|
|
210
|
+
name=name,
|
|
211
|
+
key=f"{blob.parent.parent.name}/{session_id}/{name}",
|
|
212
|
+
meta=meta,
|
|
213
|
+
created_at=created_at,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
if rows:
|
|
217
|
+
upsert_rows(conn, pa.Table.from_pylist(rows, schema=CATALOG_ARROW_SCHEMA))
|
|
218
|
+
return len(rows)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Files catalog daemon manager + client helpers.
|
|
2
|
+
|
|
3
|
+
Subclasses ``DaemonManager`` for the files catalog DuckDB daemon and
|
|
4
|
+
provides the client-side query/push/discovery helpers consumers use to
|
|
5
|
+
reach the daemon's warm catalog (req 2) instead of walking the tree.
|
|
6
|
+
|
|
7
|
+
Blobs + ``.meta.json`` sidecars are the durable truth; the in-memory
|
|
8
|
+
catalog is rebuilt from them on every daemon start.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import pyarrow as pa
|
|
20
|
+
import pyarrow.flight as flight
|
|
21
|
+
|
|
22
|
+
from testerkit.data._daemon_lifecycle import DaemonManager, _pid_alive, wait_for_location
|
|
23
|
+
from testerkit.data._flight_query import (
|
|
24
|
+
FlightQueryClient,
|
|
25
|
+
_drop_pooled_client,
|
|
26
|
+
_get_pooled_client,
|
|
27
|
+
call_options,
|
|
28
|
+
probe_sql,
|
|
29
|
+
)
|
|
30
|
+
from testerkit.data._flight_subscribe import subscribe
|
|
31
|
+
from testerkit.data._push_relay import PushRelay
|
|
32
|
+
from testerkit.data.files.catalog import (
|
|
33
|
+
CATALOG_ARROW_SCHEMA,
|
|
34
|
+
FRAME_ARROW_SCHEMA,
|
|
35
|
+
FRAMES_DB,
|
|
36
|
+
)
|
|
37
|
+
from testerkit.models.data_options import FileOptions
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FilesCatalogManager(DaemonManager):
|
|
41
|
+
"""Manages the files catalog DuckDB daemon."""
|
|
42
|
+
|
|
43
|
+
_state_name = "_files_catalog.json"
|
|
44
|
+
_lock_name = "_files_catalog.lock"
|
|
45
|
+
_ready_name = "_files_catalog_ready"
|
|
46
|
+
_pid_name = "_files_catalog_pid"
|
|
47
|
+
_daemon_module = "testerkit.data.files._catalog_daemon"
|
|
48
|
+
_port_file = "_files_catalog_flight_port"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def acquire(files_dir: Path) -> str:
|
|
52
|
+
"""Acquire a ref to the catalog daemon, starting it if needed.
|
|
53
|
+
|
|
54
|
+
Returns the gRPC location string for Flight queries. Probes the daemon
|
|
55
|
+
after acquiring: a wedged or dead Flight thread (PID alive but not
|
|
56
|
+
responding) is killed and respawned so callers get a working connection.
|
|
57
|
+
"""
|
|
58
|
+
mgr = FilesCatalogManager(files_dir)
|
|
59
|
+
mgr.acquire()
|
|
60
|
+
location = wait_for_location(mgr, files_dir, "files")
|
|
61
|
+
if not probe_sql(location, "files"):
|
|
62
|
+
warnings.warn(
|
|
63
|
+
f"Files catalog daemon at {location} is not responding — killing and respawning.",
|
|
64
|
+
stacklevel=2,
|
|
65
|
+
)
|
|
66
|
+
mgr.force_restart()
|
|
67
|
+
mgr.acquire()
|
|
68
|
+
location = wait_for_location(mgr, files_dir, "files")
|
|
69
|
+
return location
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def release(files_dir: Path) -> None:
|
|
73
|
+
"""Release our reference to the catalog daemon."""
|
|
74
|
+
FilesCatalogManager(files_dir).release()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_running(files_dir: Path) -> bool:
|
|
78
|
+
"""Return True iff a live catalog daemon is serving ``files_dir``.
|
|
79
|
+
|
|
80
|
+
Inspection only — never spawns. Consumers use this to prefer the
|
|
81
|
+
warm catalog when a daemon is up, and fall back to the local walk
|
|
82
|
+
otherwise (so unit tests with throwaway data dirs don't spawn a
|
|
83
|
+
per-test daemon). Phase E removes the walk fallback entirely.
|
|
84
|
+
"""
|
|
85
|
+
state = files_dir / FilesCatalogManager._state_name
|
|
86
|
+
if not state.exists():
|
|
87
|
+
return False
|
|
88
|
+
try:
|
|
89
|
+
data = json.loads(state.read_text())
|
|
90
|
+
pid = data.get("pid")
|
|
91
|
+
except (json.JSONDecodeError, OSError):
|
|
92
|
+
return False
|
|
93
|
+
return isinstance(pid, int) and _pid_alive(pid)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sql_str(value: str) -> str:
|
|
97
|
+
return value.replace("'", "''")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def query_catalog(files_dir: Path, sql: str) -> list[dict[str, Any]]:
|
|
101
|
+
"""Run a catalog SQL query against the daemon (acquires + releases)."""
|
|
102
|
+
location = acquire(files_dir)
|
|
103
|
+
try:
|
|
104
|
+
client = FlightQueryClient(
|
|
105
|
+
location,
|
|
106
|
+
"files",
|
|
107
|
+
reacquire=lambda: acquire(files_dir),
|
|
108
|
+
label="FileCatalog",
|
|
109
|
+
)
|
|
110
|
+
return client.query(sql)
|
|
111
|
+
finally:
|
|
112
|
+
release(files_dir)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve_uri(files_dir: Path, uri: str) -> str | None:
|
|
116
|
+
"""Resolve a ``file://`` URI to its on-disk path via the warm catalog."""
|
|
117
|
+
rows = query_catalog(
|
|
118
|
+
files_dir,
|
|
119
|
+
f"SELECT path FROM file_catalog WHERE uri = '{_sql_str(uri)}' LIMIT 1",
|
|
120
|
+
)
|
|
121
|
+
return rows[0]["path"] if rows else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def list_recent(files_dir: Path, limit: int) -> list[dict[str, Any]]:
|
|
125
|
+
"""Return the ``limit`` most-recent catalog rows, newest first."""
|
|
126
|
+
return query_catalog(
|
|
127
|
+
files_dir,
|
|
128
|
+
f"SELECT * FROM file_catalog ORDER BY created_at DESC LIMIT {int(limit)}",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def list_artifacts(
|
|
133
|
+
files_dir: Path,
|
|
134
|
+
*,
|
|
135
|
+
uri: str | None = None,
|
|
136
|
+
session_id: str | None = None,
|
|
137
|
+
run_id: str | None = None,
|
|
138
|
+
limit: int = 50,
|
|
139
|
+
) -> list[dict[str, Any]]:
|
|
140
|
+
"""Return catalog rows newest-first, optionally filtered.
|
|
141
|
+
|
|
142
|
+
``uri`` returns the single matching artifact; ``session_id`` /
|
|
143
|
+
``run_id`` filter the listing. SQL is built here, inside the files
|
|
144
|
+
store, so callers (HTTP API, MCP tool) never touch the catalog
|
|
145
|
+
table directly.
|
|
146
|
+
"""
|
|
147
|
+
clauses = []
|
|
148
|
+
if uri:
|
|
149
|
+
clauses.append(f"uri = '{_sql_str(uri)}'")
|
|
150
|
+
if session_id:
|
|
151
|
+
clauses.append(f"session_id = '{_sql_str(session_id)}'")
|
|
152
|
+
if run_id:
|
|
153
|
+
clauses.append(f"run_id = '{_sql_str(run_id)}'")
|
|
154
|
+
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
|
155
|
+
return query_catalog(
|
|
156
|
+
files_dir,
|
|
157
|
+
f"SELECT * FROM file_catalog {where} ORDER BY created_at DESC LIMIT {int(limit)}",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def push_artifact(files_dir: Path, row: dict[str, Any]) -> None:
|
|
162
|
+
"""Push one catalog row to the daemon (best-effort, non-fatal).
|
|
163
|
+
|
|
164
|
+
Called by ``FileStore.write`` after the blob + sidecar land durably.
|
|
165
|
+
Non-fatal: the sidecar is the durable truth, so a failed push just
|
|
166
|
+
means the artifact isn't in the warm catalog until the next daemon
|
|
167
|
+
restart rescans it. Skips silently when no daemon is running, so
|
|
168
|
+
plain writes never spawn one.
|
|
169
|
+
"""
|
|
170
|
+
if not is_running(files_dir):
|
|
171
|
+
return
|
|
172
|
+
location = acquire(files_dir)
|
|
173
|
+
try:
|
|
174
|
+
client = _get_pooled_client(location)
|
|
175
|
+
tbl = pa.Table.from_pylist([row], schema=CATALOG_ARROW_SCHEMA)
|
|
176
|
+
descriptor = flight.FlightDescriptor.for_command(b"files\0file_catalog")
|
|
177
|
+
writer, reader = client.do_put(descriptor, tbl.schema, options=call_options())
|
|
178
|
+
writer.write_table(tbl)
|
|
179
|
+
# Drain the server ACK(s) before returning: each ACK confirms the daemon
|
|
180
|
+
# committed one batch, so a resolve_uri right after write() is guaranteed
|
|
181
|
+
# to see the row (read-after-write) instead of racing the insert.
|
|
182
|
+
for _ in tbl.to_batches():
|
|
183
|
+
reader.read()
|
|
184
|
+
writer.close()
|
|
185
|
+
except (OSError, RuntimeError, pa.ArrowException) as exc:
|
|
186
|
+
_drop_pooled_client(location)
|
|
187
|
+
warnings.warn(f"Files catalog push failed (non-fatal): {exc}", stacklevel=2)
|
|
188
|
+
finally:
|
|
189
|
+
release(files_dir)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def publish_frame(
|
|
193
|
+
files_dir: Path,
|
|
194
|
+
*,
|
|
195
|
+
file_id: str,
|
|
196
|
+
uri: str,
|
|
197
|
+
byte_offset: int,
|
|
198
|
+
length: int,
|
|
199
|
+
payload: bytes | None = None,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Publish one ephemeral stream-frame (best-effort).
|
|
202
|
+
|
|
203
|
+
Called by a streaming sink after each ``write``. Fans out to live
|
|
204
|
+
subscribers push-style (req 5: no poll). The frame carries the chunk
|
|
205
|
+
``payload`` so a consumer receives the new bytes directly — never
|
|
206
|
+
range-reading a still-growing object (an object-store backend can't
|
|
207
|
+
serve one). raw/jsonl pass the bytes; format sinks (tdms/h5) pass
|
|
208
|
+
``None`` and a subscriber rejoins via a library reload at the next
|
|
209
|
+
boundary. NOT persisted — the on-disk artifact is the durable record.
|
|
210
|
+
Skips silently when no daemon runs, so plain streaming never spawns one.
|
|
211
|
+
"""
|
|
212
|
+
if not is_running(files_dir):
|
|
213
|
+
return
|
|
214
|
+
location = acquire(files_dir)
|
|
215
|
+
try:
|
|
216
|
+
client = _get_pooled_client(location)
|
|
217
|
+
tbl = pa.Table.from_pylist(
|
|
218
|
+
[
|
|
219
|
+
{
|
|
220
|
+
"file_id": file_id,
|
|
221
|
+
"uri": uri,
|
|
222
|
+
"byte_offset": byte_offset,
|
|
223
|
+
"length": length,
|
|
224
|
+
"payload": payload,
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
schema=FRAME_ARROW_SCHEMA,
|
|
228
|
+
)
|
|
229
|
+
descriptor = flight.FlightDescriptor.for_command(f"{FRAMES_DB}\0frames".encode())
|
|
230
|
+
writer, _ = client.do_put(descriptor, tbl.schema, options=call_options())
|
|
231
|
+
writer.write_table(tbl)
|
|
232
|
+
writer.close()
|
|
233
|
+
except (OSError, RuntimeError, pa.ArrowException) as exc:
|
|
234
|
+
_drop_pooled_client(location)
|
|
235
|
+
warnings.warn(f"Files frame publish failed (non-fatal): {exc}", stacklevel=2)
|
|
236
|
+
finally:
|
|
237
|
+
release(files_dir)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class _FrameTransport:
|
|
241
|
+
"""The files-frame codec + held transport behind a :class:`PushRelay`.
|
|
242
|
+
|
|
243
|
+
Coalesces a drained burst of frame dicts into one ``RecordBatch`` and
|
|
244
|
+
``do_put``s it to the catalog daemon's hook-only frames db on a pooled,
|
|
245
|
+
held client. On error it drops + reacquires the pooled client; the on-disk
|
|
246
|
+
byte stream is the durable record, so a failed push is non-fatal.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
_DESCRIPTOR = flight.FlightDescriptor.for_command(f"{FRAMES_DB}\0frames".encode())
|
|
250
|
+
|
|
251
|
+
def __init__(self, location: str) -> None:
|
|
252
|
+
self._location = location
|
|
253
|
+
self._client = _get_pooled_client(location)
|
|
254
|
+
|
|
255
|
+
def flush(self, _key: object, rows: list[dict[str, Any]]) -> None:
|
|
256
|
+
try:
|
|
257
|
+
tbl = pa.Table.from_pylist(rows, schema=FRAME_ARROW_SCHEMA)
|
|
258
|
+
writer, _ = self._client.do_put(self._DESCRIPTOR, tbl.schema, options=call_options())
|
|
259
|
+
writer.write_table(tbl)
|
|
260
|
+
writer.close()
|
|
261
|
+
except (OSError, RuntimeError, pa.ArrowException) as exc:
|
|
262
|
+
_drop_pooled_client(self._location)
|
|
263
|
+
self._client = _get_pooled_client(self._location)
|
|
264
|
+
warnings.warn(f"Files frame relay do_put failed (non-fatal): {exc}", stacklevel=2)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def open_frame_relay(files_dir: Path, options: FileOptions | None = None) -> PushRelay | None:
|
|
268
|
+
"""Start a non-blocking frame relay iff a catalog daemon is already serving.
|
|
269
|
+
|
|
270
|
+
Resolved ONCE per stream (not per chunk). Returns ``None`` when no daemon
|
|
271
|
+
runs — the common no-subscriber / benchmark case — so the writer's hot path
|
|
272
|
+
skips all frame work. Never spawns a daemon. The shared :class:`PushRelay`
|
|
273
|
+
owns the queue + drain + drop-oldest overflow; :class:`_FrameTransport`
|
|
274
|
+
supplies the frame codec + held ``do_put``. Coalescing tuning comes from
|
|
275
|
+
``options`` (testerkit.yaml ``files:``).
|
|
276
|
+
"""
|
|
277
|
+
if not is_running(files_dir):
|
|
278
|
+
return None
|
|
279
|
+
opts = options or FileOptions()
|
|
280
|
+
transport = _FrameTransport(acquire(files_dir))
|
|
281
|
+
return PushRelay(
|
|
282
|
+
flush=transport.flush,
|
|
283
|
+
max_weight=opts.frame_push_max_rows,
|
|
284
|
+
max_wait=opts.frame_push_max_wait,
|
|
285
|
+
queue_max=opts.frame_push_queue_max,
|
|
286
|
+
thread_name="files-frame-relay",
|
|
287
|
+
on_close=lambda: release(files_dir),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def subscribe_frames(
|
|
292
|
+
files_dir: Path,
|
|
293
|
+
callback: Callable[[dict[str, Any]], None],
|
|
294
|
+
) -> Callable[[], None]:
|
|
295
|
+
"""Subscribe to live stream-frame notifications. Returns an unsub callable.
|
|
296
|
+
|
|
297
|
+
Spawns a reader thread that calls ``callback`` with each frame dict
|
|
298
|
+
(``file_id``/``uri``/``byte_offset``/``length``). Holds a daemon
|
|
299
|
+
ref for the subscription's lifetime; ``unsub`` closes the stream and
|
|
300
|
+
releases it. Uses a dedicated client so closing it cleanly interrupts
|
|
301
|
+
the held-open ``do_get``.
|
|
302
|
+
"""
|
|
303
|
+
location = acquire(files_dir)
|
|
304
|
+
client = flight.connect(location)
|
|
305
|
+
|
|
306
|
+
def _on_batch(batch: pa.RecordBatch) -> None:
|
|
307
|
+
for i in range(batch.num_rows):
|
|
308
|
+
callback({c: batch.column(c)[i].as_py() for c in batch.schema.names})
|
|
309
|
+
|
|
310
|
+
def _close() -> None:
|
|
311
|
+
try:
|
|
312
|
+
client.close()
|
|
313
|
+
except (OSError, RuntimeError, pa.ArrowException):
|
|
314
|
+
pass
|
|
315
|
+
release(files_dir)
|
|
316
|
+
|
|
317
|
+
unsub, _thread = subscribe(
|
|
318
|
+
client,
|
|
319
|
+
flight.Ticket(f"{FRAMES_DB}\0__SUBSCRIBE__".encode()),
|
|
320
|
+
_on_batch,
|
|
321
|
+
name="files-frame-sub",
|
|
322
|
+
swallow_errors=True,
|
|
323
|
+
on_close=_close,
|
|
324
|
+
)
|
|
325
|
+
return unsub
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Models for FileStore artifact metadata (build item 1c)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from testerkit.data.schema_versions import CURRENT_SCHEMA_VERSION, SchemaStore
|
|
10
|
+
|
|
11
|
+
# FileStore sidecar stamp. The sidecar ({filename}.meta.json) is a published,
|
|
12
|
+
# directly-readable consumer surface — version it so readers can detect format
|
|
13
|
+
# changes. Sourced from the central registry (one home); see
|
|
14
|
+
# ``testerkit.data.schema_versions``.
|
|
15
|
+
FILE_METADATA_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION[SchemaStore.FILES]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileArtifactMetadata(BaseModel):
|
|
19
|
+
"""Metadata persisted next to a FileStore artifact as a sidecar.
|
|
20
|
+
|
|
21
|
+
Written at put time alongside the artifact at
|
|
22
|
+
``{filename}.meta.json``. Captures the kind of file that landed,
|
|
23
|
+
its size on disk, and any user-supplied attributes routed through
|
|
24
|
+
:meth:`FileStore.write`'s ``attributes`` kwarg.
|
|
25
|
+
|
|
26
|
+
Per build item 13, the ``mime`` value follows the TesterKit
|
|
27
|
+
convention table — see :mod:`testerkit.data.files.serializers`.
|
|
28
|
+
|
|
29
|
+
Per build item 17, the metadata-bag field is called
|
|
30
|
+
``attributes`` (matching :class:`ChannelDescriptor.attributes`
|
|
31
|
+
and :class:`Waveform.attributes`).
|
|
32
|
+
|
|
33
|
+
Format-specific extraction (image dimensions, audio duration,
|
|
34
|
+
video duration) is not in the initial cut — that lands when the
|
|
35
|
+
relevant format library exposes the data inexpensively. Today
|
|
36
|
+
callers carry whatever they want via the ``attributes`` kwarg.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
mime: str
|
|
40
|
+
extension: str
|
|
41
|
+
size_bytes: int
|
|
42
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
43
|
+
# Optional provenance — populated when a Waveform / channel-shaped
|
|
44
|
+
# value falls through to FileStore (no ChannelStore wired). Without
|
|
45
|
+
# these, the FileStore fallback path silently loses the instrument
|
|
46
|
+
# context that the ChannelStore descriptor would have captured
|
|
47
|
+
# (first-write provenance). Consumers joining back to the run
|
|
48
|
+
# record can use these to reconstruct the same context that the
|
|
49
|
+
# ChannelStore path would have provided.
|
|
50
|
+
instrument_role: str = ""
|
|
51
|
+
resource: str = ""
|
|
52
|
+
# The run that produced this artifact (when written inside a run), so the
|
|
53
|
+
# catalog can be filtered by run and the UI can link back. Persisted in the
|
|
54
|
+
# sidecar so a daemon-restart rescan recovers it. ``None`` for run-less writes.
|
|
55
|
+
run_id: str | None = None
|
|
56
|
+
# Schema version stamp — included in every sidecar so readers can detect
|
|
57
|
+
# format changes without scanning field presence. Defaults to the current
|
|
58
|
+
# version so old sidecars (missing this field) still validate: Pydantic
|
|
59
|
+
# fills in the default, giving backward-tolerant reads.
|
|
60
|
+
schema_version: str = Field(default=FILE_METADATA_SCHEMA_VERSION)
|