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,76 @@
1
+ """Opt-in **migrate** sink — rewrite an owned durable file forward to the current
2
+ schema, in place, via the same adapter the reader uses.
3
+
4
+ The re-index sink (the whitelist-dispatch reader at each store's ingest boundary)
5
+ is the *mandatory floor*: it projects any readable version forward at query time
6
+ and never touches the file. Migrate is the *optional* housekeeping layer on top —
7
+ "migration is re-index that also persists the adapter output" (§4 of
8
+ ``schema-versioning-migration.md``). It exists so an operator can retire old-major
9
+ files (and their adapters) once they're rewritten, not because reading requires
10
+ it. Never a substitute for read-time projection: files in the wild are unreachable.
11
+
12
+ Both content kinds the adapters span are covered here:
13
+ - ``migrate_parquet_file`` — Arrow/parquet (runs). Events/channels are the same
14
+ Arrow pattern over IPC; wired when a real adapter first needs them (YAGNI).
15
+ - ``migrate_sidecar_file`` — the FileStore JSON sidecar (Pydantic model).
16
+
17
+ Each is idempotent (a current-version file is a no-op) and raises
18
+ :class:`~testerkit.data.schema_dispatch.SchemaVersionRefused` for a file the current
19
+ build cannot read (absent/pre-1.0 or unknown) — the caller decides whether to
20
+ skip or surface it, exactly as the re-index sink does.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from pathlib import Path
27
+
28
+ import pyarrow.parquet as pq
29
+
30
+ from testerkit.data._atomic import atomic_write_table, atomic_write_text
31
+ from testerkit.data.files.models import FileArtifactMetadata
32
+ from testerkit.data.schema_dispatch import dispatch, stamp_from_arrow_metadata
33
+ from testerkit.data.schema_versions import CURRENT_SCHEMA_VERSION, SchemaStore
34
+
35
+
36
+ def migrate_parquet_file(store: SchemaStore, path: Path | str) -> bool:
37
+ """Rewrite a parquet artifact forward to the current schema, atomically.
38
+
39
+ Reads the footer stamp, applies the ``source -> current`` adapter, re-stamps
40
+ the result to the current version, and atomically replaces the file. Returns
41
+ ``True`` if it migrated, ``False`` if already current (no-op). Raises
42
+ :class:`SchemaVersionRefused` for an unreadable version.
43
+ """
44
+ path = Path(path)
45
+ parquet_file = pq.ParquetFile(str(path))
46
+ stamp = stamp_from_arrow_metadata(parquet_file.schema_arrow.metadata)
47
+ current = CURRENT_SCHEMA_VERSION[store]
48
+ if stamp == current:
49
+ return False
50
+ adapter = dispatch(store, stamp)
51
+ migrated = adapter(parquet_file.read()).replace_schema_metadata(
52
+ {b"schema_version": current.encode()}
53
+ )
54
+ atomic_write_table(migrated, path)
55
+ return True
56
+
57
+
58
+ def migrate_sidecar_file(path: Path | str) -> bool:
59
+ """Rewrite a FileStore ``.meta.json`` sidecar forward to the current schema.
60
+
61
+ Reads the raw JSON (so an absent stamp is detectable), applies the
62
+ ``source -> current`` adapter to the ``FileArtifactMetadata`` model, re-stamps
63
+ to the current version, and atomically replaces the sidecar. The blob is never
64
+ touched (it is opaque user payload). Returns ``True`` if migrated, ``False`` if
65
+ already current. Raises :class:`SchemaVersionRefused` for an unreadable version.
66
+ """
67
+ path = Path(path)
68
+ raw = json.loads(path.read_text())
69
+ current = CURRENT_SCHEMA_VERSION[SchemaStore.FILES]
70
+ if raw.get("schema_version") == current:
71
+ return False
72
+ adapter = dispatch(SchemaStore.FILES, raw.get("schema_version"))
73
+ migrated = adapter(FileArtifactMetadata.model_validate(raw))
74
+ migrated = migrated.model_copy(update={"schema_version": current})
75
+ atomic_write_text(migrated.model_dump_json(), path)
76
+ return True
@@ -0,0 +1,85 @@
1
+ """Central registry of durable-artifact schema versions.
2
+
3
+ The single home for *which durable stores exist and which schema versions
4
+ each can read*. Every durable artifact a TesterKit daemon writes carries a
5
+ schema-version stamp in its file metadata; the stamp is the **migration
6
+ key**. At each store's ``durable-artifact -> rows`` read boundary a
7
+ whitelist-dispatch reader matches the stamp against
8
+ :data:`KNOWN_SCHEMA_VERSIONS` and picks the adapter that projects that
9
+ version forward to the current shape. See
10
+ ``docs/_internal/explorations/schema-versioning-migration.md`` for the full
11
+ strategy (read-time adaptation, coexist-always + optional-migrate).
12
+
13
+ Versioning is SemVer and **decoupled from the testerkit package version** — the
14
+ stores diverge on their own lines, so schema ``0.1`` at package ``0.3.0`` is
15
+ deliberate, not a mismatch.
16
+
17
+ The breaking unit is the **epoch = leftmost-significant SemVer component**:
18
+
19
+ - **Pre-1.0** — the **MINOR** is the epoch. ``0.1 -> 0.2`` is a breaking
20
+ reshape (a new epoch); there is no additive tier yet. Each 0.x epoch is a
21
+ clean break that regenerates or read-time-adapts prior-epoch artifacts —
22
+ deliberately rehearsing the same epoch -> quarantine/adapter path we'll bet
23
+ on at the first real ``2.0``, so the apparatus is proven before 1.0.
24
+ - **Post-1.0** — the **MAJOR** is the epoch. ``1.0 -> 1.1`` becomes additive
25
+ (``union_by_name`` null-fills old files, ``ALTER TABLE ADD COLUMN IF NOT
26
+ EXISTS`` extends the projection); ``1.x -> 2.0`` is the breaking epoch,
27
+ needing a per-version read-time adapter + a frozen reference doc for the
28
+ outgoing epoch.
29
+
30
+ All stores start at ``"0.1"`` — a distinct pre-1.0 schema line, **not frozen**.
31
+ 1.0 is graduated to later, once the schema design and this apparatus have real
32
+ mileage (see ``docs/_internal/explorations/pre-1.0-epoch-strategy.md``).
33
+ Unstamped artifacts are unsupported by design (regenerate).
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ from enum import StrEnum
39
+
40
+
41
+ class SchemaStore(StrEnum):
42
+ """A durable-artifact store that stamps and dispatches on a schema version.
43
+
44
+ Events carries **two** coordinates (§3 of the strategy doc): the storage
45
+ ``ENVELOPE`` (the ``_IPC_SCHEMA`` column shape) and the payload
46
+ ``EVENT_CATALOG`` (the event models). They version independently.
47
+ """
48
+
49
+ RUNS = "runs"
50
+ """Runs parquet — content fused into the columnar schema (footer metadata)."""
51
+ EVENTS_ENVELOPE = "events_envelope"
52
+ """Event WAL Arrow IPC envelope — ``event_log._IPC_SCHEMA``."""
53
+ EVENT_CATALOG = "event_catalog"
54
+ """Event payload catalog — the event models in ``data.events``."""
55
+ CHANNELS = "channels"
56
+ """Channel ``.arrow`` skeleton, value typed by ``value_type``."""
57
+ FILES = "files"
58
+ """FileStore sidecar (``FileArtifactMetadata``); the blob is opaque."""
59
+
60
+
61
+ # The stamp a freshly written artifact carries, per store. This is the one
62
+ # home for the current version — every store's public constant aliases the
63
+ # matching entry here (do not hardcode the string at the write site).
64
+ CURRENT_SCHEMA_VERSION: dict[SchemaStore, str] = {
65
+ SchemaStore.RUNS: "0.1",
66
+ SchemaStore.EVENTS_ENVELOPE: "0.1",
67
+ SchemaStore.EVENT_CATALOG: "0.1",
68
+ SchemaStore.CHANNELS: "0.1",
69
+ SchemaStore.FILES: "0.1",
70
+ }
71
+
72
+ # Older epochs a store still ships a read-time adapter for. Empty today
73
+ # (0.1-only). Add an entry the *same commit* its ``vN -> current`` adapter
74
+ # lands, so the whitelist reader starts accepting that version exactly when it
75
+ # can transform it.
76
+ _LEGACY_READABLE: dict[SchemaStore, frozenset[str]] = {store: frozenset() for store in SchemaStore}
77
+
78
+ # Versions each store's reader will dispatch. Current ∪ legacy-readable, so the
79
+ # current version is always accepted by construction. Anything not in this set
80
+ # is refused at read time ("unsupported schema version"); an absent stamp is
81
+ # refused as unstamped/pre-baseline ("regenerate").
82
+ KNOWN_SCHEMA_VERSIONS: dict[SchemaStore, frozenset[str]] = {
83
+ store: frozenset({CURRENT_SCHEMA_VERSION[store]}) | _LEGACY_READABLE[store]
84
+ for store in SchemaStore
85
+ }
@@ -0,0 +1,288 @@
1
+ """Canonical Arrow schema contract shared across all backends.
2
+
3
+ Defines the measurement schema, type inference for dynamic columns,
4
+ and helpers for constructing validated Arrow tables. Any backend
5
+ (Parquet, Flight, NATS) imports from here rather than defining its own
6
+ schema.
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ import pyarrow as pa
13
+
14
+ from testerkit.data.backends._row_helpers import (
15
+ INSTRUMENT_STRUCT_FIELDS,
16
+ LANE_FIELDS,
17
+ MEASUREMENT_STRUCT_FIELDS,
18
+ )
19
+ from testerkit.data.schema_versions import CURRENT_SCHEMA_VERSION, SchemaStore
20
+
21
+ __all__ = [
22
+ "RUN_ROW_SCHEMA",
23
+ "SCHEMA_VERSION",
24
+ "INSTRUMENT_STRUCT_FIELDS",
25
+ "_INSTRUMENT_LIST",
26
+ "_INSTRUMENT_STRUCT",
27
+ "_LANE_LIST",
28
+ "_MEASUREMENT_LIST",
29
+ "_MEASUREMENT_STRUCT",
30
+ "_SCHEMA_DICT",
31
+ "_build_write_schema",
32
+ "table_from_rows",
33
+ ]
34
+
35
+ # Runs parquet footer stamp. Sourced from the central registry (one home);
36
+ # see ``testerkit.data.schema_versions`` for the SemVer / migration contract.
37
+ SCHEMA_VERSION = CURRENT_SCHEMA_VERSION[SchemaStore.RUNS]
38
+
39
+ # EAV lane struct — the nested at-rest representation of one input / output
40
+ # entry. ``value_type`` selects which ``value_*`` lane holds the value. Field
41
+ # names must match ``_row_helpers.LANE_FIELDS`` / the encoder (guarded below).
42
+ _LANE_STRUCT = pa.struct(
43
+ [
44
+ ("name", pa.string()),
45
+ ("value_type", pa.string()),
46
+ ("value_int", pa.int64()),
47
+ ("value_double", pa.float64()),
48
+ ("value_bool", pa.bool_()),
49
+ ("value_text", pa.string()),
50
+ ("value_timestamp", pa.timestamp("us", tz="UTC")),
51
+ ("value_json", pa.string()),
52
+ ("unit", pa.string()),
53
+ ("uut_pin", pa.string()),
54
+ ]
55
+ )
56
+ assert [f.name for f in _LANE_STRUCT] == list(LANE_FIELDS), (
57
+ "schemas._LANE_STRUCT drifted from _row_helpers.LANE_FIELDS"
58
+ )
59
+ _LANE_LIST = pa.list_(_LANE_STRUCT)
60
+
61
+ # Nested measurement struct — the at-rest representation of one measurement,
62
+ # carried in the vector row's ``measurements`` LIST. Field names must match
63
+ # ``_row_helpers.MEASUREMENT_STRUCT_FIELDS`` (guarded below). The daemon
64
+ # UNNESTs these into the flat measurement fact at ingest.
65
+ _MEASUREMENT_STRUCT = pa.struct(
66
+ [
67
+ ("name", pa.string()),
68
+ ("value", pa.float64()),
69
+ ("unit", pa.string()),
70
+ ("outcome", pa.string()),
71
+ ("timestamp", pa.timestamp("us", tz="UTC")),
72
+ ("limit_low", pa.float64()),
73
+ ("limit_high", pa.float64()),
74
+ ("limit_nominal", pa.float64()),
75
+ ("limit_comparator", pa.string()),
76
+ ("characteristic_id", pa.string()),
77
+ ("spec_ref", pa.string()),
78
+ ("uut_pin", pa.string()),
79
+ ("fixture_connection", pa.string()),
80
+ ("instrument_name", pa.string()),
81
+ ("instrument_resource", pa.string()),
82
+ ("instrument_channel", pa.string()),
83
+ ]
84
+ )
85
+ assert [f.name for f in _MEASUREMENT_STRUCT] == list(MEASUREMENT_STRUCT_FIELDS), (
86
+ "schemas._MEASUREMENT_STRUCT drifted from _row_helpers.MEASUREMENT_STRUCT_FIELDS"
87
+ )
88
+ _MEASUREMENT_LIST = pa.list_(_MEASUREMENT_STRUCT)
89
+
90
+ # Instrument inventory struct — the at-rest representation of one instrument
91
+ # connected during a run. Carried as a ``LIST<STRUCT>`` on every row
92
+ # (dense, self-describing). The daemon UNNESTs these into the flat
93
+ # ``instruments_materialized`` table for queries. Field names must match
94
+ # ``_row_helpers.INSTRUMENT_STRUCT_FIELDS`` (guarded below).
95
+ _INSTRUMENT_STRUCT = pa.struct(
96
+ [
97
+ ("name", pa.string()),
98
+ ("id", pa.string()),
99
+ ("driver", pa.string()),
100
+ ("resource", pa.string()),
101
+ ("protocol", pa.string()),
102
+ ("manufacturer", pa.string()),
103
+ ("model", pa.string()),
104
+ ("serial_number", pa.string()),
105
+ ("firmware", pa.string()),
106
+ ("cal_due", pa.string()),
107
+ ("cal_last", pa.string()),
108
+ ("cal_certificate", pa.string()),
109
+ ("cal_lab", pa.string()),
110
+ ("mocked", pa.bool_()),
111
+ ]
112
+ )
113
+ assert [f.name for f in _INSTRUMENT_STRUCT] == list(INSTRUMENT_STRUCT_FIELDS), (
114
+ "schemas._INSTRUMENT_STRUCT drifted from _row_helpers.INSTRUMENT_STRUCT_FIELDS"
115
+ )
116
+ _INSTRUMENT_LIST = pa.list_(_INSTRUMENT_STRUCT)
117
+
118
+ # Canonical row schema for the unified per-run parquet — a chronological
119
+ # telling of the run. Every row carries an explicit ``record_type``
120
+ # discriminator with one of three values:
121
+ # * ``record_type = 'run'`` — one row per run; run / UUT / station /
122
+ # environment context. Step / vector columns are NULL.
123
+ # * ``record_type = 'step'`` — one step-execution, keyed ``(step_path,
124
+ # step_retry, vector_outer_index)``; carries code identity + timing +
125
+ # rolled-up outcome, and its OWN ``inputs``/``outputs``/``measurements``
126
+ # (a non-looping step can have zero vector rows — the step row carries
127
+ # its own data, no synthesized scope vector). ``vector_index`` is always
128
+ # NULL on this row kind.
129
+ # * ``record_type = 'vector'`` — one condition-point execution carrier: a
130
+ # swept step's own sweep variant, or an in-body ``vectors`` loop
131
+ # iteration. Holds the ``inputs``/``outputs`` lanes and the nested
132
+ # ``measurements`` list for that execution; ``vector_index`` is its own
133
+ # 0..N position.
134
+ #
135
+ # ``inputs`` / ``outputs`` are nested ``LIST<STRUCT<lanes>>`` columns (see
136
+ # ``_LANE_STRUCT``), not wide ``in_*``/``out_*`` columns; the DuckDB daemon
137
+ # UNNESTs them into the honestly-named ``inputs``/``outputs`` EAV tables (one
138
+ # per role, no ``role`` column) for queries. ``measurements`` is a nested
139
+ # ``LIST<STRUCT>`` on the vector row; the daemon UNNESTs it into the flat
140
+ # measurement fact for queries.
141
+ RUN_ROW_SCHEMA = pa.schema(
142
+ [
143
+ # Discriminator — 'run', 'step', or 'vector'
144
+ ("record_type", pa.string()),
145
+ # Identity & timing
146
+ ("session_id", pa.string()),
147
+ ("run_id", pa.string()),
148
+ ("site_index", pa.int64()),
149
+ ("site_name", pa.string()),
150
+ ("run_started_at", pa.timestamp("us", tz="UTC")),
151
+ ("run_ended_at", pa.timestamp("us", tz="UTC")),
152
+ ("step_name", pa.string()),
153
+ ("step_index", pa.int64()),
154
+ ("step_path", pa.string()),
155
+ ("step_started_at", pa.timestamp("us", tz="UTC")),
156
+ ("step_ended_at", pa.timestamp("us", tz="UTC")),
157
+ ("step_node_id", pa.string()),
158
+ ("step_module", pa.string()),
159
+ ("step_file", pa.string()),
160
+ ("step_class", pa.string()),
161
+ ("step_function", pa.string()),
162
+ ("step_markers", pa.string()),
163
+ # step_retry: 0-based outer (item) retry — pytest-rerunfailures rerun
164
+ # count of this step. Distinct from vector_retry (the inner per-vector
165
+ # retry). Both stamp every execution so a rerun is a distinct row.
166
+ ("step_retry", pa.int64()),
167
+ ("vector_index", pa.int64()),
168
+ ("vector_outer_index", pa.int64()),
169
+ ("vector_retry", pa.int64()),
170
+ ("vector_started_at", pa.timestamp("us", tz="UTC")),
171
+ ("vector_ended_at", pa.timestamp("us", tz="UTC")),
172
+ # Who
173
+ ("operator_id", pa.string()),
174
+ ("operator_name", pa.string()),
175
+ # UUT
176
+ ("uut_serial_number", pa.string()),
177
+ ("uut_part_number", pa.string()),
178
+ ("uut_revision", pa.string()),
179
+ ("uut_lot_number", pa.string()),
180
+ # Part
181
+ ("part_id", pa.string()),
182
+ ("part_name", pa.string()),
183
+ ("part_revision", pa.string()),
184
+ # Station
185
+ ("station_id", pa.string()),
186
+ ("station_name", pa.string()),
187
+ ("station_type", pa.string()),
188
+ ("station_location", pa.string()),
189
+ ("station_hostname", pa.string()),
190
+ # Fixture
191
+ ("fixture_id", pa.string()),
192
+ # Test context
193
+ ("test_phase", pa.string()),
194
+ ("project_name", pa.string()),
195
+ ("git_commit", pa.string()),
196
+ ("git_branch", pa.string()),
197
+ ("git_remote", pa.string()),
198
+ # Rollup
199
+ ("step_outcome", pa.string()),
200
+ ("vector_outcome", pa.string()),
201
+ ("run_outcome", pa.string()),
202
+ # Environment traceability
203
+ ("python_version", pa.string()),
204
+ ("testerkit_version", pa.string()),
205
+ ("env_fingerprint", pa.string()),
206
+ # Dynamic attributes — nested EAV lanes (see _row_helpers). Names are
207
+ # values inside the structs, so there is no column explosion.
208
+ ("inputs", _LANE_LIST),
209
+ ("outputs", _LANE_LIST),
210
+ # Nested measurements on the vector row; the daemon UNNESTs these into
211
+ # the flat measurement fact at ingest.
212
+ ("measurements", _MEASUREMENT_LIST),
213
+ # Instrument inventory on every row (dense, self-describing); the daemon
214
+ # UNNESTs these into the flat ``instruments_materialized`` table.
215
+ ("instruments", _INSTRUMENT_LIST),
216
+ ],
217
+ # Stamp the schema itself with its version (like events' ``_IPC_SCHEMA``), so
218
+ # every table built with ``schema=RUN_ROW_SCHEMA`` carries the stamp by
219
+ # construction — the real write path overrides this metadata with the fuller
220
+ # ``_build_parquet_metadata`` dict (still schema_version), and fixtures that
221
+ # write directly are stamped automatically instead of silently unstamped.
222
+ metadata={b"schema_version": SCHEMA_VERSION.encode()},
223
+ )
224
+
225
+ _SCHEMA_DICT = {f.name: f.type for f in RUN_ROW_SCHEMA}
226
+
227
+
228
+ def _infer_type_from_value(value: Any) -> pa.DataType:
229
+ """Infer Arrow type from a Python value. None → string (safe default)."""
230
+ if value is None:
231
+ return pa.string()
232
+ if isinstance(value, bool):
233
+ return pa.bool_()
234
+ if isinstance(value, (int, float)):
235
+ return pa.float64()
236
+ if isinstance(value, datetime):
237
+ return pa.timestamp("us", tz="UTC")
238
+ if isinstance(value, list):
239
+ return pa.list_(pa.string())
240
+ return pa.string()
241
+
242
+
243
+ def _build_write_schema(rows: list[dict[str, Any]]) -> pa.Schema:
244
+ """Build complete Arrow schema: fixed canonical + dynamic columns.
245
+
246
+ Fixed columns (including the nested ``inputs``/``outputs`` lanes and
247
+ the ``instruments`` struct list) use ``RUN_ROW_SCHEMA`` types. Any other
248
+ stray column is inferred from its first non-None value. Passed to
249
+ ``pa.Table.from_pylist()`` so Arrow validates at construction time.
250
+
251
+ Single pass over rows to collect keys and first non-None values.
252
+ """
253
+ first_values: dict[str, Any] = {}
254
+ all_keys: set[str] = set()
255
+ for row in rows:
256
+ for k, v in row.items():
257
+ all_keys.add(k)
258
+ if k not in first_values and v is not None:
259
+ first_values[k] = v
260
+
261
+ fields: list[pa.Field] = []
262
+ used: set[str] = set()
263
+
264
+ # Fixed columns first, in canonical order
265
+ for field in RUN_ROW_SCHEMA:
266
+ if field.name in all_keys:
267
+ fields.append(field)
268
+ used.add(field.name)
269
+
270
+ # Remaining dynamic columns inferred from values
271
+ for key in sorted(all_keys - used):
272
+ fields.append(pa.field(key, _infer_type_from_value(first_values.get(key))))
273
+
274
+ # Carry the version stamp (as RUN_ROW_SCHEMA does), so tables built from this
275
+ # dynamic schema are stamped by construction. The real write path overrides
276
+ # with the fuller _build_parquet_metadata dict; direct writers are covered.
277
+ return pa.schema(fields, metadata={b"schema_version": SCHEMA_VERSION.encode()})
278
+
279
+
280
+ def table_from_rows(rows: list[dict[str, Any]], schema: pa.Schema) -> pa.Table:
281
+ """Build a PyArrow Table from row dicts against ``schema``.
282
+
283
+ Wraps ``pa.Table.from_pylist`` so a build failure carries row context.
284
+ """
285
+ try:
286
+ return pa.Table.from_pylist(rows, schema=schema)
287
+ except (pa.ArrowInvalid, pa.ArrowTypeError, pa.ArrowNotImplementedError) as exc:
288
+ raise pa.ArrowInvalid(f"Cannot build measurement table from rows: {exc}") from exc
@@ -0,0 +1,48 @@
1
+ """Internal event-subscriber registry — used by the ``testerkit export``
2
+ CLI replay path (csv/json/stdf/hdf5/tdms/mdf4 converters in
3
+ :mod:`testerkit.data.exporters`). The canonical run materializer
4
+ (parquet + DuckDB index) is owned by the runs daemon and does NOT
5
+ go through this registry.
6
+
7
+ Not a public extension protocol: third-party packages should not register
8
+ formats via entry points or any other mechanism. The set of supported
9
+ formats is fixed by the package and ships through ``testerkit export``.
10
+ """
11
+
12
+ import testerkit.data.exporters.csv_exporter # noqa: F401
13
+ import testerkit.data.exporters.json_exporter # noqa: F401
14
+ from testerkit.data.event_log import EventSubscriber
15
+ from testerkit.data.subscribers._base import get_subscriber_class, list_subscribers
16
+ from testerkit.data.subscribers._output_file import OutputFile
17
+ from testerkit.data.subscribers.replay import replay_to_subscriber
18
+
19
+ # Optional industry-format subscribers — registered via ``__init_subclass__``
20
+ # at import time when their respective extras are installed
21
+ # (``testerkit[stdf]``, ``[hdf5]``, ``[tdms]``, ``[mdf4]``). When the
22
+ # extra isn't installed the import fails; swallow so the rest of the
23
+ # package still loads. The CLI surfaces a "format not registered" error
24
+ # pointing at the right extra.
25
+ try:
26
+ import testerkit.data.exporters.stdf # noqa: F401
27
+ except ImportError:
28
+ pass
29
+ try:
30
+ import testerkit.data.exporters.hdf5 # noqa: F401
31
+ except ImportError:
32
+ pass
33
+ try:
34
+ import testerkit.data.exporters.tdms # noqa: F401
35
+ except ImportError:
36
+ pass
37
+ try:
38
+ import testerkit.data.exporters.mdf4 # noqa: F401
39
+ except ImportError:
40
+ pass
41
+
42
+ __all__ = [
43
+ "EventSubscriber",
44
+ "OutputFile",
45
+ "get_subscriber_class",
46
+ "list_subscribers",
47
+ "replay_to_subscriber",
48
+ ]
@@ -0,0 +1,20 @@
1
+ """Subscriber lookup functions.
2
+
3
+ Registration is automatic via ``EventSubscriber.__init_subclass__``.
4
+ Built-in subscribers are imported in ``__init__.py``.
5
+ Third-party plugins are loaded via entry points.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from testerkit.data.event_log import EventSubscriber
11
+
12
+
13
+ def get_subscriber_class(format_name: str) -> type[EventSubscriber] | None:
14
+ """Look up a subscriber class by format name."""
15
+ return EventSubscriber._registry.get(format_name)
16
+
17
+
18
+ def list_subscribers() -> list[str]:
19
+ """Return sorted list of all registered subscriber format names."""
20
+ return sorted(EventSubscriber._registry)
@@ -0,0 +1,25 @@
1
+ """OutputFile descriptor — metadata about a file produced by a subscriber.
2
+
3
+ Transports use ``format`` to decide how to ship the file. The ``on_output``
4
+ callback receives an ``OutputFile`` after each successful write.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class OutputFile:
15
+ """Describes a file produced by a subscriber.
16
+
17
+ Attributes:
18
+ path: Absolute path to the written file.
19
+ format: Format name (``"parquet"``, ``"csv"``, ``"stdf"``, etc.).
20
+ run_id: Run ID associated with the file, if known.
21
+ """
22
+
23
+ path: Path
24
+ format: str
25
+ run_id: str | None = None
@@ -0,0 +1,55 @@
1
+ """Replay stored events through a subscriber — the post-hoc bridge.
2
+
3
+ This replaces the old ``reconstruct_test_run → exporter.export()`` chain.
4
+ Any subscriber can be fed stored events to produce its output format
5
+ after the fact, without needing a live test session.
6
+
7
+ Usage::
8
+
9
+ from testerkit.data.subscribers.replay import replay_to_subscriber
10
+ from testerkit.data.exporters.json_exporter import JsonSubscriber
11
+
12
+ sub = JsonSubscriber(output_dir=Path("out"))
13
+ replay_to_subscriber(sub, event_dicts)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import warnings
19
+ from typing import Any
20
+
21
+ from pydantic import TypeAdapter, ValidationError
22
+
23
+ from testerkit.data.event_log import EventSubscriber
24
+ from testerkit.data.events import Event
25
+
26
+ _event_adapter: TypeAdapter[Any] = TypeAdapter(Event)
27
+
28
+
29
+ def replay_to_subscriber(
30
+ subscriber: EventSubscriber,
31
+ events: list[dict[str, Any]],
32
+ ) -> None:
33
+ """Deserialize event dicts and feed them through a subscriber.
34
+
35
+ Args:
36
+ subscriber: An ``EventSubscriber`` instance (has ``open``,
37
+ ``on_event``, ``close``, and ``event_types``).
38
+ events: Raw event dicts, e.g. from ``EventLog.events()``
39
+ or ``EventStore.events(session_id=...)``.
40
+ """
41
+ subscriber.open()
42
+ try:
43
+ for raw in events:
44
+ try:
45
+ event = _event_adapter.validate_python(raw)
46
+ except ValidationError:
47
+ warnings.warn(
48
+ f"Skipping invalid event: {raw.get('event_type', '?')}",
49
+ stacklevel=2,
50
+ )
51
+ continue
52
+ if type(event) in subscriber.event_types:
53
+ subscriber.on_event(event)
54
+ finally:
55
+ subscriber.close()
@@ -0,0 +1,84 @@
1
+ """Environment snapshot for software traceability.
2
+
3
+ Captures the Python runtime environment (version, OS, top-level dependencies)
4
+ at test start. Full package resolution is tracked by ``uv.lock`` in git;
5
+ we store only the hash here for correlation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import logging
12
+ import platform
13
+ from pathlib import Path
14
+
15
+ from pydantic import BaseModel
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class EnvironmentSnapshot(BaseModel):
21
+ """Snapshot of the software environment at test time."""
22
+
23
+ python_version: str
24
+ os_name: str
25
+ os_version: str
26
+ platform_machine: str
27
+ testerkit_version: str
28
+ dependencies: list[str]
29
+ lockfile_hash: str | None = None
30
+
31
+
32
+ def _read_top_level_deps() -> list[str]:
33
+ """Read direct dependencies from pyproject.toml [project.dependencies]."""
34
+ pyproject = Path("pyproject.toml")
35
+ if not pyproject.exists():
36
+ return []
37
+ try:
38
+ import tomllib
39
+ except ModuleNotFoundError:
40
+ import tomli as tomllib # type: ignore[no-redef]
41
+
42
+ try:
43
+ data = tomllib.loads(pyproject.read_text())
44
+ return sorted(data.get("project", {}).get("dependencies", []))
45
+ except Exception as exc:
46
+ logger.debug("Could not parse pyproject.toml: %s", exc)
47
+ return []
48
+
49
+
50
+ def _hash_file(path: Path) -> str | None:
51
+ """SHA-256 prefix of a file, or None if missing/unreadable."""
52
+ try:
53
+ if path.exists():
54
+ return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
55
+ except OSError as exc:
56
+ logger.debug("Could not read %s: %s", path, exc)
57
+ return None
58
+
59
+
60
+ def capture_environment() -> EnvironmentSnapshot:
61
+ """Capture current environment snapshot.
62
+
63
+ Stores top-level dependencies from pyproject.toml (the ones you chose),
64
+ not the full transitive dependency tree. ``lockfile_hash`` covers the
65
+ exact resolved versions; ``pyproject_hash`` covers the intent.
66
+ """
67
+ from testerkit import __version__
68
+
69
+ # Hash lockfile (uv.lock preferred, fallback to others)
70
+ lockfile_hash = None
71
+ for name in ("uv.lock", "poetry.lock", "Pipfile.lock", "requirements.txt"):
72
+ lockfile_hash = _hash_file(Path(name))
73
+ if lockfile_hash:
74
+ break
75
+
76
+ return EnvironmentSnapshot(
77
+ python_version=platform.python_version(),
78
+ os_name=platform.system(),
79
+ os_version=platform.release(),
80
+ platform_machine=platform.machine(),
81
+ testerkit_version=__version__,
82
+ dependencies=_read_top_level_deps(),
83
+ lockfile_hash=lockfile_hash,
84
+ )