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,463 @@
1
+ """FileStore — durable file artifact storage, session-scoped.
2
+
3
+ Holds non-numeric blobs: images, video, vendor files (.tdms), NPZ
4
+ waveforms, JSON exports, anything that fits the file-shaped pattern
5
+ (one artifact, one URI). Companion to ChannelStore for typed numeric
6
+ streaming; not for ChannelStore-shaped data (rejected by ChannelStore
7
+ via ``classify_value``).
8
+
9
+ URI format: ``file://{date}/{session_id}/{filename}``
10
+ On disk: ``{data_dir}/files/{date}/{session_id}/{filename}``
11
+ Sidecar: ``{filename}.meta.json`` (item 1c — MIME + size +
12
+ user attributes)
13
+
14
+ Filename convention:
15
+ - with ``vector_id``: ``{vector_id_short}_{name}.{ext}``
16
+ - without: ``{name}.{ext}``
17
+ - on collision: ``_2``, ``_3``, … suffix (preserves claim-check
18
+ immutability — repeated puts create distinct files; the vector's
19
+ ``out_<name>`` last-write-wins separately at materialization)
20
+
21
+ Type dispatch + MIME convention live in
22
+ :mod:`testerkit.data.files.serializers` (build items 12 + 13). Custom
23
+ types either expose ``testerkit_serialize(dest)`` (the protocol) or
24
+ register via :func:`register_serializer`.
25
+
26
+ Streaming sink lands in 1b. Migration of the two legacy ``_ref``
27
+ dirs lands in 1d.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from datetime import UTC, datetime
33
+ from pathlib import Path
34
+ from typing import TYPE_CHECKING, Any
35
+ from uuid import UUID
36
+
37
+ from testerkit.data.data_dir import resolve_data_dir
38
+ from testerkit.data.files._backend import BlobBackend, resolve_files_backend
39
+ from testerkit.data.files.catalog import catalog_row
40
+ from testerkit.data.files.catalog_manager import (
41
+ push_artifact as _catalog_push,
42
+ )
43
+ from testerkit.data.files.models import FileArtifactMetadata
44
+ from testerkit.data.files.serializers import find_serializer
45
+ from testerkit.data.files.streaming import StreamingSink, _BaseSink, get_format
46
+
47
+ if TYPE_CHECKING:
48
+ from testerkit.data.event_log import EventLog
49
+
50
+ _SIDECAR_SUFFIX = ".meta.json"
51
+
52
+ # Truncate vector_id to N chars for filename prefix (audit trail
53
+ # without bloating). Matches existing ``VECTOR_ID_LENGTH`` convention
54
+ # in ``data/backends/_row_helpers.py``.
55
+ _VECTOR_ID_LENGTH = 8
56
+
57
+
58
+ class FileStore:
59
+ """Durable file artifact storage, session-scoped.
60
+
61
+ Typical use is indirect — ``observe()`` / ``verify()`` route
62
+ non-channel-shaped values here automatically. Direct use is for
63
+ power-user cases (the streaming sink lands in build item 1b; for
64
+ one-shot puts, callers can go through this class today).
65
+
66
+ Holds non-numeric blobs only — not channel-shaped numerics (those
67
+ go to ChannelStore). The dispatch decision is the caller's; this
68
+ store does not type-check the incoming value beyond what its
69
+ serializer can handle.
70
+
71
+ Per build item 1c, every put writes a sidecar
72
+ ``{filename}.meta.json`` next to the artifact carrying
73
+ :class:`FileArtifactMetadata` (mime / extension / size_bytes /
74
+ user attributes). Read it back with :meth:`read_attributes`.
75
+ """
76
+
77
+ def __init__(
78
+ self, *, _data_dir: Path | str | None = None, backend_uri: str | None = None
79
+ ) -> None:
80
+ self._data_dir = resolve_data_dir(_data_dir)
81
+ self._files_dir = self._data_dir / "files"
82
+ # Blob bytes flow through this backend (local disk by default; an
83
+ # object store by config — the only thing that changes for a swap).
84
+ # Default root is self._files_dir so it tracks whatever resolved the
85
+ # data dir (incl. test monkeypatches); env/config override only when
86
+ # no explicit _data_dir was given.
87
+ self._backend = BlobBackend.from_uri(
88
+ resolve_files_backend(
89
+ self._files_dir, allow_override=_data_dir is None, backend_uri=backend_uri
90
+ )
91
+ )
92
+
93
+ def write(
94
+ self,
95
+ name: str,
96
+ value: Any,
97
+ *,
98
+ session_id: str,
99
+ vector_id: str | None = None,
100
+ attributes: dict[str, Any] | None = None,
101
+ instrument_role: str = "",
102
+ resource: str = "",
103
+ run_id: UUID | None = None,
104
+ ) -> str:
105
+ """Write ``value`` to FileStore; return its ``file://`` URI.
106
+
107
+ Args:
108
+ name: Logical name for the artifact (e.g.
109
+ ``"scope.ch1.capture"``). Forms the bulk of the
110
+ filename. Should not include extension; extension is
111
+ chosen by the registry (build item 12).
112
+ value: The value to write. Routed through
113
+ :func:`testerkit.data.files.serializers.find_serializer`
114
+ — see that module for the built-in convention table
115
+ and the ``testerkit_serialize`` protocol /
116
+ :func:`register_serializer` extension points.
117
+ Last-resort fallback is pickle with a
118
+ ``RuntimeWarning`` naming the type.
119
+ session_id: Session this artifact belongs to. Required.
120
+ Callers (verb layer) pull from the active-session
121
+ ContextVar.
122
+ vector_id: Optional vector context. When provided, the
123
+ first 8 chars prefix the filename for audit
124
+ (matches existing convention).
125
+ attributes: User-supplied metadata bag persisted into the
126
+ sidecar (item 1c). ``None`` writes an empty
127
+ attributes dict. The bag is round-trippable via
128
+ :meth:`read_attributes`.
129
+ instrument_role: Optional provenance — the station-config
130
+ instrument role (e.g. ``"scope"``, ``"psu"``) that
131
+ produced this artifact. Populated when a Waveform /
132
+ channel-shaped value falls through to FileStore from
133
+ ``Context.observe`` because no ChannelStore was wired
134
+ (bare-Context bringup tests). Without this the FileStore
135
+ fallback path would silently lose the provenance the
136
+ ChannelStore descriptor would have captured.
137
+ resource: Optional provenance — VISA / network resource
138
+ string for the instrument (paired with
139
+ ``instrument_role``). Same population path as above.
140
+
141
+ Returns:
142
+ URI of the form ``file://{date}/{session_id}/{filename}``.
143
+ The filename reflects the actual on-disk name (including
144
+ any collision-avoidance ``_2`` / ``_3`` suffix).
145
+ """
146
+ serializer = find_serializer(value)
147
+ # Path values: the source file's suffix wins over the
148
+ # serializer's default ``.bin`` so e.g. ``capture.tdms`` stays
149
+ # ``.tdms`` on disk.
150
+ if isinstance(value, Path):
151
+ ext = value.suffix or serializer.extension
152
+ else:
153
+ ext = serializer.extension
154
+ prefix = f"{vector_id[:_VECTOR_ID_LENGTH]}_" if vector_id else ""
155
+
156
+ # Compute session directory (date-partitioned for retention/ops)
157
+ session_dir = self._session_dir(session_id)
158
+ session_dir.mkdir(parents=True, exist_ok=True)
159
+
160
+ # Resolve a unique filename within the session dir; the backend key
161
+ # is that filename under {date}/{session_id}.
162
+ filename = self._unique_filename(session_dir, f"{prefix}{name}", ext)
163
+ date = session_dir.parent.name
164
+ key = f"{date}/{session_id}/{filename}"
165
+
166
+ # Serialize to a local staging path (suffix preserved — writers like
167
+ # np.savez rename themselves otherwise), then publish atomically to
168
+ # the blob backend: a same-fs rename locally, an upload remotely. A
169
+ # crash mid-serialize leaves the staging temp, never a published
170
+ # artifact the catalog could point at.
171
+ staged = self._backend.stage_path(key)
172
+ serializer.write(value, staged)
173
+ self._backend.publish_atomic(staged, key)
174
+
175
+ # Item 1c: write the sidecar metadata. size_bytes is read back from
176
+ # the backend after publish so it reflects the actual stored size.
177
+ # The sidecar pairs the artifact one-to-one (mime, size, attributes);
178
+ # backend.write_bytes is atomic (temp+rename local, single PUT remote)
179
+ # so it either lands fully or not at all.
180
+ metadata = FileArtifactMetadata(
181
+ mime=serializer.mime,
182
+ extension=ext,
183
+ size_bytes=self._backend.size(key) or 0,
184
+ attributes=dict(attributes or {}),
185
+ instrument_role=instrument_role,
186
+ resource=resource,
187
+ run_id=str(run_id) if run_id else None,
188
+ )
189
+ self._backend.write_bytes(f"{key}{_SIDECAR_SUFFIX}", metadata.model_dump_json().encode())
190
+
191
+ uri = f"file://{key}"
192
+ # Keep the daemon's warm catalog current (req 2). Best-effort and
193
+ # non-spawning: skips silently if no daemon is running, and the
194
+ # sidecar is the durable truth a restart rebuilds from. The catalog
195
+ # stores the backend ``key`` (not an absolute path) so resolution
196
+ # stays backend-agnostic.
197
+ _catalog_push(
198
+ self._files_dir,
199
+ catalog_row(
200
+ uri=uri,
201
+ session_id=session_id,
202
+ name=filename,
203
+ key=key,
204
+ meta=metadata,
205
+ created_at=datetime.now(UTC),
206
+ ),
207
+ )
208
+ return uri
209
+
210
+ def open_stream(
211
+ self,
212
+ name: str,
213
+ *,
214
+ format: str,
215
+ session_id: str,
216
+ vector_id: str | None = None,
217
+ attributes: dict[str, Any] | None = None,
218
+ event_log: EventLog | None = None,
219
+ run_id: UUID | None = None,
220
+ checkpoint_cadence: float | None = None,
221
+ ) -> StreamingSink:
222
+ """Open a streaming sink — one file, written incrementally.
223
+
224
+ Build item 2 (C5). Companion to :meth:`write` for cases where
225
+ bytes arrive over time rather than all-at-once (continuous DAQ,
226
+ video capture, line-delimited logs). The sink:
227
+
228
+ - allocates the artifact's backend key (collision-safe via the
229
+ same ``_unique_filename`` scheme as :meth:`write`)
230
+ - emits :class:`FileStarted` on open
231
+ - pushes the new bytes as an ephemeral frame after every
232
+ :meth:`write` (via the files daemon, not the event log) so
233
+ live consumers receive them push-style, no poll
234
+ - emits :class:`FileEnded` on :meth:`close` (carries final
235
+ ``file://`` URI + total size)
236
+ - writes the item-1c sidecar metadata + catalog row on close
237
+
238
+ Live consumers subscribe to the stream's frames and receive each
239
+ new chunk push-style; the final URI arrives in
240
+ :class:`FileEnded`. See the
241
+ :mod:`testerkit.data.files.streaming` module docstring for format
242
+ coverage + caveats per format on partial-decode-during-write.
243
+
244
+ Args:
245
+ name: Artifact name (becomes part of the filename).
246
+ format: One of the registered streaming formats —
247
+ ``"raw"``, ``"jsonl"``, ``"tdms"``, ``"h5"`` in
248
+ v0.2.0. See :func:`testerkit.data.files.streaming.registered_formats`.
249
+ session_id: Session this artifact belongs to. Required.
250
+ vector_id: Optional vector context; first 8 chars prefix
251
+ the filename for audit.
252
+ attributes: User-supplied metadata bag — persisted into
253
+ the item-1c sidecar at close.
254
+ event_log: Event log to emit Stream* events into. ``None``
255
+ is allowed (silent writes — useful for tests of the
256
+ file path in isolation); production paths always plumb
257
+ this from the active session.
258
+ run_id: Optional run UUID stamped on Stream* events.
259
+
260
+ Returns:
261
+ A :class:`StreamingSink` — context-manageable; call
262
+ :meth:`StreamingSink.close` (or use a ``with`` block) to
263
+ finalize and receive the ``file://`` URI.
264
+ """
265
+ fmt = get_format(format)
266
+ prefix = f"{vector_id[:_VECTOR_ID_LENGTH]}_" if vector_id else ""
267
+ session_dir = self._session_dir(session_id)
268
+ session_dir.mkdir(parents=True, exist_ok=True)
269
+ filename = self._unique_filename(session_dir, f"{prefix}{name}", fmt.extension)
270
+ date = session_dir.parent.name
271
+ key = f"{date}/{session_id}/{filename}"
272
+ uri = f"file://{key}"
273
+
274
+ # Capture for sidecar write at close
275
+ attrs_for_sidecar = dict(attributes or {})
276
+
277
+ def _finalize(staged: Path | None) -> None:
278
+ # Format sinks (tdms/h5) staged to a local path → publish it now;
279
+ # raw/jsonl already published by closing the backend output stream.
280
+ # Then the sidecar + catalog row — the same durable tail write() runs.
281
+ if staged is not None:
282
+ self._backend.publish_atomic(staged, key)
283
+ metadata = FileArtifactMetadata(
284
+ mime=fmt.mime,
285
+ extension=fmt.extension,
286
+ size_bytes=self._backend.size(key) or 0,
287
+ attributes=attrs_for_sidecar,
288
+ run_id=str(run_id) if run_id else None,
289
+ )
290
+ self._backend.write_bytes(
291
+ f"{key}{_SIDECAR_SUFFIX}", metadata.model_dump_json().encode()
292
+ )
293
+ _catalog_push(
294
+ self._files_dir,
295
+ catalog_row(
296
+ uri=uri,
297
+ session_id=session_id,
298
+ name=filename,
299
+ key=key,
300
+ meta=metadata,
301
+ created_at=datetime.now(UTC),
302
+ ),
303
+ )
304
+
305
+ common: dict[str, Any] = {
306
+ "uri": uri,
307
+ "files_dir": self._files_dir,
308
+ "name": name,
309
+ "format_name": format,
310
+ "session_id": session_id,
311
+ "event_log": event_log,
312
+ "run_id": run_id,
313
+ }
314
+ if fmt.needs_local_path:
315
+ # nptdms / h5py need a seekable local file; stage there, publish on close.
316
+ staged = self._backend.stage_path(key)
317
+ sink = fmt.open(path=staged, finalizer=lambda: _finalize(staged), **common)
318
+ else:
319
+ # raw/jsonl write straight to the backend output stream (local file /
320
+ # S3 multipart) — completes on close, so no separate publish step.
321
+ stream = self._backend.open_output_stream(key)
322
+ sink = fmt.open(stream=stream, finalizer=lambda: _finalize(None), **common)
323
+ # Stream liveness cadence is set post-construction so custom formats
324
+ # (any StreamingSink) need not accept it; only _BaseSink subclasses
325
+ # carry the checkpoint machinery.
326
+ if checkpoint_cadence is not None and isinstance(sink, _BaseSink):
327
+ sink._checkpoint_cadence = checkpoint_cadence
328
+ return sink
329
+
330
+ def read(self, uri: str) -> bytes | None:
331
+ """Return the full bytes of the artifact at ``uri``, or ``None``.
332
+
333
+ The store serves the file — callers never touch the filesystem
334
+ or know where the bytes live. Goes through the blob backend, so
335
+ a remote (S3/GCS) backend serves the same way as local disk.
336
+ """
337
+ key = self._resolve_key(uri)
338
+ return None if key is None else self._backend.read_bytes(key)
339
+
340
+ def read_range(self, uri: str, *, offset: int = 0, length: int | None = None) -> bytes | None:
341
+ """Range-read ``[offset, offset+length)`` of ``uri`` (HTTP Range)."""
342
+ key = self._resolve_key(uri)
343
+ return None if key is None else self._backend.read_range(key, offset=offset, length=length)
344
+
345
+ def size(self, uri: str) -> int | None:
346
+ """Stored byte size of the artifact at ``uri``, or ``None``."""
347
+ key = self._resolve_key(uri)
348
+ return None if key is None else self._backend.size(key)
349
+
350
+ def modified_at(self, uri: str) -> datetime | None:
351
+ """Last-modified time of the artifact at ``uri``, or ``None``.
352
+
353
+ Backend-portable (local mtime / object-store LastModified). For an
354
+ immutable write-once artifact this is effectively its creation time.
355
+ """
356
+ key = self._resolve_key(uri)
357
+ return None if key is None else self._backend.modified_at(key)
358
+
359
+ def open_input(self, uri: str) -> Any:
360
+ """Open a streaming read handle for ``uri`` (caller closes it), or ``None``.
361
+
362
+ Returns a random-access ``pyarrow.NativeFile`` so a consumer can stream
363
+ a large artifact in chunks rather than buffering it whole — sequential
364
+ reads become ranged GETs on a remote backend.
365
+ """
366
+ key = self._resolve_key(uri)
367
+ return None if key is None else self._backend.open_input(key)
368
+
369
+ def read_attributes(self, uri: str) -> FileArtifactMetadata | None:
370
+ """Return the :class:`FileArtifactMetadata` for ``uri``, or None.
371
+
372
+ ``None`` when the URI doesn't resolve to a FileStore artifact, or
373
+ when its sidecar is missing (e.g. an artifact put before item 1c
374
+ landed). The sidecar is read through the backend like any blob.
375
+
376
+ Args:
377
+ uri: A ``file://{date}/{session_id}/{filename}`` URI returned
378
+ by :meth:`put`.
379
+ """
380
+ key = self._resolve_key(uri)
381
+ if key is None:
382
+ return None
383
+ raw = self._backend.read_bytes(f"{key}{_SIDECAR_SUFFIX}")
384
+ if raw is None:
385
+ return None
386
+ return FileArtifactMetadata.model_validate_json(raw)
387
+
388
+ def delete(self, uri: str) -> None:
389
+ """Delete the artifact at ``uri`` and its sidecar (best-effort).
390
+
391
+ A no-op when the URI doesn't resolve. Removes the blob bytes through
392
+ the backend; the catalog row, if any, clears when the daemon next
393
+ rescans (the sidecar — its durable source — is gone).
394
+ """
395
+ key = self._resolve_key(uri)
396
+ if key is None:
397
+ return
398
+ self._backend.delete(key)
399
+ self._backend.delete(f"{key}{_SIDECAR_SUFFIX}")
400
+
401
+ def close(self) -> None:
402
+ """No-op — FileStore holds no daemon ref or background thread.
403
+
404
+ Present so FileStore satisfies the optional-close store contract
405
+ (usable as ``with FileStore(...) as fs:``) like the other stores.
406
+ """
407
+
408
+ def __enter__(self) -> FileStore:
409
+ return self
410
+
411
+ def __exit__(self, *_: object) -> None:
412
+ self.close()
413
+
414
+ def _resolve_key(self, uri: str) -> str | None:
415
+ """Map a ``file://`` URI to its backend-relative key — pure parsing.
416
+
417
+ The URI carries the full key (``{date}/{session_id}/{filename}``), so
418
+ resolution is a string strip: no catalog lookup, no disk scan, no
419
+ daemon. The key is identical across backends; the backend root is
420
+ config, so a local→remote swap is transparent and a point read needs
421
+ no index (the catalog daemon serves queries/discovery, not point
422
+ resolution). ``None`` for a non-``file://`` URI, an empty key, or one
423
+ that names a sidecar.
424
+ """
425
+ if not uri.startswith("file://"):
426
+ return None
427
+ key = uri[len("file://") :]
428
+ # Refuse empty keys + sidecars (so a caller can't read one as an artifact).
429
+ if not key or key.endswith(_SIDECAR_SUFFIX):
430
+ return None
431
+ return key
432
+
433
+ # ----- internals -------------------------------------------------
434
+
435
+ def _session_dir(self, session_id: str) -> Path:
436
+ """Resolve the on-disk directory for a session's files.
437
+
438
+ Layout: ``{data_dir}/files/{date}/{session_id}/`` where
439
+ ``date`` is the UTC date of *now* (the moment of put). This
440
+ matches the date-partitioning convention of ``events/`` and
441
+ ``channels/`` — keeps retention / ops boundaries consistent.
442
+ """
443
+ today = datetime.now(UTC).date().isoformat()
444
+ return self._files_dir / today / session_id
445
+
446
+ @staticmethod
447
+ def _unique_filename(directory: Path, stem: str, ext: str) -> str:
448
+ """Return a filename that does not collide in ``directory``.
449
+
450
+ First attempt is ``{stem}{ext}``. On collision, appends
451
+ ``_2``, ``_3``, … until an unused name is found. Preserves
452
+ claim-check immutability — a repeated put never overwrites
453
+ an existing artifact's bytes.
454
+ """
455
+ candidate = f"{stem}{ext}"
456
+ if not (directory / candidate).exists():
457
+ return candidate
458
+ n = 2
459
+ while True:
460
+ candidate = f"{stem}_{n}{ext}"
461
+ if not (directory / candidate).exists():
462
+ return candidate
463
+ n += 1