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,713 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-16 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/bundle/_nested.py
5
+
6
+ """
7
+ SciTeX Nested Bundle Access - Transparent access to nested bundles.
8
+
9
+ Provides unified API to access nested bundles (pltz inside figz) regardless
10
+ of whether they are stored as:
11
+ - ZIP files (.figz, .pltz)
12
+ - Directories (.figz.d, .pltz.d)
13
+ - Nested paths (Figure1.figz/A.pltz.d or Figure1.figz.d/A.pltz.d)
14
+
15
+ Usage:
16
+ from scitex.io.bundle import nested
17
+
18
+ # Get pltz bundle from inside figz (works with both ZIP and directory)
19
+ pltz_data = nested.resolve("Figure1.figz/A.pltz.d")
20
+ pltz_data = nested.resolve("Figure1.figz.d/A.pltz.d")
21
+
22
+ # Get specific file from nested bundle
23
+ png_bytes = nested.get_file("Figure1.figz/A.pltz.d/exports/plot.png")
24
+ spec = nested.get_json("Figure1.figz/A.pltz.d/spec.json")
25
+
26
+ # Get preview image (common use case)
27
+ preview_bytes = nested.get_preview("Figure1.figz/A.pltz.d")
28
+ """
29
+
30
+ import json
31
+ import zipfile
32
+ from pathlib import Path
33
+ from typing import Any, Dict, List, Optional, Tuple, Union
34
+
35
+ from ._types import NestedBundleNotFoundError
36
+
37
+ __all__ = [
38
+ "resolve",
39
+ "get_file",
40
+ "get_json",
41
+ "get_preview",
42
+ "put_file",
43
+ "put_json",
44
+ "list_files",
45
+ "parse_path",
46
+ ]
47
+
48
+
49
+ def parse_path(
50
+ path: Union[str, Path]
51
+ ) -> Tuple[Optional[Path], Optional[str], Optional[str]]:
52
+ """Parse a path to identify parent bundle, nested bundle, and file.
53
+
54
+ Handles paths like:
55
+ - Figure1.figz/A.pltz.d
56
+ - Figure1.figz.d/A.pltz.d
57
+ - Figure1.figz/A.pltz.d/exports/plot.png
58
+ - Figure1.figz.d/A.pltz.d/spec.json
59
+ - A.pltz.d (standalone)
60
+ - A.pltz (standalone ZIP)
61
+
62
+ Args:
63
+ path: Path to parse. Can be absolute or relative.
64
+
65
+ Returns:
66
+ Tuple of (parent_bundle_path, nested_bundle_name, file_path_within_nested):
67
+ - parent_bundle_path: Path to .figz or .figz.d, or None if standalone
68
+ - nested_bundle_name: Name of nested bundle (e.g., "A.pltz.d"), or None
69
+ - file_path_within_nested: Path within nested bundle, or None
70
+ """
71
+ p = Path(path)
72
+ parts = p.parts
73
+
74
+ parent_bundle = None
75
+ nested_bundle = None
76
+ file_within = None
77
+
78
+ # Find the first .figz or .figz.d component
79
+ figz_idx = None
80
+ for i, part in enumerate(parts):
81
+ if part.endswith(".figz") or part.endswith(".figz.d"):
82
+ figz_idx = i
83
+ break
84
+
85
+ if figz_idx is not None:
86
+ parent_bundle = Path(*parts[: figz_idx + 1])
87
+ remaining = parts[figz_idx + 1 :]
88
+
89
+ if remaining:
90
+ if remaining[0].endswith(".pltz.d") or remaining[0].endswith(".pltz"):
91
+ nested_bundle = remaining[0]
92
+ if len(remaining) > 1:
93
+ file_within = str(Path(*remaining[1:]))
94
+ else:
95
+ file_within = str(Path(*remaining))
96
+ else:
97
+ for i, part in enumerate(parts):
98
+ if part.endswith(".pltz.d") or part.endswith(".pltz"):
99
+ nested_bundle = part
100
+ parent_bundle = Path(*parts[:i]) if i > 0 else None
101
+ if i + 1 < len(parts):
102
+ file_within = str(Path(*parts[i + 1 :]))
103
+ break
104
+ else:
105
+ return None, None, str(p)
106
+
107
+ return parent_bundle, nested_bundle, file_within
108
+
109
+
110
+ def _find_bundle_path(base_path: Path, prefer_directory: bool = True) -> Optional[Path]:
111
+ """Find the actual bundle path (ZIP or directory).
112
+
113
+ Args:
114
+ base_path: Path to search for (with or without .d extension).
115
+ prefer_directory: If True, prefer .d directory over ZIP when both exist.
116
+ This is important for figz bundles where panels may be in the directory
117
+ while the ZIP is an older export.
118
+
119
+ Returns:
120
+ Path to the bundle (directory or ZIP), or None if not found.
121
+ """
122
+ # Check for .d directory variant
123
+ if base_path.suffix in (".figz", ".pltz", ".statsz"):
124
+ dir_path = Path(str(base_path) + ".d")
125
+ if prefer_directory and dir_path.exists():
126
+ return dir_path
127
+ if base_path.exists():
128
+ return base_path
129
+ if dir_path.exists():
130
+ return dir_path
131
+
132
+ # Check for ZIP variant
133
+ if str(base_path).endswith(".d"):
134
+ zip_path = Path(str(base_path)[:-2])
135
+ if not prefer_directory and zip_path.exists():
136
+ return zip_path
137
+ if base_path.exists():
138
+ return base_path
139
+ if zip_path.exists():
140
+ return zip_path
141
+
142
+ # Direct path
143
+ if base_path.exists():
144
+ return base_path
145
+
146
+ return None
147
+
148
+
149
+ def _read_from_zip(zip_path: Path, internal_path: str) -> bytes:
150
+ """Read file from ZIP archive, dynamically searching for nested bundles."""
151
+ with zipfile.ZipFile(zip_path, "r") as zf:
152
+ namelist = zf.namelist()
153
+
154
+ if internal_path in namelist:
155
+ return zf.read(internal_path)
156
+
157
+ for name in namelist:
158
+ if name.endswith("/" + internal_path) or name.endswith(internal_path):
159
+ return zf.read(name)
160
+
161
+ if ".pltz.d/" in internal_path:
162
+ pltz_dir_name, file_in_pltz = internal_path.split(".pltz.d/", 1)
163
+ pltz_dir = pltz_dir_name + ".pltz.d"
164
+
165
+ for name in namelist:
166
+ if f"/{pltz_dir}/" in name or name.startswith(pltz_dir + "/"):
167
+ if name.endswith("/" + file_in_pltz) or name.endswith(file_in_pltz):
168
+ return zf.read(name)
169
+
170
+ base_name = pltz_dir_name
171
+ for name in namelist:
172
+ if name.endswith(".pltz") and base_name in name:
173
+ pltz_data = zf.read(name)
174
+ return _read_from_nested_zip(pltz_data, file_in_pltz, name)
175
+
176
+ raise NestedBundleNotFoundError(
177
+ f"File not found in {zip_path}: {internal_path}\n"
178
+ f"Available: {namelist[:10]}{'...' if len(namelist) > 10 else ''}"
179
+ )
180
+
181
+
182
+ def _read_from_nested_zip(zip_data: bytes, internal_path: str, zip_name: str = "") -> bytes:
183
+ """Read file from a ZIP archive stored as bytes (nested ZIP)."""
184
+ import io
185
+
186
+ with zipfile.ZipFile(io.BytesIO(zip_data), "r") as nested_zf:
187
+ namelist = nested_zf.namelist()
188
+
189
+ if internal_path in namelist:
190
+ return nested_zf.read(internal_path)
191
+
192
+ base_name = zip_name.replace(".pltz", "") if zip_name else ""
193
+ d_prefix = f"{base_name}.pltz.d/"
194
+
195
+ full_path = d_prefix + internal_path
196
+ if full_path in namelist:
197
+ return nested_zf.read(full_path)
198
+
199
+ for name in namelist:
200
+ if name.endswith("/" + internal_path) or name.endswith(internal_path):
201
+ return nested_zf.read(name)
202
+
203
+ raise NestedBundleNotFoundError(
204
+ f"File not found in nested ZIP {zip_name}: {internal_path}\n"
205
+ f"Available: {namelist[:10]}{'...' if len(namelist) > 10 else ''}"
206
+ )
207
+
208
+
209
+ def _read_from_directory(dir_path: Path, internal_path: str) -> bytes:
210
+ """Read file from directory structure."""
211
+ file_path = dir_path / internal_path
212
+ if file_path.exists():
213
+ return file_path.read_bytes()
214
+
215
+ if ".pltz/" in internal_path:
216
+ alt_path = internal_path.replace(".pltz/", ".pltz.d/")
217
+ alt_file_path = dir_path / alt_path
218
+ if alt_file_path.exists():
219
+ return alt_file_path.read_bytes()
220
+
221
+ raise NestedBundleNotFoundError(f"File not found: {file_path}")
222
+
223
+
224
+ def get_file(path: Union[str, Path]) -> bytes:
225
+ """Get file bytes from a nested bundle path.
226
+
227
+ Transparently handles both ZIP and directory bundles.
228
+
229
+ Args:
230
+ path: Full path to file, e.g.:
231
+ - "Figure1.figz/A.pltz.d/exports/plot.png"
232
+ - "Figure1.figz.d/A.pltz.d/exports/plot.png"
233
+ - "/abs/path/Figure1.figz/A.pltz.d/spec.json"
234
+
235
+ Returns:
236
+ File contents as bytes.
237
+
238
+ Raises:
239
+ NestedBundleNotFoundError: If file or bundle not found.
240
+ """
241
+ parent_bundle, nested_bundle, file_within = parse_path(path)
242
+
243
+ if parent_bundle is None and nested_bundle is None:
244
+ p = Path(path)
245
+ if p.exists():
246
+ return p.read_bytes()
247
+ raise NestedBundleNotFoundError(f"File not found: {path}")
248
+
249
+ if parent_bundle:
250
+ actual_parent = _find_bundle_path(parent_bundle)
251
+ if actual_parent is None:
252
+ raise NestedBundleNotFoundError(f"Parent bundle not found: {parent_bundle}")
253
+
254
+ if nested_bundle and file_within:
255
+ internal_path = f"{nested_bundle}/{file_within}"
256
+ elif nested_bundle:
257
+ internal_path = nested_bundle
258
+ else:
259
+ internal_path = file_within or ""
260
+
261
+ if actual_parent.is_file():
262
+ return _read_from_zip(actual_parent, internal_path)
263
+ else:
264
+ return _read_from_directory(actual_parent, internal_path)
265
+
266
+ if nested_bundle:
267
+ bundle_path = _find_bundle_path(Path(nested_bundle))
268
+ if bundle_path is None:
269
+ raise NestedBundleNotFoundError(f"Bundle not found: {nested_bundle}")
270
+
271
+ if file_within:
272
+ if bundle_path.is_file():
273
+ return _read_from_zip(bundle_path, file_within)
274
+ else:
275
+ return _read_from_directory(bundle_path, file_within)
276
+
277
+ raise NestedBundleNotFoundError(f"Cannot resolve path: {path}")
278
+
279
+
280
+ def get_json(path: Union[str, Path]) -> Dict[str, Any]:
281
+ """Get JSON from a nested bundle path.
282
+
283
+ Args:
284
+ path: Path to JSON file within bundle.
285
+
286
+ Returns:
287
+ Parsed JSON as dictionary.
288
+ """
289
+ data = get_file(path)
290
+ return json.loads(data.decode("utf-8"))
291
+
292
+
293
+ def put_file(path: Union[str, Path], data: bytes) -> None:
294
+ """Write file bytes to a nested bundle path.
295
+
296
+ Transparently handles both ZIP and directory bundles.
297
+
298
+ Args:
299
+ path: Full path to file, e.g.:
300
+ - "Figure1.figz/A.pltz.d/exports/plot.png"
301
+ - "Figure1.figz.d/A.pltz.d/exports/plot.png"
302
+ data: File contents as bytes.
303
+
304
+ Raises:
305
+ NestedBundleNotFoundError: If bundle not found.
306
+ """
307
+ parent_bundle, nested_bundle, file_within = parse_path(path)
308
+
309
+ if parent_bundle is None and nested_bundle is None:
310
+ p = Path(path)
311
+ p.parent.mkdir(parents=True, exist_ok=True)
312
+ p.write_bytes(data)
313
+ return
314
+
315
+ if parent_bundle:
316
+ actual_parent = _find_bundle_path(parent_bundle)
317
+ if actual_parent is None:
318
+ raise NestedBundleNotFoundError(f"Parent bundle not found: {parent_bundle}")
319
+
320
+ if nested_bundle and file_within:
321
+ internal_path = f"{nested_bundle}/{file_within}"
322
+ elif nested_bundle:
323
+ internal_path = nested_bundle
324
+ else:
325
+ internal_path = file_within or ""
326
+
327
+ if actual_parent.is_file():
328
+ _write_to_zip(actual_parent, internal_path, data)
329
+ else:
330
+ _write_to_directory(actual_parent, internal_path, data)
331
+ return
332
+
333
+ if nested_bundle:
334
+ bundle_path = _find_bundle_path(Path(nested_bundle))
335
+ if bundle_path is None:
336
+ raise NestedBundleNotFoundError(f"Bundle not found: {nested_bundle}")
337
+
338
+ if file_within:
339
+ if bundle_path.is_file():
340
+ _write_to_zip(bundle_path, file_within, data)
341
+ else:
342
+ _write_to_directory(bundle_path, file_within, data)
343
+ return
344
+
345
+ raise NestedBundleNotFoundError(f"Cannot resolve path for writing: {path}")
346
+
347
+
348
+ def put_json(path: Union[str, Path], data: Dict[str, Any]) -> None:
349
+ """Write JSON to a nested bundle path.
350
+
351
+ Args:
352
+ path: Path to JSON file within bundle.
353
+ data: Dictionary to write as JSON.
354
+ """
355
+ json_bytes = json.dumps(data, indent=2).encode("utf-8")
356
+ put_file(path, json_bytes)
357
+
358
+
359
+ def _write_to_zip(zip_path: Path, internal_path: str, data: bytes) -> None:
360
+ """Write file to a ZIP archive."""
361
+ with zipfile.ZipFile(zip_path, "r") as zf:
362
+ namelist = zf.namelist()
363
+ contents = {name: zf.read(name) for name in namelist}
364
+
365
+ target_path = None
366
+ for name in namelist:
367
+ if name.endswith("/" + internal_path) or name == internal_path:
368
+ target_path = name
369
+ break
370
+ if f"/{internal_path.split('/')[0]}/" in name:
371
+ idx = name.find(f"/{internal_path.split('/')[0]}/")
372
+ prefix = name[: idx + 1]
373
+ candidate = prefix + internal_path
374
+ if candidate in namelist:
375
+ target_path = candidate
376
+ break
377
+
378
+ if target_path is None:
379
+ for name in namelist:
380
+ if internal_path.split("/")[0] + "/" in name:
381
+ idx = name.find(internal_path.split("/")[0] + "/")
382
+ if idx > 0:
383
+ target_path = name[:idx] + internal_path
384
+ break
385
+ if target_path is None:
386
+ target_path = internal_path
387
+
388
+ contents[target_path] = data
389
+
390
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
391
+ for name, file_data in contents.items():
392
+ zf.writestr(name, file_data)
393
+
394
+
395
+ def _write_to_directory(dir_path: Path, internal_path: str, data: bytes) -> None:
396
+ """Write file to directory structure."""
397
+ file_path = dir_path / internal_path
398
+ file_path.parent.mkdir(parents=True, exist_ok=True)
399
+ file_path.write_bytes(data)
400
+
401
+
402
+ def get_preview(
403
+ bundle_path: Union[str, Path],
404
+ filename: str = None,
405
+ ) -> bytes:
406
+ """Get preview PNG from a nested pltz bundle.
407
+
408
+ Handles .pltz and .pltz.d interchangeably. Does not assume the PNG
409
+ filename matches the bundle name (e.g., B.pltz.d may contain stx_scatter.png).
410
+
411
+ Search order:
412
+ 1. Specific filename if provided
413
+ 2. exports/{basename}.png (bundle name)
414
+ 3. exports/plot.png
415
+ 4. {basename}.png
416
+ 5. plot.png
417
+ 6. Any PNG in exports/ not containing 'hitmap' or 'overview'
418
+ 7. Any PNG in bundle not containing 'hitmap' or 'overview'
419
+
420
+ Args:
421
+ bundle_path: Path to pltz bundle (can be nested in figz).
422
+ Handles both .pltz and .pltz.d extensions interchangeably.
423
+ filename: Specific filename to look for (optional).
424
+
425
+ Returns:
426
+ PNG image bytes.
427
+ """
428
+ parent_bundle, nested_bundle, file_within = parse_path(bundle_path)
429
+
430
+ # Build base path, handling .pltz ↔ .pltz.d interchangeably
431
+ if parent_bundle and nested_bundle:
432
+ base_path = f"{parent_bundle}/{nested_bundle}"
433
+ elif nested_bundle:
434
+ base_path = nested_bundle
435
+ else:
436
+ base_path = str(bundle_path)
437
+
438
+ # Try alternate extension if list_files fails
439
+ def try_with_fallback(path: str) -> List[str]:
440
+ """Try to list files, with .pltz ↔ .pltz.d fallback."""
441
+ try:
442
+ return list_files(path)
443
+ except NestedBundleNotFoundError:
444
+ # Try alternate extension
445
+ if path.endswith(".pltz"):
446
+ alt_path = path + ".d"
447
+ elif path.endswith(".pltz.d"):
448
+ alt_path = path[:-2] # Remove .d
449
+ else:
450
+ raise
451
+ return list_files(alt_path)
452
+
453
+ # Find actual working path
454
+ working_path = base_path
455
+ try:
456
+ files = try_with_fallback(base_path)
457
+ # Determine which path worked
458
+ if base_path.endswith(".pltz"):
459
+ alt_path = base_path + ".d"
460
+ try:
461
+ list_files(base_path)
462
+ except NestedBundleNotFoundError:
463
+ working_path = alt_path
464
+ elif base_path.endswith(".pltz.d"):
465
+ alt_path = base_path[:-2]
466
+ try:
467
+ list_files(base_path)
468
+ except NestedBundleNotFoundError:
469
+ working_path = alt_path
470
+ except NestedBundleNotFoundError:
471
+ files = []
472
+
473
+ bundle_name = Path(working_path).stem.replace(".pltz", "")
474
+
475
+ # Standard locations to try
476
+ locations = [
477
+ f"exports/{bundle_name}.png",
478
+ "exports/plot.png",
479
+ f"{bundle_name}.png",
480
+ "plot.png",
481
+ ]
482
+
483
+ if filename:
484
+ locations.insert(0, filename)
485
+
486
+ # Try standard locations first
487
+ for loc in locations:
488
+ try:
489
+ return get_file(f"{working_path}/{loc}")
490
+ except NestedBundleNotFoundError:
491
+ continue
492
+
493
+ # Fallback: find ANY suitable PNG in the bundle
494
+ # Prioritize exports/ directory, then root
495
+ exports_pngs = []
496
+ root_pngs = []
497
+
498
+ for f in files:
499
+ if f.endswith(".png") and "hitmap" not in f and "overview" not in f:
500
+ if f.startswith("exports/"):
501
+ exports_pngs.append(f)
502
+ else:
503
+ root_pngs.append(f)
504
+
505
+ # Try exports/ PNGs first
506
+ for f in exports_pngs:
507
+ try:
508
+ return get_file(f"{working_path}/{f}")
509
+ except NestedBundleNotFoundError:
510
+ continue
511
+
512
+ # Then try root PNGs
513
+ for f in root_pngs:
514
+ try:
515
+ return get_file(f"{working_path}/{f}")
516
+ except NestedBundleNotFoundError:
517
+ continue
518
+
519
+ raise NestedBundleNotFoundError(
520
+ f"No preview image found in {bundle_path}. "
521
+ f"Tried: {locations}, then searched {len(files)} files"
522
+ )
523
+
524
+
525
+ def list_files(bundle_path: Union[str, Path]) -> List[str]:
526
+ """List files in a nested bundle.
527
+
528
+ Args:
529
+ bundle_path: Path to bundle (nested or standalone).
530
+
531
+ Returns:
532
+ List of file paths relative to bundle root.
533
+ """
534
+ parent_bundle, nested_bundle, _ = parse_path(bundle_path)
535
+
536
+ if parent_bundle:
537
+ actual_parent = _find_bundle_path(parent_bundle)
538
+ if actual_parent is None:
539
+ raise NestedBundleNotFoundError(f"Bundle not found: {parent_bundle}")
540
+
541
+ if actual_parent.is_file():
542
+ with zipfile.ZipFile(actual_parent, "r") as zf:
543
+ namelist = zf.namelist()
544
+
545
+ if nested_bundle:
546
+ files = []
547
+
548
+ for name in namelist:
549
+ if f"/{nested_bundle}/" in name or name.startswith(
550
+ nested_bundle + "/"
551
+ ):
552
+ if name.startswith(nested_bundle + "/"):
553
+ rel = name[len(nested_bundle) + 1 :]
554
+ else:
555
+ idx = name.find(f"/{nested_bundle}/")
556
+ rel = name[idx + len(nested_bundle) + 2 :]
557
+ if rel and not rel.endswith("/"):
558
+ files.append(rel)
559
+
560
+ if files:
561
+ return files
562
+
563
+ if nested_bundle.endswith(".pltz.d"):
564
+ base_name = nested_bundle[:-7]
565
+ for name in namelist:
566
+ if name.endswith(".pltz") and base_name in name:
567
+ pltz_data = zf.read(name)
568
+ return _list_nested_zip_files(pltz_data, name)
569
+
570
+ return files
571
+ else:
572
+ return [n for n in namelist if not n.endswith("/")]
573
+ else:
574
+ target = actual_parent
575
+ if nested_bundle:
576
+ # Prefer .d directory over ZIP when both exist
577
+ if nested_bundle.endswith(".pltz"):
578
+ dir_target = actual_parent / (nested_bundle + ".d")
579
+ if dir_target.exists():
580
+ target = dir_target
581
+ else:
582
+ target = actual_parent / nested_bundle
583
+ elif nested_bundle.endswith(".pltz.d"):
584
+ target = actual_parent / nested_bundle
585
+ if not target.exists():
586
+ # Try without .d
587
+ zip_target = actual_parent / nested_bundle[:-2]
588
+ if zip_target.exists():
589
+ target = zip_target
590
+ else:
591
+ target = actual_parent / nested_bundle
592
+
593
+ if not target.exists():
594
+ raise NestedBundleNotFoundError(f"Bundle not found: {target}")
595
+
596
+ return [str(f.relative_to(target)) for f in target.rglob("*") if f.is_file()]
597
+
598
+ bundle = Path(bundle_path)
599
+ actual = _find_bundle_path(bundle)
600
+ if actual is None:
601
+ raise NestedBundleNotFoundError(f"Bundle not found: {bundle_path}")
602
+
603
+ if actual.is_file():
604
+ with zipfile.ZipFile(actual, "r") as zf:
605
+ return [n for n in zf.namelist() if not n.endswith("/")]
606
+ else:
607
+ return [str(f.relative_to(actual)) for f in actual.rglob("*") if f.is_file()]
608
+
609
+
610
+ def _list_nested_zip_files(zip_data: bytes, zip_name: str = "") -> List[str]:
611
+ """List files in a nested ZIP archive."""
612
+ import io
613
+
614
+ with zipfile.ZipFile(io.BytesIO(zip_data), "r") as nested_zf:
615
+ namelist = nested_zf.namelist()
616
+
617
+ base_name = zip_name.replace(".pltz", "") if zip_name else ""
618
+ d_prefix = f"{base_name}.pltz.d/"
619
+
620
+ files = []
621
+ for name in namelist:
622
+ if name.startswith(d_prefix):
623
+ rel = name[len(d_prefix) :]
624
+ if rel and not rel.endswith("/"):
625
+ files.append(rel)
626
+ elif not name.endswith("/"):
627
+ files.append(name)
628
+
629
+ return files
630
+
631
+
632
+ def resolve(
633
+ bundle_path: Union[str, Path],
634
+ extract_to: Optional[Path] = None,
635
+ ) -> Dict[str, Any]:
636
+ """Load a nested bundle's data.
637
+
638
+ Transparently handles:
639
+ - Standalone .pltz or .pltz.d
640
+ - Nested .pltz.d inside .figz
641
+ - Nested .pltz.d inside .figz.d
642
+
643
+ Args:
644
+ bundle_path: Path to bundle, e.g.:
645
+ - "Figure1.figz/A.pltz.d"
646
+ - "Figure1.figz.d/A.pltz.d"
647
+ - "A.pltz.d"
648
+ - "A.pltz"
649
+ extract_to: If provided, extract ZIP contents to this directory.
650
+
651
+ Returns:
652
+ Dictionary with bundle data:
653
+ - 'spec': Parsed spec.json
654
+ - 'style': Parsed style.json (if exists)
655
+ - 'data': CSV data as DataFrame (if exists)
656
+ - 'path': Original path
657
+ - 'is_nested': Whether bundle is nested in another
658
+ - 'files': List of files in bundle
659
+ """
660
+ result = {
661
+ "path": str(bundle_path),
662
+ "is_nested": False,
663
+ "spec": None,
664
+ "style": None,
665
+ "data": None,
666
+ "files": [],
667
+ }
668
+
669
+ parent_bundle, nested_bundle, _ = parse_path(bundle_path)
670
+ result["is_nested"] = parent_bundle is not None and nested_bundle is not None
671
+
672
+ try:
673
+ result["files"] = list_files(bundle_path)
674
+ except Exception:
675
+ result["files"] = []
676
+
677
+ try:
678
+ result["spec"] = get_json(f"{bundle_path}/spec.json")
679
+ except NestedBundleNotFoundError:
680
+ bundle_name = Path(bundle_path).stem.replace(".pltz", "")
681
+ try:
682
+ result["spec"] = get_json(f"{bundle_path}/{bundle_name}.json")
683
+ except NestedBundleNotFoundError:
684
+ pass
685
+
686
+ try:
687
+ result["style"] = get_json(f"{bundle_path}/style.json")
688
+ except NestedBundleNotFoundError:
689
+ pass
690
+
691
+ try:
692
+ import io
693
+
694
+ import pandas as pd
695
+
696
+ csv_bytes = get_file(f"{bundle_path}/data.csv")
697
+ result["data"] = pd.read_csv(io.BytesIO(csv_bytes))
698
+ except (NestedBundleNotFoundError, ImportError):
699
+ bundle_name = Path(bundle_path).stem.replace(".pltz", "")
700
+ try:
701
+ import io
702
+
703
+ import pandas as pd
704
+
705
+ csv_bytes = get_file(f"{bundle_path}/{bundle_name}.csv")
706
+ result["data"] = pd.read_csv(io.BytesIO(csv_bytes))
707
+ except (NestedBundleNotFoundError, ImportError):
708
+ pass
709
+
710
+ return result
711
+
712
+
713
+ # EOF