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,524 @@
1
+ """Filter facets and field-reference types for the parametric viewer.
2
+
3
+ The ``/explore`` page lets users scope cross-run measurement queries by
4
+ filtering on columns of the ``measurements`` view. This module is the
5
+ single source of truth for *which columns are facetable* and *what kind
6
+ of facet each column wants* — closed enum sets vs open string sets vs
7
+ date ranges.
8
+
9
+ Closed sets (``Outcome``, ``Comparator``) come straight from the
10
+ Pydantic models in ``testerkit.data.models`` / ``testerkit.models.enums`` —
11
+ no DB query needed; the universe is known at import time. Open sets
12
+ (``part_id``, ``station_id``, ``uut_serial_number``, ``step_name``,
13
+ ``measurement_name``, ``test_phase``) require a ``SELECT DISTINCT``
14
+ against the current filter set so the dropdowns reflect what the user
15
+ can actually pick from given their other selections.
16
+
17
+ The Pydantic types here also describe every analytics result row that
18
+ crosses a boundary — ``ParametricRow`` / ``HistogramRow`` /
19
+ ``FacetOption`` / ``SummaryCounts`` / ``FilterSet``. No
20
+ ``dict[str, Any]`` enters or exits ``MeasurementsQuery`` with these in
21
+ place.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from datetime import date, datetime
27
+ from enum import StrEnum
28
+
29
+ from pydantic import BaseModel, Field, model_validator
30
+
31
+ from testerkit.data.models import Outcome
32
+ from testerkit.models.enums import Comparator
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Field identity — role + name + optional value_type
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ class FieldRole(StrEnum):
40
+ """Which role a recorded field plays in a measurement vector."""
41
+
42
+ INPUT = "input"
43
+ OUTPUT = "output"
44
+ MEASUREMENT = "measurement"
45
+
46
+
47
+ class FieldRef(BaseModel):
48
+ """Reference to a named field, identified by (role, name).
49
+
50
+ Use the classmethod constructors for everyday code::
51
+
52
+ FieldRef.measurement("v_rail")
53
+ FieldRef.output("v_rail")
54
+ FieldRef.input("vin")
55
+
56
+ The plain constructor also works and is used at wire boundaries::
57
+
58
+ FieldRef(role=FieldRole.OUTPUT, name="v_rail")
59
+ FieldRef(role="output", name="v_rail") # FieldRole coerces from str
60
+
61
+ ``value_type`` is an open string (not an enum) — it reflects the
62
+ stored tag (e.g. ``"scalar:float"``, ``"scalar:int"``) and is only
63
+ required when a (role, name) pair has mixed value_types in scope.
64
+ """
65
+
66
+ role: FieldRole
67
+ name: str
68
+ value_type: str | None = None
69
+
70
+ @classmethod
71
+ def input(cls, name: str, value_type: str | None = None) -> FieldRef:
72
+ return cls(role=FieldRole.INPUT, name=name, value_type=value_type)
73
+
74
+ @classmethod
75
+ def output(cls, name: str, value_type: str | None = None) -> FieldRef:
76
+ return cls(role=FieldRole.OUTPUT, name=name, value_type=value_type)
77
+
78
+ @classmethod
79
+ def measurement(cls, name: str, value_type: str | None = None) -> FieldRef:
80
+ return cls(role=FieldRole.MEASUREMENT, name=name, value_type=value_type)
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # describe_columns() result models
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class FixedColumnDescriptor(BaseModel):
89
+ """One plottable fixed column from the measurements view."""
90
+
91
+ name: str
92
+ column_type: str
93
+
94
+
95
+ class DynamicFieldDescriptor(BaseModel):
96
+ """One role-keyed field discovered in the catalog."""
97
+
98
+ role: FieldRole
99
+ name: str
100
+ value_types: list[str]
101
+
102
+
103
+ class ColumnSchema(BaseModel):
104
+ """Return type of ``MeasurementsQuery.describe_columns()``."""
105
+
106
+ fixed: list[FixedColumnDescriptor]
107
+ fields: list[DynamicFieldDescriptor]
108
+
109
+
110
+ class FacetKind(StrEnum):
111
+ """How a facet's options are sourced and displayed in the UI."""
112
+
113
+ ENUM = "enum"
114
+ STRING = "string"
115
+ DATE = "date"
116
+
117
+
118
+ class FacetSpec(BaseModel):
119
+ """Self-describing definition of one filter facet.
120
+
121
+ ``column`` is the facet's storage column — the ``FilterSet`` dict key,
122
+ and the value that's filtered and enumerated. Facets name the real
123
+ storage column (read follows storage), so the same key means the same
124
+ thing on every page. ``param`` gives an optional friendlier URL query
125
+ key (e.g. ``?part=`` for ``uut_part_number``); empty ``param`` → URL
126
+ key is ``column``. ``expr`` lets a facet filter/enumerate on a SQL
127
+ expression instead of the bare column (e.g. ``test_phase`` labels its
128
+ NULLs ``'unknown'``); empty ``expr`` → ``column`` directly.
129
+ """
130
+
131
+ column: str
132
+ kind: FacetKind
133
+ enum_class: type[StrEnum] | None = None
134
+ label: str
135
+ description: str = ""
136
+ param: str = ""
137
+ expr: str = ""
138
+
139
+ @property
140
+ def url_key(self) -> str:
141
+ """The query-string key this facet reads/writes (``param`` or ``column``)."""
142
+ return self.param or self.column
143
+
144
+ @model_validator(mode="after")
145
+ def _enum_class_required_for_enum_kind(self) -> FacetSpec:
146
+ if self.kind is FacetKind.ENUM and self.enum_class is None:
147
+ raise ValueError(f"{self.column}: ENUM facet requires enum_class")
148
+ if self.kind is not FacetKind.ENUM and self.enum_class is not None:
149
+ raise ValueError(f"{self.column}: enum_class only valid for ENUM facets")
150
+ return self
151
+
152
+
153
+ class FacetOption(BaseModel):
154
+ """One pickable value within a facet, with the row count it covers."""
155
+
156
+ value: str
157
+ count: int
158
+
159
+
160
+ class SummaryCounts(BaseModel):
161
+ """Cardinality stats for the cardinality badge — single query result."""
162
+
163
+ total_rows: int
164
+ distinct_runs: int
165
+ distinct_measurements: int
166
+ distinct_parts: int
167
+
168
+
169
+ class ParametricRow(BaseModel):
170
+ """One long-format row from a scatter / line / bar parametric query.
171
+
172
+ ``x`` widens to accept datetime / date because measurements view
173
+ columns include timestamps; the chart layer coerces these to
174
+ epoch ms for ECharts ``time`` axes. ``group`` is always coerced to
175
+ str so numeric EAV fields used as group_by axes render as labels.
176
+ """
177
+
178
+ x: float | str | datetime | date | None = None
179
+ y: float
180
+ group: str = ""
181
+
182
+ @model_validator(mode="before")
183
+ @classmethod
184
+ def _coerce_group(cls, data: object) -> object:
185
+ if isinstance(data, dict) and "group" in data and data["group"] is not None:
186
+ data = dict(data)
187
+ data["group"] = str(data["group"])
188
+ return data
189
+
190
+
191
+ class HistogramRow(BaseModel):
192
+ """One bin in a histogram result. ``x`` is the bin midpoint."""
193
+
194
+ bin: int
195
+ x: float
196
+ y: int
197
+ group: str = ""
198
+
199
+
200
+ class YieldRow(BaseModel):
201
+ """One row from :meth:`MeasurementsQuery.yield_summary` or
202
+ :meth:`MeasurementsQuery.yield_overall`."""
203
+
204
+ part: str
205
+ station: str
206
+ phase: str
207
+ period: object # date from DuckDB — typed as object to accept date/str
208
+ total_runs: int
209
+ passed: int
210
+ failed: int
211
+ errored: int
212
+ unique_serials: int
213
+ first_pass_total: int
214
+ first_pass_passed: int
215
+ final_passed: int
216
+ avg_duration_s: float | None = None
217
+ p95_duration_s: float | None = None
218
+ min_duration_s: float | None = None
219
+ max_duration_s: float | None = None
220
+ # Quality metrics — rty from step records; dpmo from measurement records; dppm from runs.
221
+ # rty is None when no step records exist in the matching scope.
222
+ rty: float | None = None # Rolled Throughput Yield — product of per-step FPY
223
+ dpmo: float | None = None # Defects Per Million Opportunities (measurement-level)
224
+ dppm: float | None = None # Defective Parts Per Million (run-level)
225
+
226
+
227
+ class ParetoRow(BaseModel):
228
+ """One row from :meth:`MeasurementsQuery.pareto`.
229
+
230
+ Represents one (part, station, step, measurement) failure bucket.
231
+ """
232
+
233
+ part: str
234
+ station: str
235
+ step_name: str | None = None
236
+ measurement_name: str | None = None
237
+ total_count: int
238
+ fail_count: int
239
+ fail_rate: float | None = None
240
+
241
+ def to_bucket_dict(self) -> dict[str, object]:
242
+ """Normalize to the shared failure-pareto display shape."""
243
+ return {
244
+ "bucket": f"{self.step_name or ''}: {self.measurement_name or ''}",
245
+ "failed_count": self.fail_count,
246
+ "total": self.total_count,
247
+ "fail_rate_pct": self.fail_rate,
248
+ }
249
+
250
+
251
+ class PpkRow(BaseModel):
252
+ """One row from :meth:`MeasurementsQuery.ppk` — one homogeneous population:
253
+ one (part, station, measurement_name, characteristic_id, uut_pin) sharing a
254
+ single spec limit pair. Splitting on characteristic / pin / limits keeps Ppk
255
+ over a single distribution instead of pooling differing specs under a shared
256
+ name."""
257
+
258
+ part: str
259
+ station: str
260
+ measurement_name: str
261
+ characteristic_id: str | None = None
262
+ uut_pin: str | None = None
263
+ n: int
264
+ mean: float | None = None
265
+ sigma: float | None = None
266
+ lsl: float | None = None
267
+ usl: float | None = None
268
+ pp: float | None = None
269
+ ppk: float | None = None
270
+
271
+
272
+ class TrendRow(BaseModel):
273
+ """One row from :meth:`MeasurementsQuery.trend` — one (part, station, phase, period)."""
274
+
275
+ part: str
276
+ station: str
277
+ phase: str
278
+ period: object # date from DuckDB — typed as object to accept date/str
279
+ total: int
280
+ passed: int
281
+ yield_pct: float | None = None
282
+
283
+
284
+ class RetestRow(BaseModel):
285
+ """One row from :meth:`MeasurementsQuery.retest` — one (part, station, phase, period)."""
286
+
287
+ part: str
288
+ station: str
289
+ phase: str
290
+ period: object # date from DuckDB — typed as object to accept date/str
291
+ total_serials: int
292
+ retested_count: int
293
+ retest_rate: float | None = None
294
+ avg_retries: float | None = None
295
+
296
+
297
+ class TimeLossRow(BaseModel):
298
+ """One row from :meth:`MeasurementsQuery.time_loss` — one (part, station, phase, period)."""
299
+
300
+ part: str
301
+ station: str
302
+ phase: str
303
+ period: object # date from DuckDB — typed as object to accept date/str
304
+ total_time_s: float | None = None
305
+ pass_time_s: float | None = None
306
+ fail_time_s: float | None = None
307
+ error_time_s: float | None = None
308
+
309
+
310
+ class LimitBandRow(BaseModel):
311
+ """One point of a measurement's limit envelope, keyed by the chart's X.
312
+
313
+ The chart layer draws ``low`` and ``high`` as step lines against the
314
+ same X axis as the data — a staircase when limits are condition-indexed,
315
+ a flat band when they don't vary.
316
+ """
317
+
318
+ x: float | str | datetime | date | None = None
319
+ low: float | None = None
320
+ high: float | None = None
321
+
322
+
323
+ class FilterSet(BaseModel):
324
+ """Current filter state — URL-shareable, validated against ``MEASUREMENT_FACETS``.
325
+
326
+ Splits enum / string filters because they take different SQL
327
+ treatment downstream: enum filters can be passed straight through
328
+ (their values are the enum.value strings) while string filters
329
+ need cross-filtering when populating their own DISTINCT options.
330
+ """
331
+
332
+ string_filters: dict[str, list[str]] = Field(default_factory=dict)
333
+ enum_filters: dict[str, list[str]] = Field(default_factory=dict)
334
+ since: date | None = None
335
+ until: date | None = None
336
+
337
+ @model_validator(mode="after")
338
+ def _validate_against_registry(self) -> FilterSet:
339
+ for col in self.string_filters:
340
+ spec = _spec_by_column(col)
341
+ if spec is None or spec.kind is not FacetKind.STRING:
342
+ raise ValueError(f"unknown or non-string facet column: {col}")
343
+ for col in self.enum_filters:
344
+ spec = _spec_by_column(col)
345
+ if spec is None or spec.kind is not FacetKind.ENUM:
346
+ raise ValueError(f"unknown or non-enum facet column: {col}")
347
+ return self
348
+
349
+ def is_empty(self) -> bool:
350
+ return (
351
+ not self.string_filters
352
+ and not self.enum_filters
353
+ and self.since is None
354
+ and self.until is None
355
+ )
356
+
357
+ def to_url_params(self) -> list[tuple[str, str]]:
358
+ """Encode as repeated query keys for ``urlencode([(k, v), ...])``.
359
+
360
+ Each filter's URL key is its facet's ``url_key`` (the operator-
361
+ facing ``param`` when set, else the column).
362
+ """
363
+ params: list[tuple[str, str]] = []
364
+ for filters in (self.string_filters, self.enum_filters):
365
+ for col, values in filters.items():
366
+ spec = _spec_by_column(col)
367
+ key = spec.url_key if spec is not None else col
368
+ for v in values:
369
+ params.append((key, v))
370
+ if self.since is not None:
371
+ params.append(("since", self.since.isoformat()))
372
+ if self.until is not None:
373
+ params.append(("until", self.until.isoformat()))
374
+ return params
375
+
376
+ @classmethod
377
+ def from_url_params(cls, params: dict[str, list[str]]) -> FilterSet:
378
+ """Decode a query-string dict — unknown columns are dropped silently.
379
+
380
+ We drop rather than raise so a stale URL gracefully degrades
381
+ to a partially-empty filter rather than a 500. Callers can
382
+ distinguish "user typed garbage" from "schema changed under us"
383
+ by comparing the input keys to ``MEASUREMENT_FACETS``.
384
+ """
385
+ string_filters: dict[str, list[str]] = {}
386
+ enum_filters: dict[str, list[str]] = {}
387
+ since: date | None = None
388
+ until: date | None = None
389
+ for key, values in params.items():
390
+ if key == "since" and values:
391
+ try:
392
+ since = date.fromisoformat(values[0])
393
+ except ValueError:
394
+ pass
395
+ continue
396
+ if key == "until" and values:
397
+ try:
398
+ until = date.fromisoformat(values[0])
399
+ except ValueError:
400
+ pass
401
+ continue
402
+ spec = _spec_by_url_key(key)
403
+ if spec is None:
404
+ continue
405
+ if spec.kind is FacetKind.STRING:
406
+ string_filters[spec.column] = list(values)
407
+ elif spec.kind is FacetKind.ENUM:
408
+ assert spec.enum_class is not None
409
+ allowed = {m.value for m in spec.enum_class.__members__.values()}
410
+ enum_filters[spec.column] = [v for v in values if v in allowed]
411
+ return cls(
412
+ string_filters=string_filters,
413
+ enum_filters=enum_filters,
414
+ since=since,
415
+ until=until,
416
+ )
417
+
418
+
419
+ # ---------------------------------------------------------------------------
420
+ # The registry — single source of truth for facetable measurement columns.
421
+ # ---------------------------------------------------------------------------
422
+
423
+ MEASUREMENT_FACETS: list[FacetSpec] = [
424
+ FacetSpec(
425
+ column="run_outcome",
426
+ kind=FacetKind.ENUM,
427
+ enum_class=Outcome,
428
+ label="Run outcome",
429
+ description="Did the run pass overall?",
430
+ ),
431
+ FacetSpec(
432
+ column="measurement_outcome",
433
+ kind=FacetKind.ENUM,
434
+ enum_class=Outcome,
435
+ label="Measurement outcome",
436
+ description="Did this measurement meet its limit?",
437
+ ),
438
+ FacetSpec(
439
+ column="limit_comparator",
440
+ kind=FacetKind.ENUM,
441
+ enum_class=Comparator,
442
+ label="Limit comparator",
443
+ description="How the measurement is checked against its limits",
444
+ ),
445
+ FacetSpec(
446
+ column="uut_part_number",
447
+ param="part",
448
+ kind=FacetKind.STRING,
449
+ label="Part",
450
+ description="Manufacturer part number of the UUT (scanned, or from the part config)",
451
+ ),
452
+ FacetSpec(
453
+ column="station_hostname",
454
+ param="station",
455
+ kind=FacetKind.STRING,
456
+ label="Station",
457
+ description="Hostname of the station that ran the test",
458
+ ),
459
+ FacetSpec(
460
+ column="test_phase",
461
+ param="phase",
462
+ kind=FacetKind.STRING,
463
+ label="Test phase",
464
+ description="Production / qual / development",
465
+ expr="COALESCE(test_phase, 'unknown')",
466
+ ),
467
+ FacetSpec(
468
+ column="step_name",
469
+ kind=FacetKind.STRING,
470
+ label="Step",
471
+ description="Test step name",
472
+ ),
473
+ FacetSpec(
474
+ column="measurement_name",
475
+ kind=FacetKind.STRING,
476
+ label="Measurement",
477
+ description="Named measurement (e.g. vout)",
478
+ ),
479
+ FacetSpec(
480
+ column="uut_serial_number",
481
+ kind=FacetKind.STRING,
482
+ label="UUT serial",
483
+ description="Specific unit",
484
+ ),
485
+ FacetSpec(
486
+ column="run_started_at",
487
+ kind=FacetKind.DATE,
488
+ label="Date range",
489
+ description="When the run started",
490
+ ),
491
+ ]
492
+
493
+
494
+ _BY_COLUMN: dict[str, FacetSpec] = {f.column: f for f in MEASUREMENT_FACETS}
495
+
496
+
497
+ def _spec_by_column(column: str) -> FacetSpec | None:
498
+ """Look up a facet by column name, or ``None`` if not in the registry."""
499
+ return _BY_COLUMN.get(column)
500
+
501
+
502
+ _BY_URL_KEY: dict[str, FacetSpec] = {f.url_key: f for f in MEASUREMENT_FACETS}
503
+
504
+
505
+ def _spec_by_url_key(key: str) -> FacetSpec | None:
506
+ """Look up a facet by its URL query key (``param`` or ``column``)."""
507
+ return _BY_URL_KEY.get(key)
508
+
509
+
510
+ def facet_sql_expr(column: str) -> str | None:
511
+ """The SQL expression a facet filters / enumerates on.
512
+
513
+ Returns the facet's ``expr`` (e.g. the operator-facing ``COALESCE``
514
+ for Part / Station / Test phase) when it has one, else ``None`` — in
515
+ which case callers quote ``column`` as a plain identifier. Lives here
516
+ so the column→expression mapping has one source of truth.
517
+ """
518
+ spec = _spec_by_column(column)
519
+ return spec.expr if (spec is not None and spec.expr) else None
520
+
521
+
522
+ def facets_by_kind(kind: FacetKind) -> list[FacetSpec]:
523
+ """Return every facet of the given kind, in registry order."""
524
+ return [f for f in MEASUREMENT_FACETS if f.kind is kind]