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
@@ -1,24 +1,17 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- # Timestamp: "2025-12-13 (ywatanabe)"
4
- # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_bundle.py
3
+ # Timestamp: "2025-12-16 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/bundle/_core.py
5
5
 
6
6
  """
7
- SciTeX Bundle I/O - Shared utilities for .figz, .pltz, and .statsz formats.
7
+ SciTeX Bundle Core Operations.
8
8
 
9
- This module provides common bundle operations. Domain-specific logic is in:
10
- - scitex.plt.io._bundle (pltz: hitmap, CSV, overview)
11
- - scitex.fig.io._bundle (figz: panel composition, nested pltz)
12
- - scitex.stats.io._bundle (statsz: comparison metadata)
9
+ Provides load, save, copy, pack, unpack, and validate operations for
10
+ .figz, .pltz, and .statsz bundle formats.
13
11
 
14
- Bundle formats:
15
- .figz - Publication Figure Bundle (panels + layout)
16
- .pltz - Reproducible Plot Bundle (data + spec + exports)
17
- .statsz - Statistical Results Bundle (stats + metadata)
18
-
19
- Each bundle can be:
20
- - Directory form: Figure1.figz.d/
21
- - ZIP archive form: Figure1.figz
12
+ Each bundle can exist in two forms:
13
+ - Directory: Figure1.figz.d/
14
+ - ZIP archive: Figure1.figz
22
15
  """
23
16
 
24
17
  import json
@@ -27,59 +20,55 @@ import zipfile
27
20
  from pathlib import Path
28
21
  from typing import Any, Dict, List, Optional, Union
29
22
 
23
+ from ._types import (
24
+ EXTENSIONS,
25
+ BundleNotFoundError,
26
+ BundleType,
27
+ BundleValidationError,
28
+ )
29
+
30
30
  __all__ = [
31
- "is_bundle",
32
- "load_bundle",
33
- "save_bundle",
34
- "pack_bundle",
35
- "unpack_bundle",
36
- "validate_bundle",
31
+ "load",
32
+ "save",
33
+ "copy",
34
+ "pack",
35
+ "unpack",
36
+ "validate",
37
37
  "validate_spec",
38
- "BundleType",
39
- "BundleValidationError",
40
- "BUNDLE_EXTENSIONS",
41
- "get_bundle_type",
38
+ "is_bundle",
39
+ "get_type",
42
40
  "dir_to_zip_path",
43
41
  "zip_to_dir_path",
44
42
  ]
45
43
 
46
- # Bundle extensions
47
- BUNDLE_EXTENSIONS = (".figz", ".pltz", ".statsz")
48
-
49
-
50
- class BundleValidationError(ValueError):
51
- """Error raised when bundle validation fails."""
52
- pass
53
44
 
54
-
55
- class BundleType:
56
- """Bundle type constants."""
57
- FIGZ = "figz"
58
- PLTZ = "pltz"
59
- STATSZ = "statsz"
60
-
61
-
62
- def get_bundle_type(path: Union[str, Path]) -> Optional[str]:
45
+ def get_type(path: Union[str, Path]) -> Optional[str]:
63
46
  """Get bundle type from path.
64
47
 
65
48
  Args:
66
49
  path: Path to bundle (directory or ZIP).
67
50
 
68
51
  Returns:
69
- Bundle type string or None if not a bundle.
52
+ Bundle type string ('figz', 'pltz', 'statsz') or None if not a bundle.
53
+
54
+ Example:
55
+ >>> get_type("Figure1.figz")
56
+ 'figz'
57
+ >>> get_type("plot.pltz.d")
58
+ 'pltz'
70
59
  """
71
60
  p = Path(path)
72
61
 
73
62
  # Directory bundle: ends with .figz.d, .pltz.d, .statsz.d
74
63
  if p.is_dir() and p.suffix == ".d":
75
64
  stem = p.stem # e.g., "Figure1.figz"
76
- for ext in BUNDLE_EXTENSIONS:
65
+ for ext in EXTENSIONS:
77
66
  if stem.endswith(ext):
78
67
  return ext[1:] # Remove leading dot
79
68
  return None
80
69
 
81
70
  # ZIP bundle: ends with .figz, .pltz, .statsz
82
- if p.suffix in BUNDLE_EXTENSIONS:
71
+ if p.suffix in EXTENSIONS:
83
72
  return p.suffix[1:] # Remove leading dot
