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,414 @@
1
+ """Serialization registry for FileStore artifacts (build items 12 + 13).
2
+
3
+ A single source of truth for "how does this value land on disk?"
4
+ Both :meth:`FileStore.write` and the legacy ``save_ref_to_dir`` helper
5
+ use this registry so the dispatch logic lives in one place and the
6
+ MIME / extension convention is the same regardless of which file
7
+ layout the caller writes to.
8
+
9
+ Each registered handler carries:
10
+
11
+ - ``extension``: file extension including the dot (e.g. ``".npz"``)
12
+ - ``mime``: MIME type per the TesterKit convention table (item 13)
13
+ - ``write(value, dest)``: callable that writes ``value`` to ``dest``
14
+
15
+ Built-in handlers (in priority order — first match wins):
16
+
17
+ - ``Path`` → copied via :func:`shutil.copy`; extension preserved
18
+ from source (default ``.bin``); MIME defaults to
19
+ ``application/octet-stream`` (the user is presumed to know the
20
+ payload's real MIME and can override via ``attributes``)
21
+ - ``Waveform`` → ``.npz`` via ``np.savez``; MIME
22
+ ``application/x-numpy-npz``. Falls back to ``.json`` + Pydantic
23
+ dump when numpy isn't importable.
24
+ - ``bytes`` → ``.bin``; MIME ``application/octet-stream``
25
+ - Pydantic ``BaseModel`` (anything with ``model_dump_json``) →
26
+ ``.json``; MIME ``application/json``
27
+ - numpy ``ndarray`` (anything with ``tolist`` + ``dtype``) →
28
+ ``.npy`` via ``np.save``; MIME ``application/x-numpy-npy``.
29
+ JSON fallback when numpy is unavailable.
30
+
31
+ Opportunistic handlers (registered only when the library is
32
+ importable):
33
+
34
+ - ``PIL.Image.Image`` → ``.png``; MIME ``image/png``
35
+ - ``pandas.DataFrame`` → ``.parquet``; MIME
36
+ ``application/vnd.apache.parquet``
37
+
38
+ Custom types may either:
39
+
40
+ - expose a ``testerkit_serialize(dest: Path) -> Path`` method (the
41
+ serializer protocol — preferred when the object knows its own
42
+ format), OR
43
+ - be registered explicitly via :func:`register_serializer`.
44
+
45
+ Pickle is the **last-resort fallback** for any value the registry
46
+ can't match. It emits a ``RuntimeWarning`` naming the type so
47
+ callers see what they should be registering a handler for; the
48
+ warning is what nudges the codebase toward typed serializers
49
+ rather than silent pickle bloat.
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import importlib.util as _ilu
55
+ import json
56
+ import pickle
57
+ import shutil
58
+ import warnings
59
+ from collections.abc import Callable
60
+ from pathlib import Path
61
+ from typing import Any, NamedTuple
62
+
63
+ from testerkit.data.models import Waveform, XYData
64
+
65
+ try:
66
+ HAS_NUMPY = _ilu.find_spec("numpy") is not None
67
+ except Exception: # pragma: no cover - defensive
68
+ HAS_NUMPY = False
69
+
70
+
71
+ class Serializer(NamedTuple):
72
+ """One entry in the registry: how a value type lands on disk."""
73
+
74
+ extension: str
75
+ mime: str
76
+ write: Callable[[Any, Path], None]
77
+
78
+
79
+ # The registry is a list of (predicate, serializer) pairs. Predicate is a
80
+ # callable so we can use ``isinstance`` checks (typical case) or arbitrary
81
+ # duck-typing (numpy-array detection without importing numpy at module
82
+ # load time). First match wins; user registrations are inserted at the
83
+ # front so they shadow built-ins.
84
+ _Registry = list[tuple[Callable[[Any], bool], Serializer]]
85
+ _registry: _Registry = []
86
+
87
+
88
+ # --------------------------------------------------------------------- #
89
+ # Built-in writers #
90
+ # --------------------------------------------------------------------- #
91
+
92
+
93
+ def _write_path(value: Any, dest: Path) -> None:
94
+ shutil.copy(value, dest)
95
+
96
+
97
+ def _write_bytes(value: Any, dest: Path) -> None:
98
+ dest.write_bytes(value)
99
+
100
+
101
+ def _write_basemodel_json(value: Any, dest: Path) -> None:
102
+ dest.write_text(value.model_dump_json())
103
+
104
+
105
+ def _write_waveform(value: Waveform, dest: Path) -> None:
106
+ if HAS_NUMPY:
107
+ import numpy as np
108
+
109
+ # t0 is a datetime (or None). np.savez can't store datetimes
110
+ # directly — serialize as an ISO-8601 string when set, empty
111
+ # string when None. Readers must round-trip through
112
+ # ``datetime.fromisoformat``.
113
+ t0_iso = value.t0.isoformat() if value.t0 is not None else ""
114
+ np.savez(dest, Y=value.Y, t0=t0_iso, dt=value.dt, **value.attributes)
115
+ else:
116
+ dest.write_text(value.model_dump_json())
117
+
118
+
119
+ def _write_xydata(value: XYData, dest: Path) -> None:
120
+ """Item 15: pack paired x/y arrays into a single ``.npz``.
121
+
122
+ Optional unit / names land as scalar string entries in the same
123
+ archive so a reader (UI plot, materializer ref-deref) can
124
+ reconstruct axis labels without a sidecar lookup. Only set keys
125
+ are written — readers should treat absent keys as None.
126
+ """
127
+ if HAS_NUMPY:
128
+ import numpy as np
129
+
130
+ kwargs: dict[str, Any] = {"x": value.x, "y": value.y}
131
+ if value.x_unit is not None:
132
+ kwargs["x_unit"] = value.x_unit
133
+ if value.y_unit is not None:
134
+ kwargs["y_unit"] = value.y_unit
135
+ if value.x_name is not None:
136
+ kwargs["x_name"] = value.x_name
137
+ if value.y_name is not None:
138
+ kwargs["y_name"] = value.y_name
139
+ np.savez(dest, **kwargs)
140
+ else:
141
+ dest.write_text(value.model_dump_json())
142
+
143
+
144
+ def _write_ndarray(value: Any, dest: Path) -> None:
145
+ if HAS_NUMPY:
146
+ import numpy as np
147
+
148
+ np.save(dest, value)
149
+ else:
150
+ dest.write_text(json.dumps(value.tolist()))
151
+
152
+
153
+ def _write_pil_image(value: Any, dest: Path) -> None:
154
+ value.save(dest, format="PNG")
155
+
156
+
157
+ def _write_pandas_dataframe(value: Any, dest: Path) -> None:
158
+ value.to_parquet(dest)
159
+
160
+
161
+ def _write_arrow_table(value: Any, dest: Path) -> None:
162
+ """Write a pyarrow Table as an Arrow IPC stream file.
163
+
164
+ Used by the materializer's channel-data preservation flow
165
+ (item 1d) — preserves a channel's Arrow rows verbatim under
166
+ FileStore.
167
+ """
168
+ import pyarrow.ipc as ipc
169
+
170
+ writer = ipc.new_stream(dest, value.schema)
171
+ writer.write_table(value)
172
+ writer.close()
173
+
174
+
175
+ def _write_pickle(value: Any, dest: Path) -> None:
176
+ with open(dest, "wb") as f:
177
+ pickle.dump(value, f)
178
+
179
+
180
+ # --------------------------------------------------------------------- #
181
+ # Built-in registration #
182
+ # --------------------------------------------------------------------- #
183
+
184
+
185
+ def _waveform_ext_and_mime() -> tuple[str, str]:
186
+ """Waveform extension + MIME depends on numpy availability."""
187
+ if HAS_NUMPY:
188
+ return ".npz", "application/x-numpy-npz"
189
+ return ".json", "application/json"
190
+
191
+
192
+ def _ndarray_ext_and_mime() -> tuple[str, str]:
193
+ if HAS_NUMPY:
194
+ return ".npy", "application/x-numpy-npy"
195
+ return ".json", "application/json"
196
+
197
+
198
+ def _is_pil_image(value: Any) -> bool:
199
+ # Duck-type without importing PIL at module load.
200
+ if not hasattr(value, "save"):
201
+ return False
202
+ mod = type(value).__module__
203
+ return mod.startswith("PIL.") or mod == "PIL"
204
+
205
+
206
+ def _is_pandas_dataframe(value: Any) -> bool:
207
+ if not hasattr(value, "to_parquet"):
208
+ return False
209
+ mod = type(value).__module__
210
+ return mod.startswith("pandas.")
211
+
212
+
213
+ def _is_ndarray(value: Any) -> bool:
214
+ """True for numpy-array-like values without importing numpy."""
215
+ return hasattr(value, "tolist") and hasattr(value, "dtype")
216
+
217
+
218
+ def _is_basemodel(value: Any) -> bool:
219
+ """Pydantic BaseModel ducktype — any model with model_dump_json."""
220
+ return hasattr(value, "model_dump_json")
221
+
222
+
223
+ def _is_arrow_table(value: Any) -> bool:
224
+ """True for pyarrow Table-shaped values (item 1d)."""
225
+ # Duck-type rather than ``isinstance(value, pa.Table)`` so the
226
+ # check is cheap when pyarrow isn't already loaded by the call
227
+ # site. The materializer's preservation path always has pyarrow
228
+ # imported, but the registry itself shouldn't be transitively
229
+ # dependent.
230
+ mod = type(value).__module__
231
+ return mod.startswith("pyarrow.") and type(value).__name__ == "Table"
232
+
233
+
234
+ def _register_builtins() -> None:
235
+ """Register the built-in handlers in priority order.
236
+
237
+ The order matters because :func:`find_serializer` returns the
238
+ first match — types that are subclasses of more general types
239
+ must come first.
240
+
241
+ Predicate order:
242
+
243
+ 1. ``Path`` — distinct from bytes; copy-with-suffix
244
+ 2. ``Waveform`` — distinct from BaseModel (also a BaseModel
245
+ subclass; needs to land as .npz, not .json)
246
+ 3. ``XYData`` — distinct from BaseModel (item 15; lands as
247
+ .npz paired arrays, not .json)
248
+ 4. ``bytes`` — raw payload
249
+ 5. pyarrow ``Table``— item 1d; used by the materializer's
250
+ channel-data preservation flow
251
+ 6. PIL ``Image`` — opportunistic, before ndarray
252
+ (PIL.Image quacks like an array)
253
+ 7. pandas DataFrame — opportunistic
254
+ 8. Pydantic — any model_dump_json-capable object
255
+ 9. numpy ``ndarray``— covers any tolist + dtype object
256
+ """
257
+ # Type-specific dispatch lives at the bottom of each predicate.
258
+ waveform_ext, waveform_mime = _waveform_ext_and_mime()
259
+ ndarray_ext, ndarray_mime = _ndarray_ext_and_mime()
260
+
261
+ builtins: list[tuple[Callable[[Any], bool], Serializer]] = [
262
+ (
263
+ lambda v: isinstance(v, Path),
264
+ Serializer(extension=".bin", mime="application/octet-stream", write=_write_path),
265
+ ),
266
+ (
267
+ lambda v: isinstance(v, Waveform),
268
+ Serializer(extension=waveform_ext, mime=waveform_mime, write=_write_waveform),
269
+ ),
270
+ (
271
+ lambda v: isinstance(v, XYData),
272
+ # Item 15: paired x/y arrays + optional unit/name keys → .npz.
273
+ # MIME shares the numpy-npz convention (item 13).
274
+ Serializer(extension=waveform_ext, mime=waveform_mime, write=_write_xydata),
275
+ ),
276
+ (
277
+ lambda v: isinstance(v, bytes),
278
+ Serializer(extension=".bin", mime="application/octet-stream", write=_write_bytes),
279
+ ),
280
+ (
281
+ _is_arrow_table,
282
+ # Item 1d: pyarrow.Table → ``.arrow`` IPC stream.
283
+ # Used by the materializer to preserve channel data
284
+ # before retention pruning.
285
+ Serializer(
286
+ extension=".arrow",
287
+ mime="application/vnd.apache.arrow.stream",
288
+ write=_write_arrow_table,
289
+ ),
290
+ ),
291
+ (
292
+ _is_pil_image,
293
+ Serializer(extension=".png", mime="image/png", write=_write_pil_image),
294
+ ),
295
+ (
296
+ _is_pandas_dataframe,
297
+ Serializer(
298
+ extension=".parquet",
299
+ mime="application/vnd.apache.parquet",
300
+ write=_write_pandas_dataframe,
301
+ ),
302
+ ),
303
+ (
304
+ _is_basemodel,
305
+ Serializer(extension=".json", mime="application/json", write=_write_basemodel_json),
306
+ ),
307
+ (
308
+ _is_ndarray,
309
+ Serializer(extension=ndarray_ext, mime=ndarray_mime, write=_write_ndarray),
310
+ ),
311
+ ]
312
+ _registry.extend(builtins)
313
+
314
+
315
+ _register_builtins()
316
+
317
+
318
+ # --------------------------------------------------------------------- #
319
+ # Pickle fallback #
320
+ # --------------------------------------------------------------------- #
321
+
322
+
323
+ PICKLE_FALLBACK = Serializer(
324
+ extension=".pkl",
325
+ mime="application/x-python-pickle",
326
+ write=_write_pickle,
327
+ )
328
+
329
+
330
+ # --------------------------------------------------------------------- #
331
+ # Public API #
332
+ # --------------------------------------------------------------------- #
333
+
334
+
335
+ def register_serializer(
336
+ predicate: Callable[[Any], bool] | type,
337
+ *,
338
+ extension: str,
339
+ mime: str,
340
+ write: Callable[[Any, Path], None],
341
+ ) -> None:
342
+ """Register a custom serializer.
343
+
344
+ User registrations are inserted at index 0 so they shadow the
345
+ built-in handlers — call this to override how a built-in type
346
+ serializes, or to add a new type entirely.
347
+
348
+ Args:
349
+ predicate: Either a Python ``type`` (matched via
350
+ ``isinstance``) or a callable ``(value) -> bool`` that
351
+ decides whether this serializer applies. Use the
352
+ callable form when the type isn't importable at module
353
+ load time (e.g. optional dependencies).
354
+ extension: File extension including the dot (e.g. ``".npz"``).
355
+ mime: MIME type per the TesterKit convention table (item 13).
356
+ write: Callable ``(value, dest)`` that writes ``value`` to
357
+ the destination ``Path``.
358
+ """
359
+ if isinstance(predicate, type):
360
+ cls = predicate
361
+ match: Callable[[Any], bool] = lambda v, _cls=cls: isinstance(v, _cls) # noqa: E731
362
+ else:
363
+ match = predicate
364
+ _registry.insert(0, (match, Serializer(extension=extension, mime=mime, write=write)))
365
+
366
+
367
+ def find_serializer(value: Any) -> Serializer:
368
+ """Return the serializer for ``value``; fall back to pickle with a warning.
369
+
370
+ Lookup order:
371
+
372
+ 1. The ``testerkit_serialize`` protocol — if the value exposes
373
+ ``testerkit_serialize(dest: Path) -> Path``, use it. The object
374
+ owns its file format; the extension is read from the object's
375
+ ``testerkit_extension`` attribute (default ``".bin"``) and the
376
+ MIME from ``testerkit_mime`` (default ``application/octet-stream``).
377
+ 2. The registry — first-match across user registrations + built-ins.
378
+ 3. Pickle fallback with ``RuntimeWarning`` naming the type.
379
+ """
380
+ if _has_testerkit_serialize_protocol(value):
381
+ extension = getattr(value, "testerkit_extension", ".bin")
382
+ mime = getattr(value, "testerkit_mime", "application/octet-stream")
383
+ return Serializer(extension=extension, mime=mime, write=_write_protocol)
384
+
385
+ for predicate, serializer in _registry:
386
+ if predicate(value):
387
+ return serializer
388
+
389
+ warnings.warn(
390
+ f"FileStore: no registered serializer for {type(value).__name__}; "
391
+ "falling back to pickle. Register a handler via "
392
+ "`from testerkit.data.files import register_serializer` so the artifact "
393
+ "lands as a typed file instead.",
394
+ RuntimeWarning,
395
+ stacklevel=3,
396
+ )
397
+ return PICKLE_FALLBACK
398
+
399
+
400
+ def _has_testerkit_serialize_protocol(value: Any) -> bool:
401
+ """True for objects that know how to write themselves."""
402
+ return callable(getattr(value, "testerkit_serialize", None))
403
+
404
+
405
+ def _write_protocol(value: Any, dest: Path) -> None:
406
+ """Adapter for the ``testerkit_serialize`` protocol."""
407
+ value.testerkit_serialize(dest)
408
+
409
+
410
+ def _reset_registry_for_tests() -> None:
411
+ """Reset the registry to built-ins-only. Tests that register a
412
+ custom serializer use this in teardown to avoid leaking state."""
413
+ _registry.clear()
414
+ _register_builtins()