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,170 @@
1
+ """New part creation page."""
2
+
3
+ from nicegui import ui
4
+
5
+ from testerkit.ui.shared.components import validate_resource_id
6
+ from testerkit.ui.shared.layout import create_layout
7
+ from testerkit.ui.shared.services import create_part, discover_parts
8
+
9
+
10
+ @ui.page("/parts/new")
11
+ def new_part_page():
12
+ """Create a new part."""
13
+ create_layout("New Part")
14
+
15
+ # Get existing part IDs to check for duplicates
16
+ existing_ids = {p["id"] for p in discover_parts()}
17
+
18
+ # Form state
19
+ form = {
20
+ "part_id": "",
21
+ "name": "",
22
+ "description": "",
23
+ }
24
+ validation = {
25
+ "id_error": "",
26
+ "name_error": "",
27
+ }
28
+
29
+ with ui.column().classes("w-full p-6 gap-6"):
30
+ # Header
31
+ with ui.row().classes("items-center gap-2"):
32
+ ui.icon("add_circle").classes("text-slate-600")
33
+ ui.label("Create New Part").classes("text-lg font-semibold text-slate-700")
34
+
35
+ # Form card
36
+ with ui.card().classes("w-full max-w-xl"):
37
+ with ui.card_section():
38
+ ui.label("Part Information").classes("font-semibold mb-4")
39
+
40
+ with ui.column().classes("gap-4 w-full"):
41
+ # Part ID
42
+ with ui.column().classes("gap-1 w-full"):
43
+ ui.label("Part ID").classes("text-sm font-medium text-slate-700")
44
+ ui.label(
45
+ "Unique identifier (lowercase, letters/numbers/hyphens only)"
46
+ ).classes("text-xs text-slate-400")
47
+ id_input = (
48
+ ui.input(
49
+ placeholder="e.g., tps54302",
50
+ )
51
+ .props("outlined dense")
52
+ .classes("w-full")
53
+ )
54
+ ui.label("").classes("text-xs text-red-500").bind_text_from(
55
+ validation, "id_error"
56
+ )
57
+
58
+ def validate_id(e):
59
+ value = e.value.lower().strip()
60
+ form["part_id"] = value
61
+ id_input.value = value
62
+ validation["id_error"] = validate_resource_id(
63
+ value, existing_ids, "Part ID"
64
+ )
65
+
66
+ id_input.on("change", validate_id)
67
+
68
+ # Name
69
+ with ui.column().classes("gap-1 w-full"):
70
+ ui.label("Name").classes("text-sm font-medium text-slate-700")
71
+ name_input = (
72
+ ui.input(
73
+ placeholder="e.g., TPS54302 Buck Converter",
74
+ )
75
+ .props("outlined dense")
76
+ .classes("w-full")
77
+ )
78
+ ui.label("").classes("text-xs text-red-500").bind_text_from(
79
+ validation, "name_error"
80
+ )
81
+
82
+ def validate_name(e):
83
+ value = e.value.strip()
84
+ form["name"] = value
85
+
86
+ if not value:
87
+ validation["name_error"] = "Name is required"
88
+ else:
89
+ validation["name_error"] = ""
90
+
91
+ name_input.on("change", validate_name)
92
+
93
+ # Description
94
+ with ui.column().classes("gap-1 w-full"):
95
+ ui.label("Description").classes("text-sm font-medium text-slate-700")
96
+ ui.label("Optional").classes("text-xs text-slate-400")
97
+ desc_input = (
98
+ ui.textarea(
99
+ placeholder="Brief description of the part...",
100
+ )
101
+ .props("outlined dense")
102
+ .classes("w-full")
103
+ )
104
+
105
+ def update_description(e):
106
+ value = e.value or ""
107
+ form["description"] = value.strip()
108
+
109
+ desc_input.on("change", update_description)
110
+
111
+ with ui.card_actions().classes("justify-end"):
112
+ ui.button(
113
+ "Cancel",
114
+ icon="close",
115
+ on_click=lambda: ui.navigate.to("/parts"),
116
+ ).props("flat")
117
+
118
+ def create():
119
+ # Validate
120
+ if not form["part_id"]:
121
+ validation["id_error"] = "Part ID is required"
122
+ return
123
+ if not form["name"]:
124
+ validation["name_error"] = "Name is required"
125
+ return
126
+ if validation["id_error"] or validation["name_error"]:
127
+ ui.notify("Please fix validation errors", type="warning")
128
+ return
129
+
130
+ # Create part
131
+ result = create_part(
132
+ part_id=form["part_id"],
133
+ name=form["name"],
134
+ description=form["description"],
135
+ )
136
+
137
+ if result:
138
+ ui.notify(
139
+ f"Part '{result['name']}' created successfully",
140
+ type="positive",
141
+ )
142
+ ui.navigate.to(f"/parts/{result['id']}/edit")
143
+ else:
144
+ ui.notify(
145
+ "Part ID already exists",
146
+ type="negative",
147
+ )
148
+
149
+ ui.button(
150
+ "Create Part",
151
+ icon="add",
152
+ on_click=create,
153
+ ).props("color=primary")
154
+
155
+ # Help text
156
+ with ui.card().classes("w-full max-w-xl bg-blue-50"):
157
+ with ui.card_section():
158
+ with ui.row().classes("items-start gap-3"):
159
+ ui.icon("lightbulb").classes("text-blue-500 mt-0.5")
160
+ with ui.column().classes("gap-1"):
161
+ ui.label("What happens next?").classes("font-medium text-blue-700")
162
+ ui.label(
163
+ "After creating the part, you'll be taken to the edit page "
164
+ "where you can add characteristics, test requirements, and more."
165
+ ).classes("text-sm text-blue-600")
166
+ ui.label(
167
+ "You can also upload a datasheet to help extract specifications."
168
+ ).classes("text-sm text-blue-600")
169
+
170
+ ui.link("← Back to Parts", "/parts").classes("text-blue-600 hover:underline")
@@ -0,0 +1,153 @@
1
+ """Part requirements page for procurement planning."""
2
+
3
+ from nicegui import ui
4
+
5
+ from testerkit.ui.shared.components import data_table
6
+ from testerkit.ui.shared.layout import create_layout
7
+ from testerkit.ui.shared.services import (
8
+ discover_parts,
9
+ get_all_station_matches_for_part,
10
+ get_required_capabilities,
11
+ load_part_model,
12
+ )
13
+
14
+
15
+ @ui.page("/parts/{part_id}/requirements")
16
+ def requirements_page(part_id: str):
17
+ """View required instruments for testing this part.
18
+
19
+ This helps with procurement - know what to order early,
20
+ mock tests while waiting for hardware.
21
+ """
22
+ parts = discover_parts()
23
+ part = next((p for p in parts if p["id"] == part_id), None)
24
+
25
+ if part:
26
+ create_layout(f"{part['name']} - Requirements")
27
+ else:
28
+ create_layout("Part Not Found")
29
+
30
+ with ui.column().classes("w-full p-6 gap-6"):
31
+ if part:
32
+ _render_requirements(part_id, part)
33
+ else:
34
+ _render_not_found()
35
+
36
+
37
+ def _render_requirements(part_id: str, part: dict):
38
+ """Render the requirements view."""
39
+ part_model = load_part_model(part_id)
40
+
41
+ # Header with back link
42
+ with ui.row().classes("items-center gap-4 mb-4"):
43
+ ui.link(f"← {part['name']}", f"/parts/{part_id}").classes("text-blue-600 hover:underline")
44
+
45
+ # Required capabilities card
46
+ with ui.card().classes("w-full"):
47
+ with ui.card_section():
48
+ ui.label("Required Instrument Capabilities").classes("text-lg font-semibold")
49
+ ui.label(
50
+ "These capabilities are needed to test this part. "
51
+ "Order missing instruments early - you can mock in the meantime."
52
+ ).classes("text-sm text-slate-500")
53
+
54
+ with ui.card_section():
55
+ if part_model:
56
+ capabilities = get_required_capabilities(part_model)
57
+ if capabilities:
58
+ columns = [
59
+ {
60
+ "name": "characteristic",
61
+ "label": "For Characteristic",
62
+ "field": "characteristic",
63
+ "align": "left",
64
+ },
65
+ {"name": "function", "label": "Function", "field": "function"},
66
+ {
67
+ "name": "direction",
68
+ "label": "Instrument Direction",
69
+ "field": "direction",
70
+ },
71
+ {"name": "signals", "label": "Signals", "field": "signals"},
72
+ ]
73
+ rows = [
74
+ {
75
+ "characteristic": cap["characteristic"],
76
+ "function": cap["function"],
77
+ "direction": cap["direction"],
78
+ "signals": cap.get("signals", ""),
79
+ }
80
+ for cap in capabilities
81
+ ]
82
+ data_table(columns=columns, rows=rows, row_key="characteristic")
83
+ else:
84
+ ui.label(
85
+ "No characteristics defined - add characteristics to generate requirements."
86
+ ).classes("text-slate-500 italic")
87
+ else:
88
+ ui.label("Could not load part model.").classes("text-amber-600")
89
+
90
+ # Station coverage card
91
+ station_matches = get_all_station_matches_for_part(part_id)
92
+ compatible = station_matches.get("compatible", [])
93
+ partial = station_matches.get("partial", [])
94
+
95
+ with ui.card().classes("w-full mt-4"):
96
+ with ui.card_section():
97
+ ui.label("Station Coverage").classes("text-lg font-semibold")
98
+
99
+ with ui.card_section():
100
+ if compatible:
101
+ with ui.row().classes("items-center gap-2 mb-3"):
102
+ ui.icon("check_circle").classes("text-emerald-500")
103
+ ui.label(f"{len(compatible)} station(s) fully compatible").classes(
104
+ "text-emerald-600 font-medium"
105
+ )
106
+
107
+ if partial:
108
+ ui.label("Partially Compatible Stations").classes(
109
+ "font-medium text-amber-600 mt-2 mb-2"
110
+ )
111
+ ui.label(
112
+ "These stations have some capabilities. "
113
+ "Add missing instruments to enable full testing."
114
+ ).classes("text-xs text-slate-500 mb-2")
115
+
116
+ for station in partial:
117
+ with ui.row().classes("items-center gap-3 py-2 border-b border-slate-100"):
118
+ ui.icon("warning").classes("text-amber-500")
119
+ with ui.column().classes("flex-1"):
120
+ ui.label(station["name"]).classes("font-medium")
121
+ with ui.row().classes("gap-1 flex-wrap"):
122
+ ui.label("Missing:").classes("text-xs text-slate-500")
123
+ for cap in station.get("missing", []):
124
+ ui.badge(cap, color="amber").props("outline dense")
125
+ ui.badge(f"{station['coverage']}%", color="amber")
126
+
127
+ if not compatible and not partial:
128
+ with ui.row().classes("items-center gap-2"):
129
+ ui.icon("info").classes("text-slate-400")
130
+ ui.label("No stations have any required instruments.").classes("text-slate-500")
131
+ ui.label("Consider creating a station or ordering instruments.").classes(
132
+ "text-sm text-slate-400 mt-1"
133
+ )
134
+
135
+ # Action buttons
136
+ with ui.row().classes("mt-4 gap-3"):
137
+ ui.button(
138
+ "View Compatible Stations",
139
+ icon="memory",
140
+ on_click=lambda: ui.navigate.to(f"/parts/{part_id}/stations"),
141
+ ).props("outline")
142
+ ui.button(
143
+ "Launch with Mocks",
144
+ icon="play_arrow",
145
+ on_click=lambda: ui.navigate.to(f"/launch?part={part_id}&mock=1"),
146
+ ).props("color=primary")
147
+
148
+
149
+ def _render_not_found():
150
+ """Render part not found message."""
151
+ with ui.card().classes("w-full p-6 text-center"):
152
+ ui.label("Part not found.").classes("text-xl text-slate-600")
153
+ ui.link("← Back to Parts", "/parts").classes("text-blue-600 hover:underline")
@@ -0,0 +1,199 @@
1
+ """Part station compatibility page."""
2
+
3
+ from nicegui import ui
4
+
5
+ from testerkit.ui.shared.layout import create_layout
6
+ from testerkit.ui.shared.services import (
7
+ discover_parts,
8
+ get_all_station_matches_for_part,
9
+ load_part_model,
10
+ )
11
+
12
+
13
+ @ui.page("/parts/{part_id}/stations")
14
+ def part_stations_page(part_id: str):
15
+ """View station compatibility for this part.
16
+
17
+ Shows which stations can fully or partially test this part.
18
+ Helps with procurement planning and test execution decisions.
19
+ """
20
+ parts = discover_parts()
21
+ part = next((p for p in parts if p["id"] == part_id), None)
22
+
23
+ if part:
24
+ create_layout(f"{part['name']} - Station Compatibility")
25
+ else:
26
+ create_layout("Part Not Found")
27
+
28
+ with ui.column().classes("w-full p-6 gap-6"):
29
+ if part:
30
+ _render_station_compatibility(part_id, part)
31
+ else:
32
+ _render_not_found()
33
+
34
+
35
+ def _render_station_compatibility(part_id: str, part: dict):
36
+ """Render the station compatibility view."""
37
+ part_model = load_part_model(part_id)
38
+ station_matches = get_all_station_matches_for_part(part_id)
39
+
40
+ compatible = station_matches.get("compatible", [])
41
+ partial = station_matches.get("partial", [])
42
+ incompatible = station_matches.get("incompatible", [])
43
+
44
+ # Header with back link
45
+ with ui.row().classes("items-center gap-4 mb-4"):
46
+ ui.link(f"← {part['name']}", f"/parts/{part_id}").classes("text-blue-600 hover:underline")
47
+
48
+ # Summary card
49
+ with ui.card().classes("w-full"):
50
+ with ui.card_section():
51
+ ui.label(f"Station Compatibility for {part['name']}").classes("text-lg font-semibold")
52
+ if part_model:
53
+ char_count = len(part_model.characteristics)
54
+ ui.label(f"Matching stations against {char_count} characteristic(s)").classes(
55
+ "text-sm text-slate-500"
56
+ )
57
+
58
+ with ui.card_section():
59
+ with ui.row().classes("gap-6"):
60
+ with ui.column().classes("items-center"):
61
+ ui.label(str(len(compatible))).classes("text-2xl font-bold text-emerald-600")
62
+ ui.label("Ready").classes("text-xs text-slate-500")
63
+ with ui.column().classes("items-center"):
64
+ ui.label(str(len(partial))).classes("text-2xl font-bold text-amber-600")
65
+ ui.label("Partial").classes("text-xs text-slate-500")
66
+ with ui.column().classes("items-center"):
67
+ ui.label(str(len(incompatible))).classes("text-2xl font-bold text-slate-400")
68
+ ui.label("None").classes("text-xs text-slate-500")
69
+
70
+ # Fully compatible stations
71
+ if compatible:
72
+ with ui.card().classes("w-full mt-4"):
73
+ with ui.card_section():
74
+ with ui.row().classes("items-center gap-2"):
75
+ ui.icon("check_circle").classes("text-emerald-500")
76
+ ui.label("Ready to Test").classes("text-lg font-semibold text-emerald-600")
77
+ ui.label("These stations have all required instruments.").classes(
78
+ "text-sm text-slate-500"
79
+ )
80
+
81
+ with ui.card_section():
82
+ for station in compatible:
83
+ _render_compatible_station(part_id, station)
84
+
85
+ # Partially compatible stations
86
+ if partial:
87
+ with ui.card().classes("w-full mt-4"):
88
+ with ui.card_section():
89
+ with ui.row().classes("items-center gap-2"):
90
+ ui.icon("warning").classes("text-amber-500")
91
+ ui.label("Needs Additional Instruments").classes(
92
+ "text-lg font-semibold text-amber-600"
93
+ )
94
+ ui.label(
95
+ "Add missing instruments to enable full testing, or use simulation."
96
+ ).classes("text-sm text-slate-500")
97
+
98
+ with ui.card_section():
99
+ for station in partial:
100
+ _render_partial_station(part_id, station)
101
+
102
+ # Incompatible stations (if any exist)
103
+ if incompatible:
104
+ with ui.expansion("Incompatible Stations", icon="block").classes("w-full mt-4"):
105
+ ui.label("These stations have none of the required instruments.").classes(
106
+ "text-sm text-slate-500 mb-2"
107
+ )
108
+ for station in incompatible:
109
+ with ui.row().classes("items-center gap-3 py-2 border-b border-slate-100"):
110
+ ui.icon("block").classes("text-slate-300")
111
+ ui.label(station["name"]).classes("text-slate-500")
112
+ if station.get("location"):
113
+ ui.label(f"({station['location']})").classes("text-xs text-slate-400")
114
+
115
+ # No stations at all
116
+ if not compatible and not partial and not incompatible:
117
+ with ui.card().classes("w-full mt-4 p-6 text-center"):
118
+ ui.icon("search_off").classes("text-4xl text-slate-300")
119
+ ui.label("No stations found").classes("text-lg text-slate-500 mt-2")
120
+ ui.label("Create a station configuration to enable testing.").classes(
121
+ "text-sm text-slate-400"
122
+ )
123
+ ui.button(
124
+ "Create Station",
125
+ icon="add",
126
+ on_click=lambda: ui.navigate.to("/stations/new"),
127
+ ).classes("mt-4")
128
+
129
+ # Action buttons
130
+ with ui.row().classes("mt-4 gap-3"):
131
+ ui.button(
132
+ "View Requirements",
133
+ icon="list_alt",
134
+ on_click=lambda: ui.navigate.to(f"/parts/{part_id}/requirements"),
135
+ ).props("outline")
136
+ if compatible:
137
+ ui.button(
138
+ "Launch Tests",
139
+ icon="play_arrow",
140
+ on_click=lambda: ui.navigate.to(f"/launch?part={part_id}"),
141
+ ).props("color=primary")
142
+ else:
143
+ ui.button(
144
+ "Launch with Mocks",
145
+ icon="play_arrow",
146
+ on_click=lambda: ui.navigate.to(f"/launch?part={part_id}&mock=1"),
147
+ ).props("color=primary")
148
+
149
+
150
+ def _render_compatible_station(part_id: str, station: dict):
151
+ """Render a fully compatible station row."""
152
+ with ui.row().classes("items-center justify-between py-3 border-b border-slate-100 w-full"):
153
+ with ui.column().classes("flex-1"):
154
+ ui.label(station["name"]).classes("font-semibold")
155
+ if station.get("location"):
156
+ ui.label(station["location"]).classes("text-sm text-slate-500")
157
+ with ui.row().classes("items-center gap-3"):
158
+ ui.badge("100%", color="green").props("dense")
159
+ ui.button(
160
+ "Launch",
161
+ icon="play_arrow",
162
+ on_click=lambda _, s=station: ui.navigate.to(
163
+ f"/launch?part={part_id}&station={s['id']}"
164
+ ),
165
+ ).props("flat dense")
166
+
167
+
168
+ def _render_partial_station(part_id: str, station: dict):
169
+ """Render a partially compatible station row."""
170
+ with ui.row().classes("items-center justify-between py-3 border-b border-slate-100 w-full"):
171
+ with ui.column().classes("flex-1"):
172
+ ui.label(station["name"]).classes("font-semibold")
173
+ if station.get("location"):
174
+ ui.label(station["location"]).classes("text-sm text-slate-500")
175
+ # Show missing capabilities
176
+ with ui.row().classes("gap-1 flex-wrap mt-1"):
177
+ ui.label("Missing:").classes("text-xs text-slate-500")
178
+ for cap in station.get("missing", [])[:5]: # Limit to 5
179
+ ui.badge(cap, color="amber").props("outline dense")
180
+ if len(station.get("missing", [])) > 5:
181
+ ui.badge(f"+{len(station['missing']) - 5} more", color="grey").props(
182
+ "outline dense"
183
+ )
184
+ with ui.row().classes("items-center gap-3"):
185
+ ui.badge(f"{station['coverage']}%", color="amber").props("dense")
186
+ ui.button(
187
+ "Mock",
188
+ icon="play_arrow",
189
+ on_click=lambda _, s=station: ui.navigate.to(
190
+ f"/launch?part={part_id}&station={s['id']}&mock=1"
191
+ ),
192
+ ).props("flat dense outline")
193
+
194
+
195
+ def _render_not_found():
196
+ """Render part not found message."""
197
+ with ui.card().classes("w-full p-6 text-center"):
198
+ ui.label("Part not found.").classes("text-xl text-slate-600")
199
+ ui.link("← Back to Parts", "/parts").classes("text-blue-600 hover:underline")
@@ -0,0 +1,4 @@
1
+ """Profile pages — list + detail."""
2
+
3
+ # Import pages to register routes
4
+ from testerkit.ui.pages.profiles import detail, list # noqa: F401
@@ -0,0 +1,113 @@
1
+ """Profile detail page — view one profile's resolved YAML + extends chain."""
2
+
3
+ import yaml
4
+ from nicegui import ui
5
+
6
+ from testerkit.ui.shared.components import page_layout
7
+ from testerkit.ui.shared.layout import create_layout
8
+ from testerkit.ui.shared.services import discover_profiles, load_profile_config
9
+
10
+
11
+ @ui.page("/profiles/{name}")
12
+ def profile_detail_page(name: str):
13
+ """One profile — header card + resolved YAML + extends-chain map.
14
+
15
+ The YAML is the merged in-memory ProfileConfig (after extends-chain
16
+ resolution at session start). Read-only — edits land via the
17
+ file system (no in-browser editor yet).
18
+ """
19
+ create_layout(f"Profile · {name}")
20
+
21
+ profile = load_profile_config(name)
22
+
23
+ with page_layout():
24
+ if profile is None:
25
+ with ui.card().classes("w-full p-6 text-center"):
26
+ ui.label(f"Profile '{name}' not found.").classes("text-xl text-slate-600")
27
+ ui.link("← Back to Profiles", "/profiles").classes("text-blue-600 hover:underline")
28
+ return
29
+
30
+ # Header row
31
+ with ui.row().classes("items-center justify-between w-full"):
32
+ with ui.row().classes("items-center gap-2"):
33
+ ui.icon("layers").classes("text-slate-600")
34
+ ui.label(name).classes("text-lg font-semibold text-slate-700")
35
+ ui.button(
36
+ "Launch Test",
37
+ icon="play_arrow",
38
+ on_click=lambda: ui.navigate.to(f"/launch?test_profile={name}"),
39
+ ).props("color=primary")
40
+
41
+ # Summary card — extends, station_type, fixture, facets
42
+ with ui.card().classes("w-full"):
43
+ with ui.card_section():
44
+ ui.label("Summary").classes("font-semibold")
45
+ with ui.card_section(), ui.row().classes("gap-6 flex-wrap"):
46
+ _meta_field("Extends", profile.extends or "—")
47
+ _meta_field("Station type", profile.station_type or "—")
48
+ _meta_field("Fixture", profile.fixture or "—")
49
+ facets_str = ", ".join(f"{k}={v}" for k, v in (profile.facets or {}).items()) or "—"
50
+ _meta_field("Facets", facets_str)
51
+ _meta_field("Tests", str(len(profile.tests or {})))
52
+
53
+ # Extends chain — walk parents recursively for the visual map
54
+ chain = _resolve_extends_chain(name)
55
+ if len(chain) > 1:
56
+ with ui.card().classes("w-full"):
57
+ with ui.card_section():
58
+ ui.label("Inheritance").classes("font-semibold")
59
+ ui.label(
60
+ "Parent profiles are applied first; this profile's "
61
+ "fields override the merged result."
62
+ ).classes("text-xs text-slate-500")
63
+ with ui.card_section(), ui.row().classes("items-center gap-2"):
64
+ for i, link in enumerate(chain):
65
+ if i > 0:
66
+ ui.icon("arrow_forward", size="sm").classes("text-slate-400")
67
+ if link == name:
68
+ ui.badge(link, color="primary")
69
+ else:
70
+ ui.link(link, target=f"/profiles/{link}").classes("text-blue-600")
71
+
72
+ # Resolved YAML — single source of truth for what this profile
73
+ # actually configures
74
+ with ui.card().classes("w-full"):
75
+ with ui.card_section():
76
+ ui.label("Resolved YAML").classes("font-semibold")
77
+ ui.label("After the extends chain resolves at session start.").classes(
78
+ "text-xs text-slate-500"
79
+ )
80
+ with ui.card_section():
81
+ yaml_text = yaml.safe_dump(
82
+ profile.model_dump(exclude_none=True),
83
+ sort_keys=False,
84
+ default_flow_style=False,
85
+ )
86
+ ui.code(yaml_text, language="yaml").classes("w-full")
87
+
88
+ with ui.row().classes("mt-2"):
89
+ ui.link("← Back to Profiles", "/profiles").classes("text-blue-600 hover:underline")
90
+
91
+
92
+ def _meta_field(label: str, value: str) -> None:
93
+ with ui.column().classes("gap-0"):
94
+ ui.label(label).classes("text-xs text-slate-500 uppercase")
95
+ ui.label(value).classes("text-sm font-medium text-slate-800")
96
+
97
+
98
+ def _resolve_extends_chain(name: str) -> list[str]:
99
+ """Walk the ``extends:`` chain from root → leaf. Stops if a parent
100
+ isn't a known profile (silently — the runtime resolver would raise).
101
+ """
102
+ profiles = {p.name: p for p in discover_profiles()}
103
+ chain: list[str] = []
104
+ current = name
105
+ seen: set[str] = set()
106
+ while current and current not in seen:
107
+ seen.add(current)
108
+ chain.append(current)
109
+ row = profiles.get(current)
110
+ if not row or not row.extends:
111
+ break
112
+ current = row.extends
113
+ return list(reversed(chain))