84
73
 
85
74
  return None
@@ -93,14 +82,22 @@ def is_bundle(path: Union[str, Path]) -> bool:
93
82
 
94
83
  Returns:
95
84
  True if path is a bundle.
85
+
86
+ Example:
87
+ >>> is_bundle("Figure1.figz")
88
+ True
89
+ >>> is_bundle("data.csv")
90
+ False
96
91
  """
97
- return get_bundle_type(path) is not None
92
+ return get_type(path) is not None
98
93
 
99
94
 
100
95
  def dir_to_zip_path(dir_path: Path) -> Path:
101
96
  """Convert directory path to ZIP path.
102
97
 
103
- Example: Figure1.figz.d/ -> Figure1.figz
98
+ Example:
99
+ >>> dir_to_zip_path(Path("Figure1.figz.d"))
100
+ Path('Figure1.figz')
104
101
  """
105
102
  if dir_path.suffix == ".d":
106
103
  return dir_path.with_suffix("")
@@ -110,12 +107,14 @@ def dir_to_zip_path(dir_path: Path) -> Path:
110
107
  def zip_to_dir_path(zip_path: Path) -> Path:
111
108
  """Convert ZIP path to directory path.
112
109
 
113
- Example: Figure1.figz -> Figure1.figz.d/
110
+ Example:
111
+ >>> zip_to_dir_path(Path("Figure1.figz"))
112
+ Path('Figure1.figz.d')
114
113
  """
115
114
  return Path(str(zip_path) + ".d")
116
115
 
117
116
 
118
- def pack_bundle(
117
+ def pack(
119
118
  dir_path: Union[str, Path], output_path: Optional[Union[str, Path]] = None
120
119
  ) -> Path:
121
120
  """Pack a bundle directory into a ZIP archive.
