scitex 2.8.1__py3-none-any.whl → 2.10.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 (415) hide show
  1. scitex/__init__.py +15 -7
  2. scitex/__version__.py +1 -2
  3. scitex/_install_guide.py +250 -0
  4. scitex/_optional_deps.py +206 -39
  5. scitex/ai/_gen_ai/_Groq.py +2 -4
  6. scitex/ai/_gen_ai/_OpenAI.py +5 -2
  7. scitex/ai/_gen_ai/_Perplexity.py +20 -6
  8. scitex/audio/__init__.py +24 -15
  9. scitex/audio/_cross_process_lock.py +139 -0
  10. scitex/audio/_mcp_handlers.py +256 -0
  11. scitex/audio/_mcp_tool_schemas.py +203 -0
  12. scitex/audio/engines/elevenlabs_engine.py +5 -2
  13. scitex/audio/mcp_server.py +98 -457
  14. scitex/bridge/__init__.py +30 -19
  15. scitex/bridge/_figrecipe.py +245 -0
  16. scitex/bridge/_helpers.py +2 -1
  17. scitex/bridge/_plt_vis.py +23 -10
  18. scitex/bridge/_stats_plt.py +18 -5
  19. scitex/bridge/_stats_vis.py +16 -2
  20. scitex/browser/__init__.py +84 -44
  21. scitex/browser/automation/__init__.py +5 -1
  22. scitex/browser/core/BrowserMixin.py +17 -4
  23. scitex/browser/core/__init__.py +11 -2
  24. scitex/browser/remote/CaptchaHandler.py +1 -1
  25. scitex/browser/remote/ZenRowsAPIClient.py +1 -1
  26. scitex/capture/grid.py +487 -0
  27. scitex/capture/mcp_handlers.py +401 -0
  28. scitex/capture/mcp_tool_defs.py +192 -0
  29. scitex/capture/mcp_tools.py +241 -0
  30. scitex/capture/mcp_utils.py +30 -0
  31. scitex/cli/convert.py +421 -0
  32. scitex/cli/main.py +6 -4
  33. scitex/datetime/__init__.py +46 -0
  34. scitex/datetime/_linspace.py +100 -0
  35. scitex/datetime/_normalize_timestamp.py +306 -0
  36. scitex/db/_delete_duplicates.py +4 -4
  37. scitex/db/_sqlite3/_delete_duplicates.py +11 -2
  38. scitex/dev/plt/__init__.py +61 -62
  39. scitex/dev/plt/demo_plotters/__init__.py +0 -0
  40. scitex/dev/plt/demo_plotters/plot_mpl_axhline.py +28 -0
  41. scitex/dev/plt/demo_plotters/plot_mpl_axhspan.py +28 -0
  42. scitex/dev/plt/demo_plotters/plot_mpl_axvline.py +28 -0
  43. scitex/dev/plt/demo_plotters/plot_mpl_axvspan.py +28 -0
  44. scitex/dev/plt/demo_plotters/plot_mpl_bar.py +29 -0
  45. scitex/dev/plt/demo_plotters/plot_mpl_barh.py +29 -0
  46. scitex/dev/plt/demo_plotters/plot_mpl_boxplot.py +28 -0
  47. scitex/dev/plt/demo_plotters/plot_mpl_contour.py +31 -0
  48. scitex/dev/plt/demo_plotters/plot_mpl_contourf.py +31 -0
  49. scitex/dev/plt/demo_plotters/plot_mpl_errorbar.py +30 -0
  50. scitex/dev/plt/demo_plotters/plot_mpl_eventplot.py +28 -0
  51. scitex/dev/plt/demo_plotters/plot_mpl_fill.py +30 -0
  52. scitex/dev/plt/demo_plotters/plot_mpl_fill_between.py +31 -0
  53. scitex/dev/plt/demo_plotters/plot_mpl_hexbin.py +28 -0
  54. scitex/dev/plt/demo_plotters/plot_mpl_hist.py +28 -0
  55. scitex/dev/plt/demo_plotters/plot_mpl_hist2d.py +28 -0
  56. scitex/dev/plt/demo_plotters/plot_mpl_imshow.py +29 -0
  57. scitex/dev/plt/demo_plotters/plot_mpl_pcolormesh.py +31 -0
  58. scitex/dev/plt/demo_plotters/plot_mpl_pie.py +29 -0
  59. scitex/dev/plt/demo_plotters/plot_mpl_plot.py +29 -0
  60. scitex/dev/plt/demo_plotters/plot_mpl_quiver.py +31 -0
  61. scitex/dev/plt/demo_plotters/plot_mpl_scatter.py +28 -0
  62. scitex/dev/plt/demo_plotters/plot_mpl_stackplot.py +31 -0
  63. scitex/dev/plt/demo_plotters/plot_mpl_stem.py +29 -0
  64. scitex/dev/plt/demo_plotters/plot_mpl_step.py +29 -0
  65. scitex/dev/plt/demo_plotters/plot_mpl_violinplot.py +28 -0
  66. scitex/dev/plt/demo_plotters/plot_sns_barplot.py +29 -0
  67. scitex/dev/plt/demo_plotters/plot_sns_boxplot.py +29 -0
  68. scitex/dev/plt/demo_plotters/plot_sns_heatmap.py +28 -0
  69. scitex/dev/plt/demo_plotters/plot_sns_histplot.py +29 -0
  70. scitex/dev/plt/demo_plotters/plot_sns_kdeplot.py +29 -0
  71. scitex/dev/plt/demo_plotters/plot_sns_lineplot.py +31 -0
  72. scitex/dev/plt/demo_plotters/plot_sns_scatterplot.py +29 -0
  73. scitex/dev/plt/demo_plotters/plot_sns_stripplot.py +29 -0
  74. scitex/dev/plt/demo_plotters/plot_sns_swarmplot.py +29 -0
  75. scitex/dev/plt/demo_plotters/plot_sns_violinplot.py +29 -0
  76. scitex/dev/plt/demo_plotters/plot_stx_bar.py +29 -0
  77. scitex/dev/plt/demo_plotters/plot_stx_barh.py +29 -0
  78. scitex/dev/plt/demo_plotters/plot_stx_box.py +28 -0
  79. scitex/dev/plt/demo_plotters/plot_stx_boxplot.py +28 -0
  80. scitex/dev/plt/demo_plotters/plot_stx_conf_mat.py +28 -0
  81. scitex/dev/plt/demo_plotters/plot_stx_contour.py +31 -0
  82. scitex/dev/plt/demo_plotters/plot_stx_ecdf.py +28 -0
  83. scitex/dev/plt/demo_plotters/plot_stx_errorbar.py +30 -0
  84. scitex/dev/plt/demo_plotters/plot_stx_fill_between.py +31 -0
  85. scitex/dev/plt/demo_plotters/plot_stx_fillv.py +28 -0
  86. scitex/dev/plt/demo_plotters/plot_stx_heatmap.py +28 -0
  87. scitex/dev/plt/demo_plotters/plot_stx_image.py +28 -0
  88. scitex/dev/plt/demo_plotters/plot_stx_imshow.py +28 -0
  89. scitex/dev/plt/demo_plotters/plot_stx_joyplot.py +28 -0
  90. scitex/dev/plt/demo_plotters/plot_stx_kde.py +28 -0
  91. scitex/dev/plt/demo_plotters/plot_stx_line.py +28 -0
  92. scitex/dev/plt/demo_plotters/plot_stx_mean_ci.py +28 -0
  93. scitex/dev/plt/demo_plotters/plot_stx_mean_std.py +28 -0
  94. scitex/dev/plt/demo_plotters/plot_stx_median_iqr.py +28 -0
  95. scitex/dev/plt/demo_plotters/plot_stx_raster.py +28 -0
  96. scitex/dev/plt/demo_plotters/plot_stx_rectangle.py +28 -0
  97. scitex/dev/plt/demo_plotters/plot_stx_scatter.py +29 -0
  98. scitex/dev/plt/demo_plotters/plot_stx_shaded_line.py +29 -0
  99. scitex/dev/plt/demo_plotters/plot_stx_violin.py +28 -0
  100. scitex/dev/plt/demo_plotters/plot_stx_violinplot.py +28 -0
  101. scitex/dev/plt/mpl/get_dir_ax.py +46 -0
  102. scitex/dev/plt/mpl/get_signatures.py +176 -0
  103. scitex/dev/plt/mpl/get_signatures_details.py +522 -0
  104. scitex/dict/_pop_keys.py +1 -7
  105. scitex/dsp/__init__.py +15 -10
  106. scitex/dsp/add_noise.py +5 -2
  107. scitex/dsp/example.py +35 -22
  108. scitex/dsp/filt.py +8 -3
  109. scitex/dsp/reference.py +3 -2
  110. scitex/dsp/utils/__init__.py +2 -1
  111. scitex/dsp/utils/_differential_bandpass_filters.py +14 -4
  112. scitex/dt/__init__.py +39 -2
  113. scitex/errors.py +82 -521
  114. scitex/fig/__init__.py +4 -4
  115. scitex/fig/editor/edit/panel_loader.py +1 -1
  116. scitex/fig/io/_bundle.py +7 -7
  117. scitex/fts/README.md +262 -0
  118. scitex/fts/TODO.md +66 -0
  119. scitex/fts/__init__.py +90 -0
  120. scitex/fts/_bundle/README_IN_BUNDLE.md +102 -0
  121. scitex/fts/_bundle/_FTS.py +657 -0
  122. scitex/fts/_bundle/__init__.py +38 -0
  123. scitex/fts/_bundle/_children.py +216 -0
  124. scitex/fts/_bundle/_conversion/__init__.py +15 -0
  125. scitex/fts/_bundle/_conversion/_bundle2dict.py +44 -0
  126. scitex/fts/_bundle/_conversion/_dict2bundle.py +50 -0
  127. scitex/fts/_bundle/_dataclasses/_Axes.py +57 -0
  128. scitex/fts/_bundle/_dataclasses/_BBox.py +54 -0
  129. scitex/fts/_bundle/_dataclasses/_ColumnDef.py +72 -0
  130. scitex/fts/_bundle/_dataclasses/_DataFormat.py +40 -0
  131. scitex/fts/_bundle/_dataclasses/_DataInfo.py +135 -0
  132. scitex/fts/_bundle/_dataclasses/_DataSource.py +44 -0
  133. scitex/fts/_bundle/_dataclasses/_Node.py +319 -0
  134. scitex/fts/_bundle/_dataclasses/_NodeRefs.py +45 -0
  135. scitex/fts/_bundle/_dataclasses/_SizeMM.py +38 -0
  136. scitex/fts/_bundle/_dataclasses/__init__.py +35 -0
  137. scitex/fts/_bundle/_extractors/__init__.py +32 -0
  138. scitex/fts/_bundle/_extractors/_extract_bar.py +131 -0
  139. scitex/fts/_bundle/_extractors/_extract_line.py +71 -0
  140. scitex/fts/_bundle/_extractors/_extract_scatter.py +79 -0
  141. scitex/fts/_bundle/_loader.py +134 -0
  142. scitex/fts/_bundle/_mpl_helpers.py +389 -0
  143. scitex/fts/_bundle/_saver.py +269 -0
  144. scitex/fts/_bundle/_storage.py +200 -0
  145. scitex/fts/_bundle/_utils/__init__.py +55 -0
  146. scitex/fts/_bundle/_utils/_const.py +26 -0
  147. scitex/fts/_bundle/_utils/_errors.py +73 -0
  148. scitex/fts/_bundle/_utils/_generate.py +21 -0
  149. scitex/fts/_bundle/_utils/_types.py +76 -0
  150. scitex/fts/_bundle/_validation.py +434 -0
  151. scitex/fts/_bundle/_zipbundle.py +165 -0
  152. scitex/fts/_fig/__init__.py +22 -0
  153. scitex/fts/_fig/_backend/__init__.py +53 -0
  154. scitex/fts/_fig/_backend/_export.py +165 -0
  155. scitex/fts/_fig/_backend/_parser.py +188 -0
  156. scitex/fts/_fig/_backend/_render.py +538 -0
  157. scitex/fts/_fig/_composite.py +345 -0
  158. scitex/fts/_fig/_dataclasses/_ChannelEncoding.py +46 -0
  159. scitex/fts/_fig/_dataclasses/_Encoding.py +82 -0
  160. scitex/fts/_fig/_dataclasses/_Theme.py +441 -0
  161. scitex/fts/_fig/_dataclasses/_TraceEncoding.py +52 -0
  162. scitex/fts/_fig/_dataclasses/__init__.py +47 -0
  163. scitex/fts/_fig/_editor/__init__.py +14 -0
  164. scitex/fts/_fig/_editor/_cui/__init__.py +33 -0
  165. scitex/fts/_fig/_editor/_cui/_backend_detector.py +39 -0
  166. scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +366 -0
  167. scitex/fts/_fig/_editor/_cui/_editor_launcher.py +175 -0
  168. scitex/fts/_fig/_editor/_cui/_manual_handler.py +52 -0
  169. scitex/fts/_fig/_editor/_cui/_panel_loader.py +246 -0
  170. scitex/fts/_fig/_editor/_cui/_path_resolver.py +66 -0
  171. scitex/fts/_fig/_editor/_defaults.py +300 -0
  172. scitex/fts/_fig/_editor/_gui/__init__.py +11 -0
  173. scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +20 -0
  174. scitex/fts/_fig/_editor/_gui/_flask_editor/_bbox.py +1339 -0
  175. scitex/fts/_fig/_editor/_gui/_flask_editor/_core.py +1688 -0
  176. scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +664 -0
  177. scitex/fts/_fig/_editor/_gui/_flask_editor/_renderer.py +853 -0
  178. scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +79 -0
  179. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +41 -0
  180. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +16 -0
  181. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +85 -0
  182. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +217 -0
  183. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +93 -0
  184. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +57 -0
  185. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +112 -0
  186. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +59 -0
  187. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +212 -0
  188. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/canvas.css +176 -0
  189. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +190 -0
  190. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +59 -0
  191. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +45 -0
  192. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +95 -0
  193. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +101 -0
  194. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +138 -0
  195. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +31 -0
  196. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +7 -0
  197. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +56 -0
  198. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +78 -0
  199. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +314 -0
  200. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +107 -0
  201. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +54 -0
  202. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +172 -0
  203. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +258 -0
  204. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +48 -0
  205. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +71 -0
  206. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +288 -0
  207. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +143 -0
  208. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +245 -0
  209. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +992 -0
  210. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +339 -0
  211. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +286 -0
  212. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +371 -0
  213. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +293 -0
  214. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +426 -0
  215. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +152 -0
  216. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +265 -0
  217. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +184 -0
  218. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +57 -0
  219. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +100 -0
  220. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +34 -0
  221. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +124 -0
  222. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +851 -0
  223. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +4932 -0
  224. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +1657 -0
  225. scitex/fts/_fig/_editor/_gui/_flask_editor.py +36 -0
  226. scitex/fts/_fig/_models/_Annotations.py +115 -0
  227. scitex/fts/_fig/_models/_Axes.py +152 -0
  228. scitex/fts/_fig/_models/_Figure.py +138 -0
  229. scitex/fts/_fig/_models/_Guides.py +104 -0
  230. scitex/fts/_fig/_models/_Plot.py +123 -0
  231. scitex/fts/_fig/_models/_Styles.py +245 -0
  232. scitex/fts/_fig/_models/__init__.py +80 -0
  233. scitex/fts/_fig/_models/_plot_types/__init__.py +156 -0
  234. scitex/fts/_fig/_models/_plot_types/_bar.py +43 -0
  235. scitex/fts/_fig/_models/_plot_types/_box.py +38 -0
  236. scitex/fts/_fig/_models/_plot_types/_distribution.py +36 -0
  237. scitex/fts/_fig/_models/_plot_types/_errorbar.py +60 -0
  238. scitex/fts/_fig/_models/_plot_types/_histogram.py +30 -0
  239. scitex/fts/_fig/_models/_plot_types/_image.py +61 -0
  240. scitex/fts/_fig/_models/_plot_types/_line.py +57 -0
  241. scitex/fts/_fig/_models/_plot_types/_scatter.py +30 -0
  242. scitex/fts/_fig/_models/_plot_types/_seaborn.py +121 -0
  243. scitex/fts/_fig/_models/_plot_types/_violin.py +36 -0
  244. scitex/fts/_fig/_utils/__init__.py +129 -0
  245. scitex/fts/_fig/_utils/_auto_layout.py +127 -0
  246. scitex/fts/_fig/_utils/_calc_bounds.py +111 -0
  247. scitex/fts/_fig/_utils/_const_sizes.py +48 -0
  248. scitex/fts/_fig/_utils/_convert_coords.py +77 -0
  249. scitex/fts/_fig/_utils/_get_template.py +178 -0
  250. scitex/fts/_fig/_utils/_normalize.py +73 -0
  251. scitex/fts/_fig/_utils/_plot_layout.py +397 -0
  252. scitex/fts/_fig/_utils/_validate.py +197 -0
  253. scitex/fts/_kinds/__init__.py +45 -0
  254. scitex/fts/_kinds/_figure/__init__.py +19 -0
  255. scitex/fts/_kinds/_figure/_composite.py +345 -0
  256. scitex/fts/_kinds/_plot/__init__.py +25 -0
  257. scitex/fts/_kinds/_plot/_backend/__init__.py +53 -0
  258. scitex/fts/_kinds/_plot/_backend/_export.py +165 -0
  259. scitex/fts/_kinds/_plot/_backend/_parser.py +188 -0
  260. scitex/fts/_kinds/_plot/_backend/_render.py +538 -0
  261. scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +46 -0
  262. scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +82 -0
  263. scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +441 -0
  264. scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +52 -0
  265. scitex/fts/_kinds/_plot/_dataclasses/__init__.py +47 -0
  266. scitex/fts/_kinds/_plot/_models/_Annotations.py +115 -0
  267. scitex/fts/_kinds/_plot/_models/_Axes.py +152 -0
  268. scitex/fts/_kinds/_plot/_models/_Figure.py +138 -0
  269. scitex/fts/_kinds/_plot/_models/_Guides.py +104 -0
  270. scitex/fts/_kinds/_plot/_models/_Plot.py +123 -0
  271. scitex/fts/_kinds/_plot/_models/_Styles.py +245 -0
  272. scitex/fts/_kinds/_plot/_models/__init__.py +80 -0
  273. scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +156 -0
  274. scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +43 -0
  275. scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +38 -0
  276. scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +36 -0
  277. scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +60 -0
  278. scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +30 -0
  279. scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +61 -0
  280. scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +57 -0
  281. scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +30 -0
  282. scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +121 -0
  283. scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +36 -0
  284. scitex/fts/_kinds/_plot/_utils/__init__.py +129 -0
  285. scitex/fts/_kinds/_plot/_utils/_auto_layout.py +127 -0
  286. scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +111 -0
  287. scitex/fts/_kinds/_plot/_utils/_const_sizes.py +48 -0
  288. scitex/fts/_kinds/_plot/_utils/_convert_coords.py +77 -0
  289. scitex/fts/_kinds/_plot/_utils/_get_template.py +178 -0
  290. scitex/fts/_kinds/_plot/_utils/_normalize.py +73 -0
  291. scitex/fts/_kinds/_plot/_utils/_plot_layout.py +397 -0
  292. scitex/fts/_kinds/_plot/_utils/_validate.py +197 -0
  293. scitex/fts/_kinds/_shape/__init__.py +141 -0
  294. scitex/fts/_kinds/_stats/__init__.py +56 -0
  295. scitex/fts/_kinds/_stats/_dataclasses/_Stats.py +423 -0
  296. scitex/fts/_kinds/_stats/_dataclasses/__init__.py +48 -0
  297. scitex/fts/_kinds/_table/__init__.py +72 -0
  298. scitex/fts/_kinds/_table/_latex/__init__.py +93 -0
  299. scitex/fts/_kinds/_table/_latex/_editor/__init__.py +11 -0
  300. scitex/fts/_kinds/_table/_latex/_editor/_app.py +725 -0
  301. scitex/fts/_kinds/_table/_latex/_export.py +279 -0
  302. scitex/fts/_kinds/_table/_latex/_figure_exporter.py +153 -0
  303. scitex/fts/_kinds/_table/_latex/_stats_formatter.py +274 -0
  304. scitex/fts/_kinds/_table/_latex/_table_exporter.py +362 -0
  305. scitex/fts/_kinds/_table/_latex/_utils.py +369 -0
  306. scitex/fts/_kinds/_table/_latex/_validator.py +445 -0
  307. scitex/fts/_kinds/_text/__init__.py +77 -0
  308. scitex/fts/_schemas/data_info.schema.json +75 -0
  309. scitex/fts/_schemas/encoding.schema.json +90 -0
  310. scitex/fts/_schemas/node.schema.json +145 -0
  311. scitex/fts/_schemas/render_manifest.schema.json +62 -0
  312. scitex/fts/_schemas/stats.schema.json +132 -0
  313. scitex/fts/_schemas/theme.schema.json +141 -0
  314. scitex/fts/_stats/__init__.py +48 -0
  315. scitex/fts/_stats/_dataclasses/_Stats.py +423 -0
  316. scitex/fts/_stats/_dataclasses/__init__.py +48 -0
  317. scitex/fts/_tables/__init__.py +65 -0
  318. scitex/fts/_tables/_latex/__init__.py +93 -0
  319. scitex/fts/_tables/_latex/_editor/__init__.py +11 -0
  320. scitex/fts/_tables/_latex/_editor/_app.py +725 -0
  321. scitex/fts/_tables/_latex/_export.py +279 -0
  322. scitex/fts/_tables/_latex/_figure_exporter.py +153 -0
  323. scitex/fts/_tables/_latex/_stats_formatter.py +274 -0
  324. scitex/fts/_tables/_latex/_table_exporter.py +362 -0
  325. scitex/fts/_tables/_latex/_utils.py +369 -0
  326. scitex/fts/_tables/_latex/_validator.py +445 -0
  327. scitex/gen/__init__.py +66 -25
  328. scitex/gen/misc.py +28 -0
  329. scitex/io/__init__.py +47 -32
  330. scitex/io/_load.py +87 -36
  331. scitex/io/_load_modules/__init__.py +10 -7
  332. scitex/io/_load_modules/_pandas.py +6 -1
  333. scitex/io/_save.py +299 -1556
  334. scitex/io/_save_modules/__init__.py +76 -19
  335. scitex/io/_save_modules/_figure_utils.py +90 -0
  336. scitex/io/_save_modules/_image_csv.py +497 -0
  337. scitex/io/_save_modules/_legends.py +91 -0
  338. scitex/io/_save_modules/_pltz_bundle.py +356 -0
  339. scitex/io/_save_modules/_pltz_stx.py +536 -0
  340. scitex/io/_save_modules/_stx_bundle.py +104 -0
  341. scitex/io/_save_modules/_symlink.py +96 -0
  342. scitex/io/_save_modules/_yaml.py +1 -1
  343. scitex/io/_save_modules/_zarr.py +64 -18
  344. scitex/io/bundle/README.md +212 -0
  345. scitex/io/bundle/__init__.py +110 -0
  346. scitex/io/{_bundle.py → bundle/_core.py} +168 -97
  347. scitex/io/bundle/_nested.py +713 -0
  348. scitex/io/bundle/_types.py +74 -0
  349. scitex/io/{_zip_bundle.py → bundle/_zip.py} +93 -45
  350. scitex/io/utils/h5_to_zarr.py +1 -1
  351. scitex/logging/__init__.py +108 -13
  352. scitex/logging/_errors.py +508 -0
  353. scitex/logging/_formatters.py +30 -6
  354. scitex/logging/_warnings.py +261 -0
  355. scitex/plt/__init__.py +4 -1
  356. scitex/plt/_figrecipe.py +236 -0
  357. scitex/plt/_subplots/_AxisWrapper.py +6 -0
  358. scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +112 -1
  359. scitex/plt/_subplots/_FigWrapper.py +15 -0
  360. scitex/plt/_subplots/_SubplotsWrapper.py +125 -489
  361. scitex/plt/_subplots/_export_as_csv.py +11 -0
  362. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +2 -0
  363. scitex/plt/_subplots/_export_as_csv_formatters/_format_pcolormesh.py +66 -0
  364. scitex/plt/_subplots/_export_as_csv_formatters/_format_stackplot.py +62 -0
  365. scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +208 -0
  366. scitex/plt/_subplots/_fonts.py +71 -0
  367. scitex/plt/_subplots/_mm_layout.py +282 -0
  368. scitex/plt/gallery/__init__.py +99 -2
  369. scitex/plt/styles/_plot_postprocess.py +3 -1
  370. scitex/plt/utils/_configure_mpl.py +16 -19
  371. scitex/repro/_RandomStateManager.py +13 -8
  372. scitex/resource/__init__.py +19 -1
  373. scitex/resource/_utils/_get_env_info.py +13 -25
  374. scitex/schema/__init__.py +149 -160
  375. scitex/schema/_encoding.py +273 -0
  376. scitex/schema/_figure_elements.py +406 -0
  377. scitex/schema/_theme.py +360 -0
  378. scitex/schema/_validation.py +0 -98
  379. scitex/scholar/__init__.py +56 -14
  380. scitex/scholar/auth/ScholarAuthManager.py +1 -1
  381. scitex/scholar/auth/__init__.py +11 -2
  382. scitex/scholar/auth/providers/BaseAuthenticator.py +1 -1
  383. scitex/scholar/auth/providers/EZProxyAuthenticator.py +1 -1
  384. scitex/scholar/auth/providers/OpenAthensAuthenticator.py +1 -1
  385. scitex/scholar/auth/providers/ShibbolethAuthenticator.py +1 -1
  386. scitex/scholar/config/ScholarConfig.py +1 -1
  387. scitex/scholar/core/Scholar.py +1 -1
  388. scitex/session/_decorator.py +18 -16
  389. scitex/session/_lifecycle.py +9 -11
  390. scitex/session/template.py +9 -8
  391. scitex/sh/test_sh.py +72 -0
  392. scitex/sh/test_sh_simple.py +61 -0
  393. scitex/stats/__init__.py +221 -97
  394. scitex/stats/_schema.py +21 -22
  395. scitex/stats/descriptive/_circular.py +212 -351
  396. scitex/stats/descriptive/_describe.py +81 -132
  397. scitex/stats/descriptive/_nan.py +205 -433
  398. scitex/stats/descriptive/_real.py +127 -141
  399. scitex/str/_format_plot_text.py +5 -5
  400. scitex/str/_latex.py +26 -84
  401. scitex/str/_latex_fallback.py +53 -47
  402. scitex/web/_search_pubmed.py +5 -4
  403. scitex/writer/tests/test_diff_between.py +451 -0
  404. scitex/writer/tests/test_document_section.py +311 -0
  405. scitex/writer/tests/test_document_workflow.py +393 -0
  406. scitex/writer/tests/test_writer.py +361 -0
  407. scitex/writer/tests/test_writer_integration.py +303 -0
  408. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/METADATA +364 -181
  409. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/RECORD +412 -97
  410. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/ARCHITECTURE_EXAMPLE.md +0 -905
  411. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/BULLETIN_BOARD_EXAMPLE.md +0 -99
  412. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/PROJECT_DESCRIPTION_EXAMPLE.md +0 -96
  413. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/WHEEL +0 -0
  414. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/entry_points.txt +0 -0
  415. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_saver.py
