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.
Files changed (521) hide show
  1. testerkit/__init__.py +68 -0
  2. testerkit/_docs/_assets/operator-ui/channels/table.png +0 -0
  3. testerkit/_docs/_assets/operator-ui/dashboard/runs.png +0 -0
  4. testerkit/_docs/_assets/operator-ui/dashboard/stations.png +0 -0
  5. testerkit/_docs/_assets/operator-ui/events/filters.png +0 -0
  6. testerkit/_docs/_assets/operator-ui/events/table.png +0 -0
  7. testerkit/_docs/_assets/operator-ui/explore/chart.png +0 -0
  8. testerkit/_docs/_assets/operator-ui/explore/filters.png +0 -0
  9. testerkit/_docs/_assets/operator-ui/explore/plot-controls.png +0 -0
  10. testerkit/_docs/_assets/operator-ui/files/table.png +0 -0
  11. testerkit/_docs/_assets/operator-ui/launch/form.png +0 -0
  12. testerkit/_docs/_assets/operator-ui/metrics/filters.png +0 -0
  13. testerkit/_docs/_assets/operator-ui/metrics/pareto.png +0 -0
  14. testerkit/_docs/_assets/operator-ui/metrics/ppk.png +0 -0
  15. testerkit/_docs/_assets/operator-ui/metrics/yield.png +0 -0
  16. testerkit/_docs/_assets/operator-ui/results/detail-header.png +0 -0
  17. testerkit/_docs/_assets/operator-ui/results/detail-measurements.png +0 -0
  18. testerkit/_docs/_assets/operator-ui/results/detail-overview.png +0 -0
  19. testerkit/_docs/_assets/operator-ui/results/detail-steps.png +0 -0
  20. testerkit/_docs/_assets/operator-ui/results/stats.png +0 -0
  21. testerkit/_docs/_assets/operator-ui/results/table.png +0 -0
  22. testerkit/_docs/_assets/operator-ui/tour/designer.png +0 -0
  23. testerkit/_docs/_assets/operator-ui/tour/docs.png +0 -0
  24. testerkit/_docs/_assets/operator-ui/tour/fixtures.png +0 -0
  25. testerkit/_docs/_assets/operator-ui/tour/instruments.png +0 -0
  26. testerkit/_docs/_assets/operator-ui/tour/parts.png +0 -0
  27. testerkit/_docs/_assets/operator-ui/tour/profiles.png +0 -0
  28. testerkit/_docs/_assets/operator-ui/tour/stations.png +0 -0
  29. testerkit/_docs/_assets/operator-ui/tour/tests.png +0 -0
  30. testerkit/_docs/_assets/operator-ui/tour/uuts.png +0 -0
  31. testerkit/_docs/concepts/configuration/capabilities.md +447 -0
  32. testerkit/_docs/concepts/configuration/fixtures.md +335 -0
  33. testerkit/_docs/concepts/configuration/index.md +14 -0
  34. testerkit/_docs/concepts/configuration/parts.md +276 -0
  35. testerkit/_docs/concepts/configuration/stations.md +214 -0
  36. testerkit/_docs/concepts/data/data-stores.md +199 -0
  37. testerkit/_docs/concepts/data/event-log.md +185 -0
  38. testerkit/_docs/concepts/data/event-sourcing.md +76 -0
  39. testerkit/_docs/concepts/data/flight-streaming.md +58 -0
  40. testerkit/_docs/concepts/data/index.md +17 -0
  41. testerkit/_docs/concepts/data/sessions.md +78 -0
  42. testerkit/_docs/concepts/data/three-verbs.md +260 -0
  43. testerkit/_docs/concepts/execution/index.md +13 -0
  44. testerkit/_docs/concepts/execution/outcomes.md +199 -0
  45. testerkit/_docs/concepts/execution/step-hierarchy.md +170 -0
  46. testerkit/_docs/concepts/execution/step-manifest.md +115 -0
  47. testerkit/_docs/concepts/index.md +40 -0
  48. testerkit/_docs/concepts/overview/ai-integration.md +57 -0
  49. testerkit/_docs/concepts/overview/architecture.md +357 -0
  50. testerkit/_docs/concepts/overview/index.md +14 -0
  51. testerkit/_docs/concepts/overview/platform-vs-framework.md +147 -0
  52. testerkit/_docs/concepts/overview/pytest.md +53 -0
  53. testerkit/_docs/concepts/overview/tiers.md +46 -0
  54. testerkit/_docs/how-to/catalog/datasheet-to-test.md +169 -0
  55. testerkit/_docs/how-to/catalog/index.md +11 -0
  56. testerkit/_docs/how-to/configuration/configuring-stations.md +364 -0
  57. testerkit/_docs/how-to/configuration/custom-drivers.md +484 -0
  58. testerkit/_docs/how-to/configuration/index.md +13 -0
  59. testerkit/_docs/how-to/configuration/mock-mode.md +342 -0
  60. testerkit/_docs/how-to/data/benchmarking.md +120 -0
  61. testerkit/_docs/how-to/data/capture-an-artifact.md +84 -0
  62. testerkit/_docs/how-to/data/capture-waveform.md +110 -0
  63. testerkit/_docs/how-to/data/choosing-a-channel-verb.md +108 -0
  64. testerkit/_docs/how-to/data/compare-runs.md +120 -0
  65. testerkit/_docs/how-to/data/export-results.md +104 -0
  66. testerkit/_docs/how-to/data/find-flaky-tests.md +108 -0
  67. testerkit/_docs/how-to/data/grafana-dashboards.md +148 -0
  68. testerkit/_docs/how-to/data/index.md +23 -0
  69. testerkit/_docs/how-to/data/mcp-debug-failures.md +125 -0
  70. testerkit/_docs/how-to/data/mcp-query-runs.md +120 -0
  71. testerkit/_docs/how-to/data/querying-channels.md +117 -0
  72. testerkit/_docs/how-to/data/querying-events.md +85 -0
  73. testerkit/_docs/how-to/data/stream-live-channel.md +134 -0
  74. testerkit/_docs/how-to/execution/custom-operator-ui.md +136 -0
  75. testerkit/_docs/how-to/execution/index.md +21 -0
  76. testerkit/_docs/how-to/execution/limits.md +193 -0
  77. testerkit/_docs/how-to/execution/managing-sessions.md +100 -0
  78. testerkit/_docs/how-to/execution/multi-uut-testing.md +211 -0
  79. testerkit/_docs/how-to/execution/operator-prompts.md +151 -0
  80. testerkit/_docs/how-to/execution/profiles.md +304 -0
  81. testerkit/_docs/how-to/execution/spec-driven-testing.md +169 -0
  82. testerkit/_docs/how-to/execution/test-context.md +212 -0
  83. testerkit/_docs/how-to/execution/traceability.md +252 -0
  84. testerkit/_docs/how-to/execution/vector-expansion.md +394 -0
  85. testerkit/_docs/how-to/execution/writing-tests.md +363 -0
  86. testerkit/_docs/how-to/index.md +59 -0
  87. testerkit/_docs/how-to/overview/index.md +11 -0
  88. testerkit/_docs/how-to/overview/mcp-integration.md +168 -0
  89. testerkit/_docs/how-to/overview/operator-ui-tour.md +234 -0
  90. testerkit/_docs/integration/data/grafana.md +162 -0
  91. testerkit/_docs/integration/data/index.md +15 -0
  92. testerkit/_docs/integration/data/lakehouse-import.md +226 -0
  93. testerkit/_docs/integration/data/logging.md +131 -0
  94. testerkit/_docs/integration/data/results-api.md +51 -0
  95. testerkit/_docs/integration/index.md +20 -0
  96. testerkit/_docs/integration/runtime/harness.md +281 -0
  97. testerkit/_docs/integration/runtime/index.md +13 -0
  98. testerkit/_docs/integration/runtime/instruments.md +284 -0
  99. testerkit/_docs/integration/runtime/pytest-existing.md +315 -0
  100. testerkit/_docs/reference/catalog/cookbook.md +389 -0
  101. testerkit/_docs/reference/catalog/index.md +11 -0
  102. testerkit/_docs/reference/catalog/schema.md +375 -0
  103. testerkit/_docs/reference/cli.md +660 -0
  104. testerkit/_docs/reference/configuration.md +406 -0
  105. testerkit/_docs/reference/data/channels-schema.md +145 -0
  106. testerkit/_docs/reference/data/event-types.md +554 -0
  107. testerkit/_docs/reference/data/events-schema.md +120 -0
  108. testerkit/_docs/reference/data/files-schema.md +141 -0
  109. testerkit/_docs/reference/data/index.md +18 -0
  110. testerkit/_docs/reference/data/models.md +2229 -0
  111. testerkit/_docs/reference/data/outputs.md +82 -0
  112. testerkit/_docs/reference/data/parquet-schema.md +529 -0
  113. testerkit/_docs/reference/data/performance-limits.md +224 -0
  114. testerkit/_docs/reference/data/query-api.md +247 -0
  115. testerkit/_docs/reference/index.md +69 -0
  116. testerkit/_docs/reference/operator-ui/channels/detail.md +82 -0
  117. testerkit/_docs/reference/operator-ui/channels/list.md +70 -0
  118. testerkit/_docs/reference/operator-ui/dashboard.md +87 -0
  119. testerkit/_docs/reference/operator-ui/designer.md +167 -0
  120. testerkit/_docs/reference/operator-ui/events.md +89 -0
  121. testerkit/_docs/reference/operator-ui/files.md +74 -0
  122. testerkit/_docs/reference/operator-ui/fixtures.md +100 -0
  123. testerkit/_docs/reference/operator-ui/instruments.md +126 -0
  124. testerkit/_docs/reference/operator-ui/launch.md +79 -0
  125. testerkit/_docs/reference/operator-ui/live.md +102 -0
  126. testerkit/_docs/reference/operator-ui/measurements.md +159 -0
  127. testerkit/_docs/reference/operator-ui/metrics.md +217 -0
  128. testerkit/_docs/reference/operator-ui/parts.md +98 -0
  129. testerkit/_docs/reference/operator-ui/profiles.md +103 -0
  130. testerkit/_docs/reference/operator-ui/results/detail.md +214 -0
  131. testerkit/_docs/reference/operator-ui/results/list.md +106 -0
  132. testerkit/_docs/reference/operator-ui/stations.md +98 -0
  133. testerkit/_docs/reference/operator-ui/tests.md +173 -0
  134. testerkit/_docs/reference/operator-ui/uuts.md +67 -0
  135. testerkit/_docs/reference/overview/index.md +11 -0
  136. testerkit/_docs/reference/overview/pytest-native.md +142 -0
  137. testerkit/_docs/reference/overview/skills.md +177 -0
  138. testerkit/_docs/reference/pytest/fixtures.md +342 -0
  139. testerkit/_docs/reference/pytest/index.md +12 -0
  140. testerkit/_docs/reference/pytest/markers.md +257 -0
  141. testerkit/_docs/reference/runtime/api.md +246 -0
  142. testerkit/_docs/reference/runtime/client.md +280 -0
  143. testerkit/_docs/reference/runtime/connect.md +140 -0
  144. testerkit/_docs/reference/runtime/index.md +13 -0
  145. testerkit/_docs/tutorial/01-first-test.md +113 -0
  146. testerkit/_docs/tutorial/02-mock-instruments.md +88 -0
  147. testerkit/_docs/tutorial/03-fixtures.md +169 -0
  148. testerkit/_docs/tutorial/04-limits.md +139 -0
  149. testerkit/_docs/tutorial/05-configuration.md +194 -0
  150. testerkit/_docs/tutorial/06-specifications.md +273 -0
  151. testerkit/_docs/tutorial/07-real-instruments.md +245 -0
  152. testerkit/_docs/tutorial/08-capabilities.md +223 -0
  153. testerkit/_docs/tutorial/09-production.md +374 -0
  154. testerkit/_docs/tutorial/10-live-monitoring.md +156 -0
  155. testerkit/_docs/tutorial/11-waveforms-and-evidence.md +169 -0
  156. testerkit/_docs/tutorial/12-continuous-monitoring.md +145 -0
  157. testerkit/_docs/tutorial/13-parallel-testing.md +233 -0
  158. testerkit/_docs/tutorial/index.md +61 -0
  159. testerkit/_docs/tutorial/quickstart.md +282 -0
  160. testerkit/analysis/__init__.py +9 -0
  161. testerkit/analysis/_common.py +24 -0
  162. testerkit/analysis/measurement_facets.py +524 -0
  163. testerkit/analysis/measurements_query.py +1458 -0
  164. testerkit/analysis/metrics.py +389 -0
  165. testerkit/analysis/runs_query.py +434 -0
  166. testerkit/analysis/steps_query.py +413 -0
  167. testerkit/api/__init__.py +6 -0
  168. testerkit/api/_mime.py +71 -0
  169. testerkit/api/app.py +1171 -0
  170. testerkit/api/dialogs/__init__.py +36 -0
  171. testerkit/api/dialogs/manager.py +617 -0
  172. testerkit/api/dialogs/models.py +79 -0
  173. testerkit/api/models.py +80 -0
  174. testerkit/api/responses.py +205 -0
  175. testerkit/api/runner.py +222 -0
  176. testerkit/api/schemas.py +311 -0
  177. testerkit/benchmark/__init__.py +34 -0
  178. testerkit/benchmark/concurrency.py +248 -0
  179. testerkit/benchmark/core.py +232 -0
  180. testerkit/benchmark/runner.py +628 -0
  181. testerkit/benchmark/scenario.py +313 -0
  182. testerkit/benchmark/system.py +191 -0
  183. testerkit/benchmark/workloads.py +507 -0
  184. testerkit/catalog/__init__.py +6 -0
  185. testerkit/catalog/generic/generic_dmm.yaml +61 -0
  186. testerkit/catalog/generic/generic_eload.yaml +31 -0
  187. testerkit/catalog/generic/generic_fgen.yaml +56 -0
  188. testerkit/catalog/generic/generic_oscilloscope.yaml +45 -0
  189. testerkit/catalog/generic/generic_psu.yaml +77 -0
  190. testerkit/catalog/generic/generic_smu.yaml +73 -0
  191. testerkit/channels.py +538 -0
  192. testerkit/cli/__init__.py +32 -0
  193. testerkit/cli/_common.py +113 -0
  194. testerkit/cli/_time.py +179 -0
  195. testerkit/cli/benchmark_cmd.py +56 -0
  196. testerkit/cli/catalog_cmd.py +39 -0
  197. testerkit/cli/daemon.py +157 -0
  198. testerkit/cli/data_cmd.py +1040 -0
  199. testerkit/cli/discover_cmd.py +94 -0
  200. testerkit/cli/docs_cmd.py +100 -0
  201. testerkit/cli/instrument.py +172 -0
  202. testerkit/cli/mcp_cmd.py +37 -0
  203. testerkit/cli/metrics.py +366 -0
  204. testerkit/cli/project.py +312 -0
  205. testerkit/cli/root.py +14 -0
  206. testerkit/cli/runs.py +412 -0
  207. testerkit/cli/schema_cmd.py +87 -0
  208. testerkit/cli/serve_cmd.py +50 -0
  209. testerkit/cli/setup_cmd.py +604 -0
  210. testerkit/cli/station.py +294 -0
  211. testerkit/cli/validate.py +112 -0
  212. testerkit/client.py +463 -0
  213. testerkit/connect.py +635 -0
  214. testerkit/data/__init__.py +8 -0
  215. testerkit/data/_accumulator_pool.py +455 -0
  216. testerkit/data/_atomic.py +59 -0
  217. testerkit/data/_collection_indices.py +67 -0
  218. testerkit/data/_daemon_lifecycle.py +535 -0
  219. testerkit/data/_duckdb_daemon.py +695 -0
  220. testerkit/data/_duckdb_flight_server.py +813 -0
  221. testerkit/data/_event_filters.py +15 -0
  222. testerkit/data/_event_reader.py +101 -0
  223. testerkit/data/_flight_errors.py +199 -0
  224. testerkit/data/_flight_query.py +202 -0
  225. testerkit/data/_flight_retry.py +148 -0
  226. testerkit/data/_flight_subscribe.py +67 -0
  227. testerkit/data/_index_epoch.py +342 -0
  228. testerkit/data/_ipc_writer.py +185 -0
  229. testerkit/data/_json_safe.py +63 -0
  230. testerkit/data/_process.py +18 -0
  231. testerkit/data/_push_relay.py +124 -0
  232. testerkit/data/_runs_duckdb_daemon.py +2704 -0
  233. testerkit/data/_session_reaper.py +110 -0
  234. testerkit/data/_sql_helpers.py +30 -0
  235. testerkit/data/_store.py +24 -0
  236. testerkit/data/backends/__init__.py +7 -0
  237. testerkit/data/backends/_event_accumulator.py +957 -0
  238. testerkit/data/backends/_row_helpers.py +1172 -0
  239. testerkit/data/backends/parquet.py +1143 -0
  240. testerkit/data/backends/protocol.py +35 -0
  241. testerkit/data/channels/__init__.py +8 -0
  242. testerkit/data/channels/_flight_daemon.py +75 -0
  243. testerkit/data/channels/client.py +242 -0
  244. testerkit/data/channels/flight_manager.py +88 -0
  245. testerkit/data/channels/index.py +664 -0
  246. testerkit/data/channels/models.py +418 -0
  247. testerkit/data/channels/server.py +162 -0
  248. testerkit/data/channels/store.py +1481 -0
  249. testerkit/data/data_dir.py +61 -0
  250. testerkit/data/duckdb_manager.py +79 -0
  251. testerkit/data/event_log.py +384 -0
  252. testerkit/data/event_store.py +768 -0
  253. testerkit/data/events.py +1025 -0
  254. testerkit/data/exporters/__init__.py +6 -0
  255. testerkit/data/exporters/_helpers.py +40 -0
  256. testerkit/data/exporters/csv_exporter.py +168 -0
  257. testerkit/data/exporters/hdf5.py +234 -0
  258. testerkit/data/exporters/json_exporter.py +229 -0
  259. testerkit/data/exporters/mdf4.py +201 -0
  260. testerkit/data/exporters/stdf.py +255 -0
  261. testerkit/data/exporters/tdms.py +292 -0
  262. testerkit/data/files/__init__.py +71 -0
  263. testerkit/data/files/_backend.py +207 -0
  264. testerkit/data/files/_catalog_daemon.py +84 -0
  265. testerkit/data/files/catalog.py +218 -0
  266. testerkit/data/files/catalog_manager.py +325 -0
  267. testerkit/data/files/models.py +60 -0
  268. testerkit/data/files/serializers.py +414 -0
  269. testerkit/data/files/store.py +463 -0
  270. testerkit/data/files/streaming.py +783 -0
  271. testerkit/data/models.py +604 -0
  272. testerkit/data/ref.py +152 -0
  273. testerkit/data/retention.py +297 -0
  274. testerkit/data/run_store.py +367 -0
  275. testerkit/data/runs_duckdb_manager.py +103 -0
  276. testerkit/data/schema_dispatch.py +191 -0
  277. testerkit/data/schema_migrate.py +76 -0
  278. testerkit/data/schema_versions.py +85 -0
  279. testerkit/data/schemas.py +288 -0
  280. testerkit/data/subscribers/__init__.py +48 -0
  281. testerkit/data/subscribers/_base.py +20 -0
  282. testerkit/data/subscribers/_output_file.py +25 -0
  283. testerkit/data/subscribers/replay.py +55 -0
  284. testerkit/environment.py +84 -0
  285. testerkit/execution/__init__.py +11 -0
  286. testerkit/execution/_git.py +190 -0
  287. testerkit/execution/_state.py +740 -0
  288. testerkit/execution/accessors.py +54 -0
  289. testerkit/execution/audit.py +51 -0
  290. testerkit/execution/cascade.py +85 -0
  291. testerkit/execution/connections.py +491 -0
  292. testerkit/execution/harness.py +1931 -0
  293. testerkit/execution/instrument_events.py +42 -0
  294. testerkit/execution/limits.py +154 -0
  295. testerkit/execution/metadata.py +102 -0
  296. testerkit/execution/mocks.py +63 -0
  297. testerkit/execution/profiles.py +616 -0
  298. testerkit/execution/run_scope.py +1331 -0
  299. testerkit/execution/session_scope.py +196 -0
  300. testerkit/execution/sidecar.py +280 -0
  301. testerkit/execution/site_runner.py +705 -0
  302. testerkit/execution/sites.py +183 -0
  303. testerkit/execution/sync.py +294 -0
  304. testerkit/execution/uut_provider.py +231 -0
  305. testerkit/execution/vectors.py +205 -0
  306. testerkit/execution/verify.py +396 -0
  307. testerkit/expand.py +75 -0
  308. testerkit/files.py +235 -0
  309. testerkit/fixtures/__init__.py +6 -0
  310. testerkit/fixtures/manager.py +357 -0
  311. testerkit/grafana/__init__.py +1 -0
  312. testerkit/grafana/bootstrap.py +61 -0
  313. testerkit/grafana/cli.py +314 -0
  314. testerkit/grafana/dashboards/asset_utilization.json +92 -0
  315. testerkit/grafana/dashboards/channel_explorer.json +77 -0
  316. testerkit/grafana/dashboards/event_log.json +92 -0
  317. testerkit/grafana/dashboards/failure_pareto.json +104 -0
  318. testerkit/grafana/dashboards/measurement_distribution.json +97 -0
  319. testerkit/grafana/dashboards/measurement_trend.json +161 -0
  320. testerkit/grafana/dashboards/station_comparison.json +95 -0
  321. testerkit/grafana/dashboards/test_duration.json +104 -0
  322. testerkit/grafana/dashboards/unit_traceability.json +73 -0
  323. testerkit/grafana/dashboards/yield_overview.json +189 -0
  324. testerkit/grafana/provisioning/dashboards.yaml.j2 +12 -0
  325. testerkit/grafana/provisioning/datasources.yaml.j2 +17 -0
  326. testerkit/grafana/server.py +282 -0
  327. testerkit/init.py +724 -0
  328. testerkit/instruments/__init__.py +10 -0
  329. testerkit/instruments/base.py +82 -0
  330. testerkit/instruments/discovery/__init__.py +62 -0
  331. testerkit/instruments/discovery/_base.py +174 -0
  332. testerkit/instruments/discovery/lxi.py +152 -0
  333. testerkit/instruments/discovery/ni.py +95 -0
  334. testerkit/instruments/discovery/serial.py +73 -0
  335. testerkit/instruments/discovery/visa.py +98 -0
  336. testerkit/instruments/lifecycle.py +213 -0
  337. testerkit/instruments/loader.py +79 -0
  338. testerkit/instruments/locks.py +178 -0
  339. testerkit/instruments/mocks.py +230 -0
  340. testerkit/instruments/observer.py +270 -0
  341. testerkit/instruments/observers/__init__.py +36 -0
  342. testerkit/instruments/observers/_base.py +68 -0
  343. testerkit/instruments/observers/daqmx.py +70 -0
  344. testerkit/instruments/observers/descriptor.py +69 -0
  345. testerkit/instruments/observers/generic.py +92 -0
  346. testerkit/instruments/observers/lantz.py +31 -0
  347. testerkit/instruments/observers/modbus.py +96 -0
  348. testerkit/instruments/observers/motion.py +68 -0
  349. testerkit/instruments/observers/ni_modular.py +73 -0
  350. testerkit/instruments/observers/ophyd.py +80 -0
  351. testerkit/instruments/observers/pymeasure.py +120 -0
  352. testerkit/instruments/observers/qcodes.py +66 -0
  353. testerkit/instruments/observers/scpi.py +88 -0
  354. testerkit/instruments/observers/tektronix.py +57 -0
  355. testerkit/instruments/observers/visa.py +103 -0
  356. testerkit/instruments/pool.py +459 -0
  357. testerkit/instruments/proxy.py +59 -0
  358. testerkit/instruments/route_manager.py +329 -0
  359. testerkit/instruments/routed_proxy.py +88 -0
  360. testerkit/instruments/server.py +508 -0
  361. testerkit/instruments/switch.py +43 -0
  362. testerkit/instruments/visa.py +294 -0
  363. testerkit/matching/__init__.py +6 -0
  364. testerkit/matching/service.py +969 -0
  365. testerkit/mcp/__init__.py +6 -0
  366. testerkit/mcp/server.py +681 -0
  367. testerkit/mcp/tools.py +1868 -0
  368. testerkit/models/__init__.py +18 -0
  369. testerkit/models/capability.py +598 -0
  370. testerkit/models/catalog.py +76 -0
  371. testerkit/models/data_options.py +153 -0
  372. testerkit/models/enums.py +334 -0
  373. testerkit/models/instrument.py +152 -0
  374. testerkit/models/instrument_asset.py +25 -0
  375. testerkit/models/part.py +271 -0
  376. testerkit/models/part_manifest.py +96 -0
  377. testerkit/models/project.py +114 -0
  378. testerkit/models/station.py +140 -0
  379. testerkit/models/test_config.py +748 -0
  380. testerkit/ontology/__init__.py +39 -0
  381. testerkit/ontology/schema.py +188 -0
  382. testerkit/ontology/testerkit.yaml +1420 -0
  383. testerkit/parts/__init__.py +8 -0
  384. testerkit/parts/context.py +181 -0
  385. testerkit/parts/folder.py +251 -0
  386. testerkit/parts/loader.py +27 -0
  387. testerkit/prompts/__init__.py +17 -0
  388. testerkit/prompts/core.py +133 -0
  389. testerkit/pytest_plugin/__init__.py +1287 -0
  390. testerkit/pytest_plugin/autouse.py +369 -0
  391. testerkit/pytest_plugin/helpers.py +354 -0
  392. testerkit/pytest_plugin/hooks.py +1982 -0
  393. testerkit/pytest_plugin/markers.py +203 -0
  394. testerkit/pytest_plugin/retry.py +37 -0
  395. testerkit/pytest_plugin/sweeps.py +117 -0
  396. testerkit/queries.py +78 -0
  397. testerkit/reports/__init__.py +6 -0
  398. testerkit/reports/core.py +492 -0
  399. testerkit/reports/datasheet.py +831 -0
  400. testerkit/reports/templates/datasheet.html +383 -0
  401. testerkit/reports/templates/default.html +153 -0
  402. testerkit/sbom.py +142 -0
  403. testerkit/schema_export.py +101 -0
  404. testerkit/signals.py +63 -0
  405. testerkit/skills/README.md +58 -0
  406. testerkit/skills/templates/project-instructions.md +125 -0
  407. testerkit/skills/testerkit-analysis/SKILL.md +87 -0
  408. testerkit/skills/testerkit-capture/SKILL.md +114 -0
  409. testerkit/skills/testerkit-data/SKILL.md +138 -0
  410. testerkit/skills/testerkit-datasheets/SKILL.md +100 -0
  411. testerkit/skills/testerkit-datasheets/agents/scaffold-writer.md +115 -0
  412. testerkit/skills/testerkit-datasheets/agents/section-extractor.md +98 -0
  413. testerkit/skills/testerkit-datasheets/agents/section-reviewer.md +153 -0
  414. testerkit/skills/testerkit-datasheets/agents/section-splitter.md +89 -0
  415. testerkit/skills/testerkit-datasheets/agents/section-writer.md +226 -0
  416. testerkit/skills/testerkit-datasheets/references/catalog-pipeline.md +271 -0
  417. testerkit/skills/testerkit-datasheets/references/process-queue.md +42 -0
  418. testerkit/skills/testerkit-datasheets/references/scaffold.md +156 -0
  419. testerkit/skills/testerkit-datasheets/references/test-pipeline.md +312 -0
  420. testerkit/skills/testerkit-debug/SKILL.md +127 -0
  421. testerkit/skills/testerkit-interactive/SKILL.md +188 -0
  422. testerkit/skills/testerkit-interactive/references/live-ui-patterns.md +102 -0
  423. testerkit/skills/testerkit-mocks/SKILL.md +128 -0
  424. testerkit/skills/testerkit-parts/SKILL.md +126 -0
  425. testerkit/skills/testerkit-profiles/SKILL.md +139 -0
  426. testerkit/skills/testerkit-sites/SKILL.md +140 -0
  427. testerkit/skills/testerkit-stations/SKILL.md +146 -0
  428. testerkit/skills/testerkit-tests/SKILL.md +136 -0
  429. testerkit/store.py +1639 -0
  430. testerkit/ui/__init__.py +71 -0
  431. testerkit/ui/_asgi.py +162 -0
  432. testerkit/ui/app.py +32 -0
  433. testerkit/ui/components/__init__.py +13 -0
  434. testerkit/ui/components/artifact_viewer.py +250 -0
  435. testerkit/ui/components/channel_values.py +159 -0
  436. testerkit/ui/components/event_timeline.py +222 -0
  437. testerkit/ui/components/execution_gantt.py +210 -0
  438. testerkit/ui/components/file_streams.py +147 -0
  439. testerkit/ui/components/instrument_activity.py +146 -0
  440. testerkit/ui/components/session_table.py +118 -0
  441. testerkit/ui/pages/__init__.py +25 -0
  442. testerkit/ui/pages/channels/__init__.py +4 -0
  443. testerkit/ui/pages/channels/detail.py +932 -0
  444. testerkit/ui/pages/channels/list.py +515 -0
  445. testerkit/ui/pages/dashboard.py +236 -0
  446. testerkit/ui/pages/designer/__init__.py +5 -0
  447. testerkit/ui/pages/designer/graph.py +635 -0
  448. testerkit/ui/pages/designer/matching.py +663 -0
  449. testerkit/ui/pages/designer/page.py +593 -0
  450. testerkit/ui/pages/designer/properties.py +591 -0
  451. testerkit/ui/pages/designer/state.py +468 -0
  452. testerkit/ui/pages/docs/__init__.py +6 -0
  453. testerkit/ui/pages/docs/index.py +95 -0
  454. testerkit/ui/pages/docs/page.py +698 -0
  455. testerkit/ui/pages/events/__init__.py +3 -0
  456. testerkit/ui/pages/events/list.py +263 -0
  457. testerkit/ui/pages/explore.py +1108 -0
  458. testerkit/ui/pages/files/__init__.py +4 -0
  459. testerkit/ui/pages/files/detail.py +426 -0
  460. testerkit/ui/pages/files/list.py +447 -0
  461. testerkit/ui/pages/fixtures/__init__.py +8 -0
  462. testerkit/ui/pages/fixtures/detail.py +280 -0
  463. testerkit/ui/pages/fixtures/edit.py +370 -0
  464. testerkit/ui/pages/fixtures/list.py +180 -0
  465. testerkit/ui/pages/fixtures/new.py +195 -0
  466. testerkit/ui/pages/instruments/__init__.py +4 -0
  467. testerkit/ui/pages/instruments/detail.py +320 -0
  468. testerkit/ui/pages/instruments/edit.py +482 -0
  469. testerkit/ui/pages/instruments/list.py +249 -0
  470. testerkit/ui/pages/instruments/new.py +200 -0
  471. testerkit/ui/pages/launch.py +261 -0
  472. testerkit/ui/pages/live.py +124 -0
  473. testerkit/ui/pages/metrics_page.py +1229 -0
  474. testerkit/ui/pages/parts/__init__.py +11 -0
  475. testerkit/ui/pages/parts/detail.py +202 -0
  476. testerkit/ui/pages/parts/edit.py +310 -0
  477. testerkit/ui/pages/parts/list.py +160 -0
  478. testerkit/ui/pages/parts/new.py +170 -0
  479. testerkit/ui/pages/parts/requirements.py +153 -0
  480. testerkit/ui/pages/parts/stations.py +199 -0
  481. testerkit/ui/pages/profiles/__init__.py +4 -0
  482. testerkit/ui/pages/profiles/detail.py +113 -0
  483. testerkit/ui/pages/profiles/list.py +86 -0
  484. testerkit/ui/pages/results/__init__.py +4 -0
  485. testerkit/ui/pages/results/detail.py +631 -0
  486. testerkit/ui/pages/results/list.py +230 -0
  487. testerkit/ui/pages/stations/__init__.py +4 -0
  488. testerkit/ui/pages/stations/detail.py +256 -0
  489. testerkit/ui/pages/stations/edit.py +273 -0
  490. testerkit/ui/pages/stations/list.py +180 -0
  491. testerkit/ui/pages/stations/new.py +311 -0
  492. testerkit/ui/pages/tests/__init__.py +4 -0
  493. testerkit/ui/pages/tests/detail.py +152 -0
  494. testerkit/ui/pages/tests/list.py +194 -0
  495. testerkit/ui/pages/uuts/__init__.py +4 -0
  496. testerkit/ui/pages/uuts/list.py +97 -0
  497. testerkit/ui/shared/__init__.py +8 -0
  498. testerkit/ui/shared/components.py +1826 -0
  499. testerkit/ui/shared/dialogs.py +105 -0
  500. testerkit/ui/shared/event_binding.py +194 -0
  501. testerkit/ui/shared/layout.py +314 -0
  502. testerkit/ui/shared/services.py +1719 -0
  503. testerkit/ui/shared/timestamps.py +21 -0
  504. testerkit/ui/static/branding/favicon-16.png +0 -0
  505. testerkit/ui/static/branding/favicon-32.png +0 -0
  506. testerkit/ui/static/branding/favicon-48.png +0 -0
  507. testerkit/ui/static/branding/testerkit-icon-256.png +0 -0
  508. testerkit/ui/static/branding/testerkit-mark.svg +1 -0
  509. testerkit/ui/static/branding/testerkit-wordmark.svg +1 -0
  510. testerkit/ui/static/global.css +465 -0
  511. testerkit/utils/__init__.py +17 -0
  512. testerkit/utils/enum_meta.py +800 -0
  513. testerkit/utils/paths.py +75 -0
  514. testerkit/utils/ranges.py +257 -0
  515. testerkit/validation.py +82 -0
  516. testerkit/verbs.py +142 -0
  517. testerkit-0.4.0.dist-info/METADATA +250 -0
  518. testerkit-0.4.0.dist-info/RECORD +521 -0
  519. testerkit-0.4.0.dist-info/WHEEL +4 -0
  520. testerkit-0.4.0.dist-info/entry_points.txt +5 -0
  521. 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)