@@ -129,6 +128,10 @@ def pack_bundle(
129
128
 
130
129
  Returns:
131
130
  Path to created ZIP archive.
131
+
132
+ Example:
133
+ >>> pack("plot.pltz.d")
134
+ Path('plot.pltz')
132
135
  """
133
136
  dir_path = Path(dir_path)
134
137
 
@@ -141,15 +144,12 @@ def pack_bundle(
141
144
  output_path = Path(output_path)
142
145
 
143
146
  # Get the directory name to use as top-level folder in ZIP
144
- # e.g., "stx_line.pltz.d" for path "/path/to/stx_line.pltz.d"
145
147
  dir_name = dir_path.name
146
148
 
147
149
  # Create ZIP archive with directory structure preserved
148
150
  with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
149
151
  for file_path in dir_path.rglob("*"):
150
152
  if file_path.is_file():
151
- # Include directory name in archive path
152
- # e.g., "stx_line.pltz.d/stx_line.csv"
153
153
  rel_path = file_path.relative_to(dir_path)
154
154
  arcname = Path(dir_name) / rel_path
155
155
  zf.write(file_path, arcname)
@@ -157,29 +157,28 @@ def pack_bundle(
157
157
  return output_path
158
158
 
159
159
 
160
- def unpack_bundle(
160
+ def unpack(
161
161
  zip_path: Union[str, Path], output_path: Optional[Union[str, Path]] = None
162
162
  ) -> Path:
163
163
  """Unpack a bundle ZIP archive into a directory.
164
164
 
165
- The ZIP archive contains a top-level .d directory, so extraction goes to
166
- the parent directory. E.g., stx_line.pltz extracts to create stx_line.pltz.d/
167
-
168
165
  Args:
169
166
  zip_path: Path to bundle ZIP (e.g., Figure1.figz).
170
167
  output_path: Output directory path. Auto-generated if None.
171
168
 
172
169
  Returns:
173
170
  Path to created directory.
171
+
172
+ Example:
173
+ >>> unpack("plot.pltz")
174
+ Path('plot.pltz.d')
174
175
  """
175
176
  zip_path = Path(zip_path)
176
177
 
177
178
  if not zip_path.is_file():
178
179
  raise ValueError(f"Not a file: {zip_path}")
179
180
 
180
- # Determine extraction target
181
181
  if output_path is None:
182
- # Extract to same directory as ZIP file (ZIP contains .d folder structure)
183
182
  extract_to = zip_path.parent
184
183
  expected_dir = zip_to_dir_path(zip_path)
185
184
  else:
@@ -187,7 +186,6 @@ def unpack_bundle(
187
186
  extract_to = output_path.parent
188
187
  expected_dir = output_path
189
188
 
190
- # Extract ZIP archive (contains .d directory structure)
191
189
  with zipfile.ZipFile(zip_path, "r") as zf:
192
190
  zf.extractall(extract_to)
193
191
 
@@ -227,12 +225,15 @@ def validate_spec(
227
225
  # Delegate to domain-specific validators
228
226
  if bundle_type == BundleType.FIGZ:
229
227
  from scitex.fig.io._bundle import validate_figz_spec
228
+
230
229
  errors.extend(validate_figz_spec(spec))
231
230
  elif bundle_type == BundleType.PLTZ:
232
231
  from scitex.plt.io._bundle import validate_pltz_spec
232
+
233
233
  errors.extend(validate_pltz_spec(spec))
234
234
  elif bundle_type == BundleType.STATSZ:
235
235
  from scitex.stats.io._bundle import validate_statsz_spec
236
+
236
237
  errors.extend(validate_statsz_spec(spec))
237
238
  else:
238
239
  errors.append(f"Unknown bundle type: {bundle_type}")
@@ -243,9 +244,7 @@ def validate_spec(
243
244
  return errors
244
245
 
245
246
 
246
- def validate_bundle(
247
- path: Union[str, Path], strict: bool = False
248
- ) -> Dict[str, Any]:
247
+ def validate(path: Union[str, Path], strict: bool = False) -> Dict[str, Any]:
249
248
  """Validate a bundle and return validation results.
250
249
 
251
250
  Args:
@@ -271,8 +270,7 @@ def validate_bundle(
271
270
 
272
271
  p = Path(path)
273
272
 
274
- # Check bundle type
275
- bundle_type = get_bundle_type(p)
273
+ bundle_type = get_type(p)
276
274
  if bundle_type is None:
277
275
  result["valid"] = False
278
276
  result["errors"].append(f"Not a valid bundle path: {path}")
@@ -282,9 +280,8 @@ def validate_bundle(
282
280
 
283
281
  result["bundle_type"] = bundle_type
284
282
 
285
- # Try to load and validate
286
283
  try:
287
- bundle = load_bundle(path)
284
+ bundle = load(path)
288
285
  spec = bundle.get("spec")
289
286
  result["spec"] = spec
290
287
 
@@ -307,7 +304,7 @@ def validate_bundle(
307
304
  return result
308
305
 
309
306
 
310
- def load_bundle(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any]:
307
+ def load(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any]:
311
308
  """Load bundle from directory or ZIP transparently.
312
309
 
313
310
  Args:
@@ -322,12 +319,19 @@ def load_bundle(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any
322
319
  - 'path': Original path
323
320
  - 'is_zip': Whether loaded from ZIP
324
321
  - Additional fields based on bundle type
322
+
323
+ Example:
324
+ >>> bundle = load("Figure1.figz")
325
+ >>> bundle['type']
326
+ 'figz'
327
+ >>> bundle['spec']['schema']['name']
328
+ 'scitex.fig'
325
329
  """
326
330
  p = Path(path)
327
- bundle_type = get_bundle_type(p)
331
+ bundle_type = get_type(p)
328
332
 
329
333
  if bundle_type is None:
330
- raise ValueError(f"Not a valid bundle: {path}")
334
+ raise BundleNotFoundError(f"Not a valid bundle: {path}")
331
335
 
332
336
  result = {
333
337
  "type": bundle_type,
@@ -336,15 +340,14 @@ def load_bundle(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any
336
340
  }
337
341
 
338
342
  # Handle ZIP vs directory
339
- if p.is_file() and p.suffix in BUNDLE_EXTENSIONS:
343
+ if p.is_file() and p.suffix in EXTENSIONS:
340
344
  result["is_zip"] = True
341
345
 
342
346
  if in_memory:
343
- # In-memory loading using ZipBundle
344
- from ._zip_bundle import ZipBundle
347
+ from ._zip import ZipBundle
348
+
345
349
  with ZipBundle(p, mode="r") as zb:
346
350
  result["_zip_bundle"] = zb
347
- # Load common files in-memory
348
351
  try:
349
352
  result["spec"] = zb.read_json("spec.json")
350
353
  except FileNotFoundError:
@@ -358,13 +361,12 @@ def load_bundle(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any
358
361
  except FileNotFoundError:
359
362
  result["data"] = None
360
363
 
361
- # Get file list
362
364
  result["files"] = zb.namelist()
363
365
 
364
366
  return result
365
367
  else:
366
- # Legacy: extract to temp and load
367
368
  import tempfile
369
+
368
370
  temp_dir = Path(tempfile.mkdtemp())
369
371
  with zipfile.ZipFile(p, "r") as zf:
370
372
  zf.extractall(temp_dir)
@@ -372,21 +374,24 @@ def load_bundle(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any
372
374
  else:
373
375
  bundle_dir = p
374
376
 
375
- # Delegate to domain-specific loaders (for directory bundles or legacy mode)
377
+ # Delegate to domain-specific loaders
376
378
  if bundle_type == BundleType.FIGZ:
377
379
  from scitex.fig.io._bundle import load_figz_bundle
380
+
378
381
  result.update(load_figz_bundle(bundle_dir))
379
382
  elif bundle_type == BundleType.PLTZ:
380
383
  from scitex.plt.io import load_layered_pltz_bundle
384
+
381
385
  result.update(load_layered_pltz_bundle(bundle_dir))
382
386
  elif bundle_type == BundleType.STATSZ:
383
387
  from scitex.stats.io._bundle import load_statsz_bundle
388
+
384
389
  result.update(load_statsz_bundle(bundle_dir))
385
390
 
386
391
  return result
387
392
 
388
393
 
389
- def save_bundle(
394
+ def save(
390
395
  data: Dict[str, Any],
391
396
  path: Union[str, Path],
392
397
  bundle_type: Optional[str] = None,
@@ -404,17 +409,20 @@ def save_bundle(
404
409
 
405
410
  Returns:
406
411
  Path to saved bundle.
412
+
413
+ Example:
414
+ >>> save({"spec": {...}, "data": df}, "plot.pltz", as_zip=True)
415
+ Path('plot.pltz')
407
416
  """
408
417
  p = Path(path)
409
418
 
410
- # Determine bundle type
411
419
  if bundle_type is None:
412
- bundle_type = get_bundle_type(p)
420
+ bundle_type = get_type(p)
413
421
  if bundle_type is None:
414
422
  raise ValueError(f"Cannot determine bundle type from path: {path}")
415
423
 
416
424
  # Determine if saving as directory or ZIP
417
- if as_zip or (p.suffix in BUNDLE_EXTENSIONS and not str(p).endswith(".d")):
425
+ if as_zip or (p.suffix in EXTENSIONS and not str(p).endswith(".d")):
418
426
  save_as_zip = True
419
427
  if p.suffix == ".d":
420
428
  zip_path = dir_to_zip_path(p)
@@ -428,28 +436,23 @@ def save_bundle(
428
436
  else:
429
437
  dir_path = p
430
438
 
431
- # For direct ZIP saving with atomic writes, use ZipBundle
432
- # Note: figz bundles need special handling for nested pltz panels,
433
- # so they go through the directory-based save_figz_bundle path
439
+ # For direct ZIP saving with atomic writes
434
440
  if save_as_zip and atomic and bundle_type != BundleType.FIGZ:
435
- from ._zip_bundle import ZipBundle
441
+ from ._zip import ZipBundle
436
442
 
437
443
  with ZipBundle(zip_path, mode="w") as zb:
438
- # Write spec
439
444
  if "spec" in data:
440
445
  zb.write_json("spec.json", data["spec"])
441
446
 
442
- # Write style
443
447
  if "style" in data:
444
448
  zb.write_json("style.json", data["style"])
445
449
 
446
- # Write CSV data
447
450
  if "data" in data and data["data"] is not None:
448
451
  import pandas as pd
452
+
449
453
  if isinstance(data["data"], pd.DataFrame):
450
454
  zb.write_csv("data.csv", data["data"])
451
455
 
452
- # Write exports (PNG, SVG, etc.)
453
456
  for key in ["png", "svg", "pdf"]:
454
457
  if key in data and data[key] is not None:
455
458
  export_data = data[key]
@@ -458,36 +461,104 @@ def save_bundle(
458
461
 
459
462
  return zip_path
460
463
 
461
- # Create directory for non-ZIP or non-atomic saves
462
464
  dir_path.mkdir(parents=True, exist_ok=True)
463
465
 
464
466
  # Delegate to domain-specific savers
465
467
  if bundle_type == BundleType.FIGZ:
466
468
  from scitex.fig.io._bundle import save_figz_bundle
469
+
467
470
  save_figz_bundle(data, dir_path)
468
471
  elif bundle_type == BundleType.PLTZ:
469
- # Note: This path is only reached when calling save_bundle() directly
470
- # The main stx.io.save() flow uses _save_pltz_bundle() which handles layered format
471
472
  from scitex.plt.io._bundle import save_pltz_bundle
473
+
472
474
  save_pltz_bundle(data, dir_path)
473
475
  elif bundle_type == BundleType.STATSZ:
474
476
  from scitex.stats.io._bundle import save_statsz_bundle
477
+
475
478
  save_statsz_bundle(data, dir_path)
476
479
  else:
477
480
  raise ValueError(f"Unknown bundle type: {bundle_type}")
478
481
 
479
- # Pack to ZIP if requested (non-atomic path)
480
482
  if save_as_zip:
481
- pack_bundle(dir_path, zip_path)
482
- shutil.rmtree(dir_path) # Remove temp directory
483
+ pack(dir_path, zip_path)
484
+ shutil.rmtree(dir_path)
483
485
  return zip_path
484
486
 
485
487
  return dir_path
486
488
 
487
489
 
488
- # Backward compatibility aliases
489
- _get_bundle_type = get_bundle_type
490
- _dir_to_zip_path = dir_to_zip_path
491
- _zip_to_dir_path = zip_to_dir_path
490
+ def copy(
491
+ src: Union[str, Path],
492
+ dst: Union[str, Path],
493
+ overwrite: bool = False,
494
+ ) -> Path:
495
+ """Copy a bundle from source to destination.
496
+
497
+ Handles both directory (.d) and ZIP formats transparently.
498
+ If source is ZIP, extracts to destination directory.
499
+ If source is directory, copies to destination directory.
500
+
501
+ Args:
502
+ src: Source bundle path (directory or ZIP).
503
+ dst: Destination path (will be created as directory).
504
+ overwrite: If True, overwrite existing destination.
505
+
506
+ Returns:
507
+ Path to copied bundle (directory form).
508
+
509
+ Raises:
510
+ BundleNotFoundError: If source bundle doesn't exist.
511
+ FileExistsError: If destination exists and overwrite=False.
512
+
513
+ Example:
514
+ >>> copy("gallery/line/plot.pltz.d", "my_project/A.pltz.d")
515
+ >>> copy("template.pltz", "output/panel.pltz.d")
516
+ """
517
+ src_path = Path(src)
518
+ dst_path = Path(dst)
519
+
520
+ # Validate source exists
521
+ if not src_path.exists():
522
+ if src_path.suffix in EXTENSIONS:
523
+ alt_path = Path(str(src_path) + ".d")
524
+ if alt_path.exists():
525
+ src_path = alt_path
526
+ else:
527
+ raise BundleNotFoundError(
528
+ f"Bundle not found: {src} (also tried {alt_path})"
529
+ )
530
+ elif str(src_path).endswith(".d"):
531
+ alt_path = Path(str(src_path)[:-2])
532
+ if alt_path.exists():
533
+ src_path = alt_path
534
+ else:
535
+ raise BundleNotFoundError(
536
+ f"Bundle not found: {src} (also tried {alt_path})"
537
+ )
538
+ else:
539
+ raise BundleNotFoundError(f"Bundle not found: {src}")
540
+
541
+ # Handle destination
542
+ if dst_path.exists():
543
+ if overwrite:
544
+ if dst_path.is_dir():
545
+ shutil.rmtree(dst_path)
546
+ else:
547
+ dst_path.unlink()
548
+ else:
549
+ raise FileExistsError(f"Destination exists: {dst}")
550
+
551
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
552
+
553
+ # Copy based on source type
554
+ if src_path.is_dir():
555
+ shutil.copytree(src_path, dst_path)
556
+ elif src_path.suffix in EXTENSIONS or zipfile.is_zipfile(src_path):
557
+ unpack(src_path, dst_path)
558
+ else:
559
+ raise ValueError(f"Unknown bundle format: {src_path}")
560
+
561
+ return dst_path
562
+
492
563
 
493
564
  # EOF