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,463 @@
|
|
|
1
|
+
"""FileStore — durable file artifact storage, session-scoped.
|
|
2
|
+
|
|
3
|
+
Holds non-numeric blobs: images, video, vendor files (.tdms), NPZ
|
|
4
|
+
waveforms, JSON exports, anything that fits the file-shaped pattern
|
|
5
|
+
(one artifact, one URI). Companion to ChannelStore for typed numeric
|
|
6
|
+
streaming; not for ChannelStore-shaped data (rejected by ChannelStore
|
|
7
|
+
via ``classify_value``).
|
|
8
|
+
|
|
9
|
+
URI format: ``file://{date}/{session_id}/{filename}``
|
|
10
|
+
On disk: ``{data_dir}/files/{date}/{session_id}/{filename}``
|
|
11
|
+
Sidecar: ``{filename}.meta.json`` (item 1c — MIME + size +
|
|
12
|
+
user attributes)
|
|
13
|
+
|
|
14
|
+
Filename convention:
|
|
15
|
+
- with ``vector_id``: ``{vector_id_short}_{name}.{ext}``
|
|
16
|
+
- without: ``{name}.{ext}``
|
|
17
|
+
- on collision: ``_2``, ``_3``, … suffix (preserves claim-check
|
|
18
|
+
immutability — repeated puts create distinct files; the vector's
|
|
19
|
+
``out_<name>`` last-write-wins separately at materialization)
|
|
20
|
+
|
|
21
|
+
Type dispatch + MIME convention live in
|
|
22
|
+
:mod:`testerkit.data.files.serializers` (build items 12 + 13). Custom
|
|
23
|
+
types either expose ``testerkit_serialize(dest)`` (the protocol) or
|
|
24
|
+
register via :func:`register_serializer`.
|
|
25
|
+
|
|
26
|
+
Streaming sink lands in 1b. Migration of the two legacy ``_ref``
|
|
27
|
+
dirs lands in 1d.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import TYPE_CHECKING, Any
|
|
35
|
+
from uuid import UUID
|
|
36
|
+
|
|
37
|
+
from testerkit.data.data_dir import resolve_data_dir
|
|
38
|
+
from testerkit.data.files._backend import BlobBackend, resolve_files_backend
|
|
39
|
+
from testerkit.data.files.catalog import catalog_row
|
|
40
|
+
from testerkit.data.files.catalog_manager import (
|
|
41
|
+
push_artifact as _catalog_push,
|
|
42
|
+
)
|
|
43
|
+
from testerkit.data.files.models import FileArtifactMetadata
|
|
44
|
+
from testerkit.data.files.serializers import find_serializer
|
|
45
|
+
from testerkit.data.files.streaming import StreamingSink, _BaseSink, get_format
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from testerkit.data.event_log import EventLog
|
|
49
|
+
|
|
50
|
+
_SIDECAR_SUFFIX = ".meta.json"
|
|
51
|
+
|
|
52
|
+
# Truncate vector_id to N chars for filename prefix (audit trail
|
|
53
|
+
# without bloating). Matches existing ``VECTOR_ID_LENGTH`` convention
|
|
54
|
+
# in ``data/backends/_row_helpers.py``.
|
|
55
|
+
_VECTOR_ID_LENGTH = 8
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FileStore:
|
|
59
|
+
"""Durable file artifact storage, session-scoped.
|
|
60
|
+
|
|
61
|
+
Typical use is indirect — ``observe()`` / ``verify()`` route
|
|
62
|
+
non-channel-shaped values here automatically. Direct use is for
|
|
63
|
+
power-user cases (the streaming sink lands in build item 1b; for
|
|
64
|
+
one-shot puts, callers can go through this class today).
|
|
65
|
+
|
|
66
|
+
Holds non-numeric blobs only — not channel-shaped numerics (those
|
|
67
|
+
go to ChannelStore). The dispatch decision is the caller's; this
|
|
68
|
+
store does not type-check the incoming value beyond what its
|
|
69
|
+
serializer can handle.
|
|
70
|
+
|
|
71
|
+
Per build item 1c, every put writes a sidecar
|
|
72
|
+
``{filename}.meta.json`` next to the artifact carrying
|
|
73
|
+
:class:`FileArtifactMetadata` (mime / extension / size_bytes /
|
|
74
|
+
user attributes). Read it back with :meth:`read_attributes`.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self, *, _data_dir: Path | str | None = None, backend_uri: str | None = None
|
|
79
|
+
) -> None:
|
|
80
|
+
self._data_dir = resolve_data_dir(_data_dir)
|
|
81
|
+
self._files_dir = self._data_dir / "files"
|
|
82
|
+
# Blob bytes flow through this backend (local disk by default; an
|
|
83
|
+
# object store by config — the only thing that changes for a swap).
|
|
84
|
+
# Default root is self._files_dir so it tracks whatever resolved the
|
|
85
|
+
# data dir (incl. test monkeypatches); env/config override only when
|
|
86
|
+
# no explicit _data_dir was given.
|
|
87
|
+
self._backend = BlobBackend.from_uri(
|
|
88
|
+
resolve_files_backend(
|
|
89
|
+
self._files_dir, allow_override=_data_dir is None, backend_uri=backend_uri
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def write(
|
|
94
|
+
self,
|
|
95
|
+
name: str,
|
|
96
|
+
value: Any,
|
|
97
|
+
*,
|
|
98
|
+
session_id: str,
|
|
99
|
+
vector_id: str | None = None,
|
|
100
|
+
attributes: dict[str, Any] | None = None,
|
|
101
|
+
instrument_role: str = "",
|
|
102
|
+
resource: str = "",
|
|
103
|
+
run_id: UUID | None = None,
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Write ``value`` to FileStore; return its ``file://`` URI.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name: Logical name for the artifact (e.g.
|
|
109
|
+
``"scope.ch1.capture"``). Forms the bulk of the
|
|
110
|
+
filename. Should not include extension; extension is
|
|
111
|
+
chosen by the registry (build item 12).
|
|
112
|
+
value: The value to write. Routed through
|
|
113
|
+
:func:`testerkit.data.files.serializers.find_serializer`
|
|
114
|
+
— see that module for the built-in convention table
|
|
115
|
+
and the ``testerkit_serialize`` protocol /
|
|
116
|
+
:func:`register_serializer` extension points.
|
|
117
|
+
Last-resort fallback is pickle with a
|
|
118
|
+
``RuntimeWarning`` naming the type.
|
|
119
|
+
session_id: Session this artifact belongs to. Required.
|
|
120
|
+
Callers (verb layer) pull from the active-session
|
|
121
|
+
ContextVar.
|
|
122
|
+
vector_id: Optional vector context. When provided, the
|
|
123
|
+
first 8 chars prefix the filename for audit
|
|
124
|
+
(matches existing convention).
|
|
125
|
+
attributes: User-supplied metadata bag persisted into the
|
|
126
|
+
sidecar (item 1c). ``None`` writes an empty
|
|
127
|
+
attributes dict. The bag is round-trippable via
|
|
128
|
+
:meth:`read_attributes`.
|
|
129
|
+
instrument_role: Optional provenance — the station-config
|
|
130
|
+
instrument role (e.g. ``"scope"``, ``"psu"``) that
|
|
131
|
+
produced this artifact. Populated when a Waveform /
|
|
132
|
+
channel-shaped value falls through to FileStore from
|
|
133
|
+
``Context.observe`` because no ChannelStore was wired
|
|
134
|
+
(bare-Context bringup tests). Without this the FileStore
|
|
135
|
+
fallback path would silently lose the provenance the
|
|
136
|
+
ChannelStore descriptor would have captured.
|
|
137
|
+
resource: Optional provenance — VISA / network resource
|
|
138
|
+
string for the instrument (paired with
|
|
139
|
+
``instrument_role``). Same population path as above.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
URI of the form ``file://{date}/{session_id}/{filename}``.
|
|
143
|
+
The filename reflects the actual on-disk name (including
|
|
144
|
+
any collision-avoidance ``_2`` / ``_3`` suffix).
|
|
145
|
+
"""
|
|
146
|
+
serializer = find_serializer(value)
|
|
147
|
+
# Path values: the source file's suffix wins over the
|
|
148
|
+
# serializer's default ``.bin`` so e.g. ``capture.tdms`` stays
|
|
149
|
+
# ``.tdms`` on disk.
|
|
150
|
+
if isinstance(value, Path):
|
|
151
|
+
ext = value.suffix or serializer.extension
|
|
152
|
+
else:
|
|
153
|
+
ext = serializer.extension
|
|
154
|
+
prefix = f"{vector_id[:_VECTOR_ID_LENGTH]}_" if vector_id else ""
|
|
155
|
+
|
|
156
|
+
# Compute session directory (date-partitioned for retention/ops)
|
|
157
|
+
session_dir = self._session_dir(session_id)
|
|
158
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
# Resolve a unique filename within the session dir; the backend key
|
|
161
|
+
# is that filename under {date}/{session_id}.
|
|
162
|
+
filename = self._unique_filename(session_dir, f"{prefix}{name}", ext)
|
|
163
|
+
date = session_dir.parent.name
|
|
164
|
+
key = f"{date}/{session_id}/{filename}"
|
|
165
|
+
|
|
166
|
+
# Serialize to a local staging path (suffix preserved — writers like
|
|
167
|
+
# np.savez rename themselves otherwise), then publish atomically to
|
|
168
|
+
# the blob backend: a same-fs rename locally, an upload remotely. A
|
|
169
|
+
# crash mid-serialize leaves the staging temp, never a published
|
|
170
|
+
# artifact the catalog could point at.
|
|
171
|
+
staged = self._backend.stage_path(key)
|
|
172
|
+
serializer.write(value, staged)
|
|
173
|
+
self._backend.publish_atomic(staged, key)
|
|
174
|
+
|
|
175
|
+
# Item 1c: write the sidecar metadata. size_bytes is read back from
|
|
176
|
+
# the backend after publish so it reflects the actual stored size.
|
|
177
|
+
# The sidecar pairs the artifact one-to-one (mime, size, attributes);
|
|
178
|
+
# backend.write_bytes is atomic (temp+rename local, single PUT remote)
|
|
179
|
+
# so it either lands fully or not at all.
|
|
180
|
+
metadata = FileArtifactMetadata(
|
|
181
|
+
mime=serializer.mime,
|
|
182
|
+
extension=ext,
|
|
183
|
+
size_bytes=self._backend.size(key) or 0,
|
|
184
|
+
attributes=dict(attributes or {}),
|
|
185
|
+
instrument_role=instrument_role,
|
|
186
|
+
resource=resource,
|
|
187
|
+
run_id=str(run_id) if run_id else None,
|
|
188
|
+
)
|
|
189
|
+
self._backend.write_bytes(f"{key}{_SIDECAR_SUFFIX}", metadata.model_dump_json().encode())
|
|
190
|
+
|
|
191
|
+
uri = f"file://{key}"
|
|
192
|
+
# Keep the daemon's warm catalog current (req 2). Best-effort and
|
|
193
|
+
# non-spawning: skips silently if no daemon is running, and the
|
|
194
|
+
# sidecar is the durable truth a restart rebuilds from. The catalog
|
|
195
|
+
# stores the backend ``key`` (not an absolute path) so resolution
|
|
196
|
+
# stays backend-agnostic.
|
|
197
|
+
_catalog_push(
|
|
198
|
+
self._files_dir,
|
|
199
|
+
catalog_row(
|
|
200
|
+
uri=uri,
|
|
201
|
+
session_id=session_id,
|
|
202
|
+
name=filename,
|
|
203
|
+
key=key,
|
|
204
|
+
meta=metadata,
|
|
205
|
+
created_at=datetime.now(UTC),
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
return uri
|
|
209
|
+
|
|
210
|
+
def open_stream(
|
|
211
|
+
self,
|
|
212
|
+
name: str,
|
|
213
|
+
*,
|
|
214
|
+
format: str,
|
|
215
|
+
session_id: str,
|
|
216
|
+
vector_id: str | None = None,
|
|
217
|
+
attributes: dict[str, Any] | None = None,
|
|
218
|
+
event_log: EventLog | None = None,
|
|
219
|
+
run_id: UUID | None = None,
|
|
220
|
+
checkpoint_cadence: float | None = None,
|
|
221
|
+
) -> StreamingSink:
|
|
222
|
+
"""Open a streaming sink — one file, written incrementally.
|
|
223
|
+
|
|
224
|
+
Build item 2 (C5). Companion to :meth:`write` for cases where
|
|
225
|
+
bytes arrive over time rather than all-at-once (continuous DAQ,
|
|
226
|
+
video capture, line-delimited logs). The sink:
|
|
227
|
+
|
|
228
|
+
- allocates the artifact's backend key (collision-safe via the
|
|
229
|
+
same ``_unique_filename`` scheme as :meth:`write`)
|
|
230
|
+
- emits :class:`FileStarted` on open
|
|
231
|
+
- pushes the new bytes as an ephemeral frame after every
|
|
232
|
+
:meth:`write` (via the files daemon, not the event log) so
|
|
233
|
+
live consumers receive them push-style, no poll
|
|
234
|
+
- emits :class:`FileEnded` on :meth:`close` (carries final
|
|
235
|
+
``file://`` URI + total size)
|
|
236
|
+
- writes the item-1c sidecar metadata + catalog row on close
|
|
237
|
+
|
|
238
|
+
Live consumers subscribe to the stream's frames and receive each
|
|
239
|
+
new chunk push-style; the final URI arrives in
|
|
240
|
+
:class:`FileEnded`. See the
|
|
241
|
+
:mod:`testerkit.data.files.streaming` module docstring for format
|
|
242
|
+
coverage + caveats per format on partial-decode-during-write.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
name: Artifact name (becomes part of the filename).
|
|
246
|
+
format: One of the registered streaming formats —
|
|
247
|
+
``"raw"``, ``"jsonl"``, ``"tdms"``, ``"h5"`` in
|
|
248
|
+
v0.2.0. See :func:`testerkit.data.files.streaming.registered_formats`.
|
|
249
|
+
session_id: Session this artifact belongs to. Required.
|
|
250
|
+
vector_id: Optional vector context; first 8 chars prefix
|
|
251
|
+
the filename for audit.
|
|
252
|
+
attributes: User-supplied metadata bag — persisted into
|
|
253
|
+
the item-1c sidecar at close.
|
|
254
|
+
event_log: Event log to emit Stream* events into. ``None``
|
|
255
|
+
is allowed (silent writes — useful for tests of the
|
|
256
|
+
file path in isolation); production paths always plumb
|
|
257
|
+
this from the active session.
|
|
258
|
+
run_id: Optional run UUID stamped on Stream* events.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
A :class:`StreamingSink` — context-manageable; call
|
|
262
|
+
:meth:`StreamingSink.close` (or use a ``with`` block) to
|
|
263
|
+
finalize and receive the ``file://`` URI.
|
|
264
|
+
"""
|
|
265
|
+
fmt = get_format(format)
|
|
266
|
+
prefix = f"{vector_id[:_VECTOR_ID_LENGTH]}_" if vector_id else ""
|
|
267
|
+
session_dir = self._session_dir(session_id)
|
|
268
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
filename = self._unique_filename(session_dir, f"{prefix}{name}", fmt.extension)
|
|
270
|
+
date = session_dir.parent.name
|
|
271
|
+
key = f"{date}/{session_id}/{filename}"
|
|
272
|
+
uri = f"file://{key}"
|
|
273
|
+
|
|
274
|
+
# Capture for sidecar write at close
|
|
275
|
+
attrs_for_sidecar = dict(attributes or {})
|
|
276
|
+
|
|
277
|
+
def _finalize(staged: Path | None) -> None:
|
|
278
|
+
# Format sinks (tdms/h5) staged to a local path → publish it now;
|
|
279
|
+
# raw/jsonl already published by closing the backend output stream.
|
|
280
|
+
# Then the sidecar + catalog row — the same durable tail write() runs.
|
|
281
|
+
if staged is not None:
|
|
282
|
+
self._backend.publish_atomic(staged, key)
|
|
283
|
+
metadata = FileArtifactMetadata(
|
|
284
|
+
mime=fmt.mime,
|
|
285
|
+
extension=fmt.extension,
|
|
286
|
+
size_bytes=self._backend.size(key) or 0,
|
|
287
|
+
attributes=attrs_for_sidecar,
|
|
288
|
+
run_id=str(run_id) if run_id else None,
|
|
289
|
+
)
|
|
290
|
+
self._backend.write_bytes(
|
|
291
|
+
f"{key}{_SIDECAR_SUFFIX}", metadata.model_dump_json().encode()
|
|
292
|
+
)
|
|
293
|
+
_catalog_push(
|
|
294
|
+
self._files_dir,
|
|
295
|
+
catalog_row(
|
|
296
|
+
uri=uri,
|
|
297
|
+
session_id=session_id,
|
|
298
|
+
name=filename,
|
|
299
|
+
key=key,
|
|
300
|
+
meta=metadata,
|
|
301
|
+
created_at=datetime.now(UTC),
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
common: dict[str, Any] = {
|
|
306
|
+
"uri": uri,
|
|
307
|
+
"files_dir": self._files_dir,
|
|
308
|
+
"name": name,
|
|
309
|
+
"format_name": format,
|
|
310
|
+
"session_id": session_id,
|
|
311
|
+
"event_log": event_log,
|
|
312
|
+
"run_id": run_id,
|
|
313
|
+
}
|
|
314
|
+
if fmt.needs_local_path:
|
|
315
|
+
# nptdms / h5py need a seekable local file; stage there, publish on close.
|
|
316
|
+
staged = self._backend.stage_path(key)
|
|
317
|
+
sink = fmt.open(path=staged, finalizer=lambda: _finalize(staged), **common)
|
|
318
|
+
else:
|
|
319
|
+
# raw/jsonl write straight to the backend output stream (local file /
|
|
320
|
+
# S3 multipart) — completes on close, so no separate publish step.
|
|
321
|
+
stream = self._backend.open_output_stream(key)
|
|
322
|
+
sink = fmt.open(stream=stream, finalizer=lambda: _finalize(None), **common)
|
|
323
|
+
# Stream liveness cadence is set post-construction so custom formats
|
|
324
|
+
# (any StreamingSink) need not accept it; only _BaseSink subclasses
|
|
325
|
+
# carry the checkpoint machinery.
|
|
326
|
+
if checkpoint_cadence is not None and isinstance(sink, _BaseSink):
|
|
327
|
+
sink._checkpoint_cadence = checkpoint_cadence
|
|
328
|
+
return sink
|
|
329
|
+
|
|
330
|
+
def read(self, uri: str) -> bytes | None:
|
|
331
|
+
"""Return the full bytes of the artifact at ``uri``, or ``None``.
|
|
332
|
+
|
|
333
|
+
The store serves the file — callers never touch the filesystem
|
|
334
|
+
or know where the bytes live. Goes through the blob backend, so
|
|
335
|
+
a remote (S3/GCS) backend serves the same way as local disk.
|
|
336
|
+
"""
|
|
337
|
+
key = self._resolve_key(uri)
|
|
338
|
+
return None if key is None else self._backend.read_bytes(key)
|
|
339
|
+
|
|
340
|
+
def read_range(self, uri: str, *, offset: int = 0, length: int | None = None) -> bytes | None:
|
|
341
|
+
"""Range-read ``[offset, offset+length)`` of ``uri`` (HTTP Range)."""
|
|
342
|
+
key = self._resolve_key(uri)
|
|
343
|
+
return None if key is None else self._backend.read_range(key, offset=offset, length=length)
|
|
344
|
+
|
|
345
|
+
def size(self, uri: str) -> int | None:
|
|
346
|
+
"""Stored byte size of the artifact at ``uri``, or ``None``."""
|
|
347
|
+
key = self._resolve_key(uri)
|
|
348
|
+
return None if key is None else self._backend.size(key)
|
|
349
|
+
|
|
350
|
+
def modified_at(self, uri: str) -> datetime | None:
|
|
351
|
+
"""Last-modified time of the artifact at ``uri``, or ``None``.
|
|
352
|
+
|
|
353
|
+
Backend-portable (local mtime / object-store LastModified). For an
|
|
354
|
+
immutable write-once artifact this is effectively its creation time.
|
|
355
|
+
"""
|
|
356
|
+
key = self._resolve_key(uri)
|
|
357
|
+
return None if key is None else self._backend.modified_at(key)
|
|
358
|
+
|
|
359
|
+
def open_input(self, uri: str) -> Any:
|
|
360
|
+
"""Open a streaming read handle for ``uri`` (caller closes it), or ``None``.
|
|
361
|
+
|
|
362
|
+
Returns a random-access ``pyarrow.NativeFile`` so a consumer can stream
|
|
363
|
+
a large artifact in chunks rather than buffering it whole — sequential
|
|
364
|
+
reads become ranged GETs on a remote backend.
|
|
365
|
+
"""
|
|
366
|
+
key = self._resolve_key(uri)
|
|
367
|
+
return None if key is None else self._backend.open_input(key)
|
|
368
|
+
|
|
369
|
+
def read_attributes(self, uri: str) -> FileArtifactMetadata | None:
|
|
370
|
+
"""Return the :class:`FileArtifactMetadata` for ``uri``, or None.
|
|
371
|
+
|
|
372
|
+
``None`` when the URI doesn't resolve to a FileStore artifact, or
|
|
373
|
+
when its sidecar is missing (e.g. an artifact put before item 1c
|
|
374
|
+
landed). The sidecar is read through the backend like any blob.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
uri: A ``file://{date}/{session_id}/{filename}`` URI returned
|
|
378
|
+
by :meth:`put`.
|
|
379
|
+
"""
|
|
380
|
+
key = self._resolve_key(uri)
|
|
381
|
+
if key is None:
|
|
382
|
+
return None
|
|
383
|
+
raw = self._backend.read_bytes(f"{key}{_SIDECAR_SUFFIX}")
|
|
384
|
+
if raw is None:
|
|
385
|
+
return None
|
|
386
|
+
return FileArtifactMetadata.model_validate_json(raw)
|
|
387
|
+
|
|
388
|
+
def delete(self, uri: str) -> None:
|
|
389
|
+
"""Delete the artifact at ``uri`` and its sidecar (best-effort).
|
|
390
|
+
|
|
391
|
+
A no-op when the URI doesn't resolve. Removes the blob bytes through
|
|
392
|
+
the backend; the catalog row, if any, clears when the daemon next
|
|
393
|
+
rescans (the sidecar — its durable source — is gone).
|
|
394
|
+
"""
|
|
395
|
+
key = self._resolve_key(uri)
|
|
396
|
+
if key is None:
|
|
397
|
+
return
|
|
398
|
+
self._backend.delete(key)
|
|
399
|
+
self._backend.delete(f"{key}{_SIDECAR_SUFFIX}")
|
|
400
|
+
|
|
401
|
+
def close(self) -> None:
|
|
402
|
+
"""No-op — FileStore holds no daemon ref or background thread.
|
|
403
|
+
|
|
404
|
+
Present so FileStore satisfies the optional-close store contract
|
|
405
|
+
(usable as ``with FileStore(...) as fs:``) like the other stores.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __enter__(self) -> FileStore:
|
|
409
|
+
return self
|
|
410
|
+
|
|
411
|
+
def __exit__(self, *_: object) -> None:
|
|
412
|
+
self.close()
|
|
413
|
+
|
|
414
|
+
def _resolve_key(self, uri: str) -> str | None:
|
|
415
|
+
"""Map a ``file://`` URI to its backend-relative key — pure parsing.
|
|
416
|
+
|
|
417
|
+
The URI carries the full key (``{date}/{session_id}/{filename}``), so
|
|
418
|
+
resolution is a string strip: no catalog lookup, no disk scan, no
|
|
419
|
+
daemon. The key is identical across backends; the backend root is
|
|
420
|
+
config, so a local→remote swap is transparent and a point read needs
|
|
421
|
+
no index (the catalog daemon serves queries/discovery, not point
|
|
422
|
+
resolution). ``None`` for a non-``file://`` URI, an empty key, or one
|
|
423
|
+
that names a sidecar.
|
|
424
|
+
"""
|
|
425
|
+
if not uri.startswith("file://"):
|
|
426
|
+
return None
|
|
427
|
+
key = uri[len("file://") :]
|
|
428
|
+
# Refuse empty keys + sidecars (so a caller can't read one as an artifact).
|
|
429
|
+
if not key or key.endswith(_SIDECAR_SUFFIX):
|
|
430
|
+
return None
|
|
431
|
+
return key
|
|
432
|
+
|
|
433
|
+
# ----- internals -------------------------------------------------
|
|
434
|
+
|
|
435
|
+
def _session_dir(self, session_id: str) -> Path:
|
|
436
|
+
"""Resolve the on-disk directory for a session's files.
|
|
437
|
+
|
|
438
|
+
Layout: ``{data_dir}/files/{date}/{session_id}/`` where
|
|
439
|
+
``date`` is the UTC date of *now* (the moment of put). This
|
|
440
|
+
matches the date-partitioning convention of ``events/`` and
|
|
441
|
+
``channels/`` — keeps retention / ops boundaries consistent.
|
|
442
|
+
"""
|
|
443
|
+
today = datetime.now(UTC).date().isoformat()
|
|
444
|
+
return self._files_dir / today / session_id
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _unique_filename(directory: Path, stem: str, ext: str) -> str:
|
|
448
|
+
"""Return a filename that does not collide in ``directory``.
|
|
449
|
+
|
|
450
|
+
First attempt is ``{stem}{ext}``. On collision, appends
|
|
451
|
+
``_2``, ``_3``, … until an unused name is found. Preserves
|
|
452
|
+
claim-check immutability — a repeated put never overwrites
|
|
453
|
+
an existing artifact's bytes.
|
|
454
|
+
"""
|
|
455
|
+
candidate = f"{stem}{ext}"
|
|
456
|
+
if not (directory / candidate).exists():
|
|
457
|
+
return candidate
|
|
458
|
+
n = 2
|
|
459
|
+
while True:
|
|
460
|
+
candidate = f"{stem}_{n}{ext}"
|
|
461
|
+
if not (directory / candidate).exists():
|
|
462
|
+
return candidate
|
|
463
|
+
n += 1
|