4
+
5
+ """FTS Bundle saving utilities.
6
+
7
+ Bundle structure (IDENTICAL for all kinds):
8
+ bundle.zip/
9
+ ├── canonical/ # Source of truth (editable, human-readable)
10
+ │ ├── spec.json # {kind, children, layout, payload_schema, ...}
11
+ │ ├── encoding.json # Data-to-visual mappings
12
+ │ ├── theme.json # Visual aesthetics
13
+ │ ├── data_info.json # Column metadata
14
+ │ └── runtime.json # Runtime configuration
15
+ ├── payload/ # ALWAYS exists (empty for kind=figure)
16
+ │ ├── data.csv # Source data (for kind=plot)
17
+ │ └── stats.json # Statistics (for kind=stats)
18
+ ├── artifacts/ # Derived (can be deleted and regenerated)
19
+ │ ├── cache/
20
+ │ │ ├── geometry_px.json
21
+ │ │ ├── hitmap.png
22
+ │ │ ├── hitmap.svg
23
+ │ │ └── render_manifest.json
24
+ │ └── exports/
25
+ │ ├── figure.png
26
+ │ ├── figure.svg
27
+ │ └── figure.pdf
28
+ └── children/ # ALWAYS exists (empty for kind=plot)
29
+ └── {child_id}.zip
30
+ """
31
+
32
+ import hashlib
33
+ import io
34
+ import json
35
+ from pathlib import Path
36
+ from typing import TYPE_CHECKING, Any, Dict, Optional
37
+
38
+ from ._storage import Storage, get_storage
39
+
40
+ if TYPE_CHECKING:
41
+ from ._dataclasses import DataInfo, Node
42
+ from .._fig import Encoding, Theme
43
+ from .._stats import Stats
44
+
45
+
46
+ def save_bundle_components(
47
+ path: Path,
48
+ node: Optional["Node"] = None,
49
+ encoding: Optional["Encoding"] = None,
50
+ theme: Optional["Theme"] = None,
51
+ stats: Optional["Stats"] = None,
52
+ data_info: Optional["DataInfo"] = None,
53
+ render: bool = True,
54
+ ) -> None:
55
+ """Save all bundle components to storage.
56
+
57
+ Uses the new canonical/artifacts/payload/children structure.
58
+
59
+ Args:
60
+ path: Bundle path (directory or ZIP)
61
+ node: Node metadata (saved to canonical/spec.json)
62
+ encoding: Encoding specification (saved to canonical/encoding.json)
63
+ theme: Theme specification (saved to canonical/theme.json)
64
+ stats: Statistics (saved to payload/stats.json for kind=stats)
65
+ data_info: Data info metadata (saved to canonical/data_info.json)
66
+ render: Whether to generate exports/cache (default True)
67
+ """
68
+ storage = get_storage(path)
69
+
70
+ # Collect all files to write
71
+ files = {}
72
+
73
+ # === ALWAYS create all directories and placeholder files ===
74
+ # Use .keep files as directory markers for ZIP compatibility
75
+ files["canonical/.keep"] = ""
76
+ files["payload/.keep"] = ""
77
+ files["payload/data.csv"] = "" # Empty CSV for consistency
78
+ files["artifacts/.keep"] = ""
79
+ files["artifacts/exports/.keep"] = ""
80
+ files["artifacts/cache/.keep"] = ""
81
+ files["children/.keep"] = ""
82
+
83
+ # canonical/ - Source of truth
84
+ if node:
85
+ files["canonical/spec.json"] = json.dumps(node.to_dict(), indent=2)
86
+
87
+ if encoding:
88
+ files["canonical/encoding.json"] = encoding.to_json()
89
+
90
+ if theme:
91
+ files["canonical/theme.json"] = theme.to_json()
92
+
93
+ if data_info:
94
+ files["canonical/data_info.json"] = data_info.to_json()
95
+
96
+ # payload/ - Data files (for leaf kinds)
97
+ if stats and stats.analyses:
98
+ files["payload/stats.json"] = stats.to_json()
99
+
100
+ # Write files, preserving any existing children/ files
101
+ # For ZIP, we need to merge with existing content
102
+ if hasattr(storage, 'write_all_preserve'):
103
+ storage.write_all_preserve(files)
104
+ else:
105
+ # Fallback: write each file individually (preserves existing)
106
+ for name, data in files.items():
107
+ if isinstance(data, str):
108
+ data = data.encode("utf-8")
109
+ storage.write(name, data)
110
+
111
+
112
+ def save_render_outputs(
113
+ storage: Storage,
114
+ figure: Any, # matplotlib.figure.Figure
115
+ geometry: Dict,
116
+ source_hash: str,
117
+ theme_hash: str,
118
+ renderer_version: str = "1.0.0",
119
+ dpi: int = 300,
120
+ ) -> None:
121
+ """Save artifacts/exports/ and artifacts/cache/.
122
+
123
+ Same structure for both kind=plot and kind=figure.
124
+
125
+ Args:
126
+ storage: Bundle storage
127
+ figure: Matplotlib figure to save
128
+ geometry: Geometry data (hit areas, element positions)
129
+ source_hash: Hash of canonical/ for cache invalidation
130
+ theme_hash: Hash of effective theme (after parent overrides)
131
+ renderer_version: Renderer version for cache invalidation
132
+ dpi: Output DPI for raster formats
133
+ """
134
+ # Save artifacts/exports/
135
+ for fmt in ["png", "svg", "pdf"]:
136
+ buf = io.BytesIO()
137
+ figure.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight")
138
+ storage.write(f"artifacts/exports/figure.{fmt}", buf.getvalue())
139
+
140
+ # Save artifacts/cache/geometry_px.json
141
+ # Add coordinate space declaration
142
+ geometry_with_space = {
143
+ "space": "figure_px", # Coordinate space declaration
144
+ **geometry,
145
+ }
146
+ storage.write(
147
+ "artifacts/cache/geometry_px.json",
148
+ json.dumps(geometry_with_space, indent=2).encode(),
149
+ )
150
+
151
+ # Generate and save hitmaps
152
+ hitmap_png, hitmap_svg = generate_hitmap(figure, geometry, dpi)
153
+ if hitmap_png:
154
+ storage.write("artifacts/cache/hitmap.png", hitmap_png)
155
+ if hitmap_svg:
156
+ storage.write("artifacts/cache/hitmap.svg", hitmap_svg)
157
+
158
+ # Save render manifest (includes cache invalidation keys)
159
+ manifest = {
160
+ "dpi": dpi,
161
+ "formats": ["png", "svg", "pdf"],
162
+ # Cache invalidation keys
163
+ "canonical_hash": source_hash,
164
+ "effective_theme_hash": theme_hash,
165
+ "renderer_version": renderer_version,
166
+ }
167
+ storage.write(
168
+ "artifacts/cache/render_manifest.json",
169
+ json.dumps(manifest, indent=2).encode(),
170
+ )
171
+
172
+
173
+ def generate_hitmap(
174
+ figure: Any, # matplotlib.figure.Figure
175
+ geometry: Dict,
176
+ dpi: int = 300,
177
+ ) -> tuple:
178
+ """Generate hitmap.png and hitmap.svg for click detection.
179
+
180
+ Returns:
181
+ (hitmap_png_bytes, hitmap_svg_bytes) - bytes or None if failed
182
+ """
183
+ # Basic implementation - generate colored regions for hit testing
184
+ # This is a simplified version; full implementation would color-code elements
185
+ try:
186
+ import io
187
+
188
+ import matplotlib.pyplot as plt
189
+ import numpy as np
190
+
191
+ # Create hitmap figure with same dimensions
192
+ fig_size = figure.get_size_inches()
193
+ hitmap_fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
194
+ ax.set_xlim(0, 1)
195
+ ax.set_ylim(0, 1)
196
+ ax.axis("off")
197
+
198
+ # Draw colored regions for each element in geometry
199
+ elements = geometry.get("elements", [])
200
+ for i, elem in enumerate(elements):
201
+ if "bbox" in elem:
202
+ bbox = elem["bbox"]
203
+ color = plt.cm.tab20(i % 20)
204
+ ax.add_patch(
205
+ plt.Rectangle(
206
+ (bbox.get("x", 0), bbox.get("y", 0)),
207
+ bbox.get("width", 0.1),
208
+ bbox.get("height", 0.1),
209
+ facecolor=color,
210
+ edgecolor="none",
211
+ )
212
+ )
213
+
214
+ # Save as PNG
215
+ png_buf = io.BytesIO()
216
+ hitmap_fig.savefig(png_buf, format="png", dpi=dpi, bbox_inches="tight")
217
+ png_bytes = png_buf.getvalue()
218
+
219
+ # Save as SVG
220
+ svg_buf = io.BytesIO()
221
+ hitmap_fig.savefig(svg_buf, format="svg", bbox_inches="tight")
222
+ svg_bytes = svg_buf.getvalue()
223
+
224
+ plt.close(hitmap_fig)
225
+ return png_bytes, svg_bytes
226
+
227
+ except Exception as e:
228
+ # If hitmap generation fails, return None (non-critical)
229
+ return None, None
230
+
231
+
232
+ def compute_canonical_hash(storage: Storage) -> str:
233
+ """Compute hash of canonical/ directory for cache invalidation."""
234
+ hasher = hashlib.sha256()
235
+
236
+ canonical_files = [
237
+ "canonical/spec.json",
238
+ "canonical/encoding.json",
239
+ "canonical/theme.json",
240
+ "canonical/data_info.json",
241
+ ]
242
+
243
+ for filepath in canonical_files:
244
+ if storage.exists(filepath):
245
+ data = storage.read(filepath)
246
+ hasher.update(data)
247
+
248
+ return hasher.hexdigest()[:16]
249
+
250
+
251
+ def compute_theme_hash(theme: Optional["Theme"]) -> str:
252
+ """Compute hash of theme for cache invalidation."""
253
+ if theme is None:
254
+ return "default"
255
+
256
+ hasher = hashlib.sha256()
257
+ hasher.update(theme.to_json().encode())
258
+ return hasher.hexdigest()[:16]
259
+
260
+
261
+ __all__ = [
262
+ "save_bundle_components",
263
+ "save_render_outputs",
264
+ "generate_hitmap",
265
+ "compute_canonical_hash",
266
+ "compute_theme_hash",
267
+ ]
268
+
269
+ # EOF
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_storage.py
4
+
5
+ """
6
+ Storage Abstraction for FTS Bundles.
7
+
8
+ Provides a unified interface for reading/writing bundle contents
9
+ regardless of whether the bundle is a ZIP file or directory.
10
+
11
+ Usage:
12
+ storage = get_storage(Path("bundle.zip")) # or Path("bundle/")
13
+ data = storage.read("node.json")
14
+ storage.write("encoding.json", json_bytes)
15
+ """
16
+
17
+ import json
18
+ import zipfile
19
+ from abc import ABC, abstractmethod
20
+ from pathlib import Path
21
+ from typing import List, Optional, Union
22
+
23
+
24
+ class Storage(ABC):
25
+ """Abstract storage interface for FTS bundles."""
26
+
27
+ def __init__(self, path: Path):
28
+ self._path = path
29
+
30
+ @property
31
+ def path(self) -> Path:
32
+ """Storage path."""
33
+ return self._path
34
+
35
+ @abstractmethod
36
+ def exists(self, name: str) -> bool:
37
+ """Check if file exists in storage."""
38
+ pass
39
+
40
+ @abstractmethod
41
+ def read(self, name: str) -> bytes:
42
+ """Read file contents as bytes."""
43
+ pass
44
+
45
+ @abstractmethod
46
+ def write(self, name: str, data: bytes) -> None:
47
+ """Write data to file."""
48
+ pass
49
+
50
+ @abstractmethod
51
+ def list(self) -> List[str]:
52
+ """List all files in storage."""
53
+ pass
54
+
55
+ def read_json(self, name: str) -> Optional[dict]:
56
+ """Read and parse JSON file."""
57
+ if not self.exists(name):
58
+ return None
59
+ data = self.read(name)
60
+ return json.loads(data.decode("utf-8"))
61
+
62
+ def write_json(self, name: str, obj: dict, indent: int = 2) -> None:
63
+ """Write object as JSON file."""
64
+ data = json.dumps(obj, indent=indent).encode("utf-8")
65
+ self.write(name, data)
66
+
67
+
68
+ class ZipStorage(Storage):
69
+ """Storage implementation for ZIP files.
70
+
71
+ ZIP files are structured with a top-level directory named after the bundle.
72
+ For example, my_figure.zip contains:
73
+ my_figure/
74
+ node.json
75
+ encoding.json
76
+ theme.json
77
+ This ensures `unzip my_figure.zip` creates a my_figure/ directory.
78
+ """
79
+
80
+ @property
81
+ def _prefix(self) -> str:
82
+ """Top-level directory name inside the ZIP (bundle stem)."""
83
+ return self._path.stem + "/"
84
+
85
+ def _prefixed(self, name: str) -> str:
86
+ """Add prefix to file path."""
87
+ return self._prefix + name
88
+
89
+ def _unprefixed(self, name: str) -> str:
90
+ """Remove prefix from file path."""
91
+ if name.startswith(self._prefix):
92
+ return name[len(self._prefix) :]
93
+ return name
94
+
95
+ def exists(self, name: str) -> bool:
96
+ if not self._path.exists():
97
+ return False
98
+ with zipfile.ZipFile(self._path, "r") as zf:
99
+ # Check both prefixed and unprefixed for backwards compatibility
100
+ return self._prefixed(name) in zf.namelist() or name in zf.namelist()
101
+
102
+ def read(self, name: str) -> bytes:
103
+ with zipfile.ZipFile(self._path, "r") as zf:
104
+ # Try prefixed first, fall back to unprefixed for backwards compatibility
105
+ prefixed = self._prefixed(name)
106
+ if prefixed in zf.namelist():
107
+ return zf.read(prefixed)
108
+ return zf.read(name)
109
+
110
+ def write(self, name: str, data: bytes) -> None:
111
+ # ZIP files need special handling - read existing, add/update, rewrite
112
+ existing = {}
113
+ prefixed_name = self._prefixed(name)
114
+ if self._path.exists():
115
+ with zipfile.ZipFile(self._path, "r") as zf:
116
+ for item in zf.namelist():
117
+ if item != prefixed_name:
118
+ existing[item] = zf.read(item)
119
+
120
+ with zipfile.ZipFile(self._path, "w", zipfile.ZIP_DEFLATED) as zf:
121
+ for item_name, item_data in existing.items():
122
+ zf.writestr(item_name, item_data)
123
+ zf.writestr(prefixed_name, data)
124
+
125
+ def list(self) -> List[str]:
126
+ if not self._path.exists():
127
+ return []
128
+ with zipfile.ZipFile(self._path, "r") as zf:
129
+ # Return unprefixed names for API consistency
130
+ return [self._unprefixed(n) for n in zf.namelist() if not n.endswith("/")]
131
+
132
+ def write_all(self, files: dict) -> None:
133
+ """Write multiple files at once (more efficient for ZIP).
134
+
135
+ Files are stored under a top-level directory matching the bundle name.
136
+ """
137
+ with zipfile.ZipFile(self._path, "w", zipfile.ZIP_DEFLATED) as zf:
138
+ for name, data in files.items():
139
+ if isinstance(data, str):
140
+ data = data.encode("utf-8")
141
+ zf.writestr(self._prefixed(name), data)
142
+
143
+
144
+ class DirStorage(Storage):
145
+ """Storage implementation for directories."""
146
+
147
+ def exists(self, name: str) -> bool:
148
+ return (self._path / name).exists()
149
+
150
+ def read(self, name: str) -> bytes:
151
+ with open(self._path / name, "rb") as f:
152
+ return f.read()
153
+
154
+ def write(self, name: str, data: bytes) -> None:
155
+ file_path = self._path / name
156
+ file_path.parent.mkdir(parents=True, exist_ok=True)
157
+ with open(file_path, "wb") as f:
158
+ f.write(data)
159
+
160
+ def list(self) -> List[str]:
161
+ if not self._path.exists():
162
+ return []
163
+ result = []
164
+ for item in self._path.rglob("*"):
165
+ if item.is_file():
166
+ result.append(str(item.relative_to(self._path)))
167
+ return result
168
+
169
+ def write_all(self, files: dict) -> None:
170
+ """Write multiple files."""
171
+ self._path.mkdir(parents=True, exist_ok=True)
172
+ for name, data in files.items():
173
+ if isinstance(data, str):
174
+ data = data.encode("utf-8")
175
+ self.write(name, data)
176
+
177
+
178
+ def get_storage(path: Union[str, Path]) -> Storage:
179
+ """Get appropriate storage for path.
180
+
181
+ Args:
182
+ path: Bundle path (directory or .zip file)
183
+
184
+ Returns:
185
+ Storage instance (ZipStorage or DirStorage)
186
+ """
187
+ path = Path(path)
188
+ if path.suffix == ".zip":
189
+ return ZipStorage(path)
190
+ return DirStorage(path)
191
+
192
+
193
+ __all__ = [
194
+ "Storage",
195
+ "ZipStorage",
196
+ "DirStorage",
197
+ "get_storage",
198
+ ]
199
+
200
+ # EOF
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/__init__.py
4
+
5
+ """FTS Bundle utilities - types, constants, errors, and helpers."""
6
+
7
+ # Constants
8
+ from ._const import EXTENSIONS, SCHEMA_NAME, SCHEMA_VERSION, ZIP_EXTENSION
9
+
10
+ # Types
11
+ from ._types import (
12
+ TYPE_DEFAULTS,
13
+ BundleType,
14
+ NodeType,
15
+ get_default_constraints,
16
+ )
17
+
18
+ # Errors
19
+ from ._errors import (
20
+ BundleError,
21
+ BundleNotFoundError,
22
+ BundleValidationError,
23
+ CircularReferenceError,
24
+ ConstraintError,
25
+ DepthLimitError,
26
+ NestedBundleNotFoundError,
27
+ )
28
+
29
+ # Generation
30
+ from ._generate import generate_bundle_id
31
+
32
+ __all__ = [
33
+ # Constants
34
+ "ZIP_EXTENSION",
35
+ "EXTENSIONS",
36
+ "SCHEMA_NAME",
37
+ "SCHEMA_VERSION",
38
+ # Types
39
+ "NodeType",
40
+ "BundleType",
41
+ "TYPE_DEFAULTS",
42
+ "get_default_constraints",
43
+ # Errors
44
+ "BundleError",
45
+ "BundleValidationError",
46
+ "BundleNotFoundError",
47
+ "NestedBundleNotFoundError",
48
+ "CircularReferenceError",
49
+ "DepthLimitError",
50
+ "ConstraintError",
51
+ # Generation
52
+ "generate_bundle_id",
53
+ ]
54
+
55
+ # EOF
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_const.py
4
+
5
+ """FTS Bundle constants."""
6
+
7
+ from typing import Tuple
8
+
9
+ # Bundle extension (ZIP for portability, or directory)
10
+ ZIP_EXTENSION: str = ".zip"
11
+
12
+ # Supported formats: .zip files or directories (no extension)
13
+ EXTENSIONS: Tuple[str, ...] = (".zip",)
14
+
15
+ # Schema constants
16
+ SCHEMA_NAME = "scitex.fts"
17
+ SCHEMA_VERSION = "1.0.0"
18
+
19
+ __all__ = [
20
+ "ZIP_EXTENSION",
21
+ "EXTENSIONS",
22
+ "SCHEMA_NAME",
23
+ "SCHEMA_VERSION",
24
+ ]
25
+
26
+ # EOF
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_errors.py
4
+
5
+ """FTS Bundle error classes."""
6
+
7
+
8
+ class BundleError(Exception):
9
+ """Base exception for bundle operations."""
10
+
11
+ pass
12
+
13
+
14
+ class BundleValidationError(BundleError, ValueError):
15
+ """Error raised when bundle validation fails."""
16
+
17
+ pass
18
+
19
+
20
+ class BundleNotFoundError(BundleError, FileNotFoundError):
21
+ """Error raised when a bundle is not found."""
22
+
23
+ pass
24
+
25
+
26
+ class NestedBundleNotFoundError(BundleNotFoundError):
27
+ """Error raised when a nested bundle or file within it is not found."""
28
+
29
+ pass
30
+
31
+
32
+ class CircularReferenceError(BundleValidationError):
33
+ """Error raised when circular reference is detected in bundle hierarchy.
34
+
35
+ This occurs when a bundle references itself directly or indirectly
36
+ through its children, detected via bundle_id tracking.
37
+ """
38
+
39
+ pass
40
+
41
+
42
+ class DepthLimitError(BundleValidationError):
43
+ """Error raised when bundle nesting exceeds max_depth constraint.
44
+
45
+ The max_depth is defined per bundle type in TYPE_DEFAULTS.
46
+ Default is 3 for figures, 1 for leaf types.
47
+ """
48
+
49
+ pass
50
+
51
+
52
+ class ConstraintError(BundleValidationError):
53
+ """Error raised when bundle violates its type constraints.
54
+
55
+ Examples:
56
+ - Leaf type (plot, stats) has children
57
+ - Unknown type specified
58
+ """
59
+
60
+ pass
61
+
62
+
63
+ __all__ = [
64
+ "BundleError",
65
+ "BundleValidationError",
66
+ "BundleNotFoundError",
67
+ "NestedBundleNotFoundError",
68
+ "CircularReferenceError",
69
+ "DepthLimitError",
70
+ "ConstraintError",
71
+ ]
72
+
73
+ # EOF
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_generate.py
4
+
5
+ """FTS Bundle ID generation utilities."""
6
+
7
+ import uuid
8
+
9
+
10
+ def generate_bundle_id() -> str:
11
+ """Generate a unique bundle ID (UUID4).
12
+
13
+ Returns:
14
+ A new UUID4 string.
15
+ """
16
+ return str(uuid.uuid4())
17
+
18
+
19
+ __all__ = ["generate_bundle_id"]
20
+
21
+ # EOF