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,497 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-19
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_save_modules/_image_csv.py
4
+
5
+ """Handle image file saving with optional CSV export and auto-cropping."""
6
+
7
+ import os
8
+
9
+ from scitex import logging
10
+
11
+ from ._figure_utils import get_figure_with_data
12
+ from ._legends import save_separate_legends
13
+
14
+ # Optional: plotly-dependent save_image
15
+ try:
16
+ from ._image import save_image
17
+ except ImportError:
18
+ save_image = None
19
+ from ._symlink import symlink, symlink_to
20
+
21
+ logger = logging.getLogger()
22
+
23
+
24
+ def handle_image_with_csv(
25
+ obj,
26
+ spath,
27
+ verbose=False,
28
+ no_csv=False,
29
+ symlink_from_cwd=False,
30
+ dry_run=False,
31
+ symlink_to_path=None,
32
+ auto_crop=True,
33
+ crop_margin_mm=1.0,
34
+ metadata_extra=None,
35
+ json_schema="editable",
36
+ **kwargs,
37
+ ):
38
+ """Handle image file saving with optional CSV export and auto-cropping."""
39
+ if dry_run:
40
+ return
41
+
42
+ # Auto-collect metadata from scitex figures if not explicitly provided
43
+ collected_metadata = _collect_metadata(
44
+ obj, kwargs, verbose, json_schema, metadata_extra
45
+ )
46
+
47
+ save_image(obj, spath, verbose=verbose, **kwargs)
48
+
49
+ # Auto-crop if requested (only for raster formats)
50
+ crop_offset = _auto_crop_image(
51
+ spath, auto_crop, crop_margin_mm, collected_metadata, kwargs, verbose
52
+ )
53
+
54
+ # Handle separate legend saving
55
+ save_separate_legends(
56
+ obj,
57
+ spath,
58
+ symlink_from_cwd=symlink_from_cwd,
59
+ dry_run=dry_run,
60
+ **kwargs,
61
+ )
62
+
63
+ # Export CSV data
64
+ csv_path = None
65
+ if not no_csv:
66
+ csv_path = _export_csv_data(
67
+ obj, spath, collected_metadata, symlink_from_cwd, symlink_to_path, dry_run
68
+ )
69
+
70
+ # Save metadata as JSON
71
+ if collected_metadata is not None and not dry_run:
72
+ _save_metadata_json(
73
+ spath,
74
+ collected_metadata,
75
+ csv_path,
76
+ json_schema,
77
+ symlink_from_cwd,
78
+ symlink_to_path,
79
+ dry_run,
80
+ )
81
+
82
+
83
+ def _collect_metadata(obj, kwargs, verbose, json_schema, metadata_extra):
84
+ """Auto-collect metadata from scitex figures."""
85
+ collected_metadata = None
86
+ if "metadata" not in kwargs or kwargs["metadata"] is None:
87
+ try:
88
+ import matplotlib.figure
89
+
90
+ fig_mpl = None
91
+ if isinstance(obj, matplotlib.figure.Figure):
92
+ fig_mpl = obj
93
+ elif hasattr(obj, "_fig_mpl"):
94
+ fig_mpl = obj._fig_mpl
95
+ elif hasattr(obj, "figure") and isinstance(
96
+ obj.figure, matplotlib.figure.Figure
97
+ ):
98
+ fig_mpl = obj.figure
99
+
100
+ if fig_mpl is not None:
101
+ ax = None
102
+ if hasattr(obj, "axes"):
103
+ ax = obj.axes
104
+ elif hasattr(fig_mpl, "axes") and len(fig_mpl.axes) > 0:
105
+ mpl_ax = fig_mpl.axes[0]
106
+ if hasattr(mpl_ax, "_scitex_wrapper"):
107
+ ax = mpl_ax._scitex_wrapper
108
+ else:
109
+ ax = mpl_ax
110
+
111
+ try:
112
+ if json_schema == "editable":
113
+ from scitex.plt.utils.metadata import export_editable_figure
114
+
115
+ auto_metadata = export_editable_figure(fig_mpl)
116
+ elif json_schema == "recipe":
117
+ from scitex.plt.utils import collect_recipe_metadata
118
+
119
+ auto_metadata = collect_recipe_metadata(fig_mpl, ax)
120
+ else:
121
+ from scitex.plt.utils import collect_figure_metadata
122
+
123
+ auto_metadata = collect_figure_metadata(fig_mpl, ax)
124
+
125
+ if auto_metadata:
126
+ kwargs["metadata"] = auto_metadata
127
+ collected_metadata = auto_metadata
128
+ if verbose:
129
+ schema_names = {
130
+ "editable": "editable v0.3",
131
+ "recipe": "recipe",
132
+ "verbose": "verbose",
133
+ }
134
+ schema_name = schema_names.get(json_schema, json_schema)
135
+ logger.info(
136
+ f" • Auto-collected metadata ({schema_name} schema)"
137
+ )
138
+ except ImportError:
139
+ pass
140
+ except Exception as e:
141
+ if verbose:
142
+ logger.warning(f"Could not auto-collect metadata: {e}")
143
+ except Exception:
144
+ pass
145
+ else:
146
+ collected_metadata = kwargs.get("metadata")
147
+
148
+ # Merge metadata_extra with collected_metadata
149
+ if metadata_extra is not None and collected_metadata is not None:
150
+ import copy
151
+
152
+ collected_metadata = copy.deepcopy(collected_metadata)
153
+ if "plot_type" in metadata_extra:
154
+ collected_metadata["plot_type"] = metadata_extra["plot_type"]
155
+ if "style" in metadata_extra:
156
+ collected_metadata["style"] = metadata_extra["style"]
157
+ for key, value in metadata_extra.items():
158
+ if key not in ["plot_type", "style"]:
159
+ collected_metadata[key] = value
160
+ kwargs["metadata"] = collected_metadata
161
+
162
+ return collected_metadata
163
+
164
+
165
+ def _auto_crop_image(
166
+ spath, auto_crop, crop_margin_mm, collected_metadata, kwargs, verbose
167
+ ):
168
+ """Auto-crop image if requested."""
169
+ crop_offset = None
170
+ if auto_crop:
171
+ ext = spath.lower()
172
+ if ext.endswith((".png", ".jpg", ".jpeg", ".tiff", ".tif")):
173
+ try:
174
+ from scitex.plt.utils._crop import crop
175
+
176
+ dpi = kwargs.get("dpi", 300)
177
+ margin_px = int(crop_margin_mm * dpi / 25.4)
178
+
179
+ _, crop_offset = crop(
180
+ spath,
181
+ output_path=spath,
182
+ margin=margin_px,
183
+ overwrite=True,
184
+ verbose=False,
185
+ return_offset=True,
186
+ )
187
+
188
+ # Adjust metadata for crop offset
189
+ if crop_offset and collected_metadata:
190
+ _adjust_metadata_for_crop(collected_metadata, crop_offset)
191
+
192
+ if verbose:
193
+ logger.info(
194
+ f" • Auto-cropped with {crop_margin_mm}mm margin ({margin_px}px at {dpi} DPI)"
195
+ )
196
+
197
+ except Exception as e:
198
+ logger.warning(f"Auto-crop failed: {e}. Image saved without cropping.")
199
+
200
+ return crop_offset
201
+
202
+
203
+ def _adjust_metadata_for_crop(collected_metadata, crop_offset):
204
+ """Adjust metadata coordinates for crop offset."""
205
+ if "axes_bbox_px" in collected_metadata:
206
+ bbox = collected_metadata["axes_bbox_px"]
207
+ left_offset = crop_offset["left"]
208
+ upper_offset = crop_offset["upper"]
209
+ bbox["x0"] = bbox.get("x0", 0) - left_offset
210
+ bbox["x1"] = bbox.get("x1", 0) - left_offset
211
+ bbox["y0"] = bbox.get("y0", 0) - upper_offset
212
+ bbox["y1"] = bbox.get("y1", 0) - upper_offset
213
+
214
+ if "figure" in collected_metadata:
215
+ fig_meta = collected_metadata["figure"]
216
+ if "size_px" in fig_meta:
217
+ fig_meta["size_px"] = [
218
+ crop_offset["new_width"],
219
+ crop_offset["new_height"],
220
+ ]
221
+ if "dimensions" in collected_metadata:
222
+ dim_meta = collected_metadata["dimensions"]
223
+ if "figure_size_px" in dim_meta:
224
+ dim_meta["figure_size_px"] = [
225
+ crop_offset["new_width"],
226
+ crop_offset["new_height"],
227
+ ]
228
+
229
+
230
+ def _export_csv_data(
231
+ obj, spath, collected_metadata, symlink_from_cwd, symlink_to_path, dry_run
232
+ ):
233
+ """Export CSV data from figure."""
234
+ ext = os.path.splitext(spath)[1].lower()
235
+ image_extensions = ["png", "jpg", "jpeg", "gif", "tiff", "tif", "svg", "pdf"]
236
+ parent_dir = os.path.dirname(spath)
237
+ parent_name = os.path.basename(parent_dir)
238
+ filename_without_ext = os.path.splitext(os.path.basename(spath))[0]
239
+
240
+ csv_path = None
241
+ try:
242
+ fig_obj = get_figure_with_data(obj)
243
+
244
+ if fig_obj is not None and hasattr(fig_obj, "export_as_csv"):
245
+ csv_data = fig_obj.export_as_csv()
246
+ if csv_data is not None and not csv_data.empty:
247
+ # Determine CSV path
248
+ if parent_name.lower() in image_extensions:
249
+ grandparent_dir = os.path.dirname(parent_dir)
250
+ csv_dir = os.path.join(grandparent_dir, "csv")
251
+ csv_path = os.path.join(csv_dir, filename_without_ext + ".csv")
252
+ else:
253
+ csv_path = os.path.splitext(spath)[0] + ".csv"
254
+
255
+ os.makedirs(os.path.dirname(csv_path), exist_ok=True)
256
+
257
+ # Import here to avoid circular import
258
+ from . import save_csv
259
+
260
+ save_csv(csv_data, csv_path)
261
+
262
+ # Update metadata with CSV info
263
+ if collected_metadata is not None:
264
+ _update_metadata_with_csv(collected_metadata, csv_data, csv_path)
265
+
266
+ # Handle symlinks for CSV
267
+ _create_csv_symlinks(
268
+ csv_path, spath, symlink_from_cwd, symlink_to_path, image_extensions
269
+ )
270
+
271
+ # Also export SigmaPlot format if available
272
+ if fig_obj is not None and hasattr(fig_obj, "export_as_csv_for_sigmaplot"):
273
+ _export_sigmaplot_csv(
274
+ fig_obj,
275
+ spath,
276
+ parent_name,
277
+ parent_dir,
278
+ filename_without_ext,
279
+ symlink_from_cwd,
280
+ symlink_to_path,
281
+ image_extensions,
282
+ dry_run,
283
+ )
284
+
285
+ except Exception as e:
286
+ logger.warning(f"CSV export failed: {e}")
287
+
288
+ return csv_path
289
+
290
+
291
+ def _update_metadata_with_csv(collected_metadata, csv_data, csv_path):
292
+ """Update metadata with actual CSV info."""
293
+ try:
294
+ from scitex.plt.utils._collect_figure_metadata import _compute_csv_hash
295
+
296
+ if "data" not in collected_metadata:
297
+ collected_metadata["data"] = {}
298
+
299
+ actual_columns = list(csv_data.columns)
300
+ collected_metadata["data"]["csv_path"] = os.path.basename(csv_path)
301
+ collected_metadata["data"]["columns_actual"] = actual_columns
302
+ collected_metadata["data"]["csv_hash"] = _compute_csv_hash(csv_data)
303
+ except Exception:
304
+ pass
305
+
306
+
307
+ def _create_csv_symlinks(
308
+ csv_path, spath, symlink_from_cwd, symlink_to_path, image_extensions
309
+ ):
310
+ """Create symlinks for CSV file."""
311
+ if symlink_to_path:
312
+ symlink_parent_dir = os.path.dirname(symlink_to_path)
313
+ symlink_parent_name = os.path.basename(symlink_parent_dir)
314
+ symlink_filename_without_ext = os.path.splitext(
315
+ os.path.basename(symlink_to_path)
316
+ )[0]
317
+
318
+ if symlink_parent_name.lower() in image_extensions:
319
+ symlink_grandparent_dir = os.path.dirname(symlink_parent_dir)
320
+ csv_symlink_to = os.path.join(
321
+ symlink_grandparent_dir, "csv", symlink_filename_without_ext + ".csv"
322
+ )
323
+ else:
324
+ csv_symlink_to = os.path.splitext(symlink_to_path)[0] + ".csv"
325
+
326
+ symlink_to(csv_path, csv_symlink_to, True)
327
+
328
+ if symlink_from_cwd:
329
+ import inspect
330
+
331
+ frame_info = inspect.stack()
332
+ for frame in frame_info:
333
+ if "specified_path" in frame.frame.f_locals:
334
+ original_path = frame.frame.f_locals["specified_path"]
335
+ if isinstance(original_path, str):
336
+ orig_parent_dir = os.path.dirname(original_path)
337
+ orig_parent_name = os.path.basename(orig_parent_dir)
338
+ orig_filename_without_ext = os.path.splitext(
339
+ os.path.basename(original_path)
340
+ )[0]
341
+
342
+ if orig_parent_name.lower() in image_extensions:
343
+ orig_grandparent_dir = os.path.dirname(orig_parent_dir)
344
+ csv_relative = os.path.join(
345
+ orig_grandparent_dir,
346
+ "csv",
347
+ orig_filename_without_ext + ".csv",
348
+ )
349
+ else:
350
+ csv_relative = original_path.replace(
351
+ os.path.splitext(original_path)[1], ".csv"
352
+ )
353
+
354
+ csv_cwd = os.path.join(os.getcwd(), csv_relative)
355
+ symlink(csv_path, csv_cwd, True, True)
356
+ break
357
+ else:
358
+ csv_cwd = os.getcwd() + "/" + os.path.basename(csv_path)
359
+ symlink(csv_path, csv_cwd, True, True)
360
+
361
+
362
+ def _export_sigmaplot_csv(
363
+ fig_obj,
364
+ spath,
365
+ parent_name,
366
+ parent_dir,
367
+ filename_without_ext,
368
+ symlink_from_cwd,
369
+ symlink_to_path,
370
+ image_extensions,
371
+ dry_run,
372
+ ):
373
+ """Export SigmaPlot-formatted CSV."""
374
+ sigmaplot_data = fig_obj.export_as_csv_for_sigmaplot()
375
+ if sigmaplot_data is not None and not sigmaplot_data.empty:
376
+ if parent_name.lower() in image_extensions:
377
+ grandparent_dir = os.path.dirname(parent_dir)
378
+ csv_dir = os.path.join(grandparent_dir, "csv")
379
+ csv_sigmaplot_path = os.path.join(
380
+ csv_dir, filename_without_ext + "_for_sigmaplot.csv"
381
+ )
382
+ else:
383
+ ext = os.path.splitext(spath)[1].lower().replace(".", "")
384
+ csv_sigmaplot_path = spath.replace(ext, "csv").replace(
385
+ ".csv", "_for_sigmaplot.csv"
386
+ )
387
+
388
+ os.makedirs(os.path.dirname(csv_sigmaplot_path), exist_ok=True)
389
+ from . import save_csv
390
+
391
+ save_csv(sigmaplot_data, csv_sigmaplot_path)
392
+
393
+
394
+ def _save_metadata_json(
395
+ spath,
396
+ collected_metadata,
397
+ csv_path,
398
+ json_schema,
399
+ symlink_from_cwd,
400
+ symlink_to_path,
401
+ dry_run,
402
+ ):
403
+ """Save metadata as JSON file."""
404
+ try:
405
+ image_extensions = ["png", "jpg", "jpeg", "gif", "tiff", "tif", "svg", "pdf"]
406
+ parent_dir = os.path.dirname(spath)
407
+ parent_name = os.path.basename(parent_dir)
408
+ filename_without_ext = os.path.splitext(os.path.basename(spath))[0]
409
+
410
+ if parent_name.lower() in image_extensions:
411
+ grandparent_dir = os.path.dirname(parent_dir)
412
+ json_dir = os.path.join(grandparent_dir, "json")
413
+ json_path = os.path.join(json_dir, filename_without_ext + ".json")
414
+ else:
415
+ json_path = os.path.splitext(spath)[0] + ".json"
416
+
417
+ os.makedirs(os.path.dirname(json_path), exist_ok=True)
418
+
419
+ from . import save_json
420
+
421
+ save_json(collected_metadata, json_path)
422
+
423
+ # Verify CSV/JSON consistency for verbose schema
424
+ if csv_path and not dry_run and json_schema == "verbose":
425
+ from scitex.plt.utils._collect_figure_metadata import (
426
+ assert_csv_json_consistency,
427
+ )
428
+
429
+ assert_csv_json_consistency(csv_path, json_path)
430
+
431
+ # Create symlinks for JSON
432
+ _create_json_symlinks(
433
+ json_path, symlink_from_cwd, symlink_to_path, image_extensions
434
+ )
435
+
436
+ except AssertionError:
437
+ raise
438
+ except Exception as e:
439
+ logger.warning(f"JSON metadata export failed: {e}")
440
+
441
+
442
+ def _create_json_symlinks(
443
+ json_path, symlink_from_cwd, symlink_to_path, image_extensions
444
+ ):
445
+ """Create symlinks for JSON file."""
446
+ if symlink_to_path:
447
+ symlink_parent_dir = os.path.dirname(symlink_to_path)
448
+ symlink_parent_name = os.path.basename(symlink_parent_dir)
449
+ symlink_filename_without_ext = os.path.splitext(
450
+ os.path.basename(symlink_to_path)
451
+ )[0]
452
+
453
+ if symlink_parent_name.lower() in image_extensions:
454
+ symlink_grandparent_dir = os.path.dirname(symlink_parent_dir)
455
+ json_symlink_to = os.path.join(
456
+ symlink_grandparent_dir, "json", symlink_filename_without_ext + ".json"
457
+ )
458
+ else:
459
+ json_symlink_to = os.path.splitext(symlink_to_path)[0] + ".json"
460
+
461
+ symlink_to(json_path, json_symlink_to, True)
462
+
463
+ if symlink_from_cwd:
464
+ import inspect
465
+
466
+ frame_info = inspect.stack()
467
+ for frame in frame_info:
468
+ if "specified_path" in frame.frame.f_locals:
469
+ original_path = frame.frame.f_locals["specified_path"]
470
+ if isinstance(original_path, str):
471
+ orig_parent_dir = os.path.dirname(original_path)
472
+ orig_parent_name = os.path.basename(orig_parent_dir)
473
+ orig_filename_without_ext = os.path.splitext(
474
+ os.path.basename(original_path)
475
+ )[0]
476
+
477
+ if orig_parent_name.lower() in image_extensions:
478
+ orig_grandparent_dir = os.path.dirname(orig_parent_dir)
479
+ json_relative = os.path.join(
480
+ orig_grandparent_dir,
481
+ "json",
482
+ orig_filename_without_ext + ".json",
483
+ )
484
+ else:
485
+ json_relative = original_path.replace(
486
+ os.path.splitext(original_path)[1], ".json"
487
+ )
488
+
489
+ json_cwd = os.path.join(os.getcwd(), json_relative)
490
+ symlink(json_path, json_cwd, True, True)
491
+ break
492
+ else:
493
+ json_cwd = os.getcwd() + "/" + os.path.basename(json_path)
494
+ symlink(json_path, json_cwd, True, True)
495
+
496
+
497
+ # EOF
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-19
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/io/_save_modules/_legends.py
4
+
5
+ """Save separate legend files if ax.legend('separate') was used."""
6
+
7
+ import os
8
+
9
+ from scitex.path._getsize import getsize
10
+ from scitex.str._color_text import color_text
11
+ from scitex.str._readable_bytes import readable_bytes
12
+
13
+ # Optional: plotly-dependent save_image
14
+ try:
15
+ from ._image import save_image
16
+ except ImportError:
17
+ save_image = None
18
+
19
+
20
+ def save_separate_legends(obj, spath, symlink_from_cwd=False, dry_run=False, **kwargs):
21
+ """Save separate legend files if ax.legend('separate') was used."""
22
+ if dry_run:
23
+ return
24
+
25
+ import matplotlib.figure
26
+ import matplotlib.pyplot as plt
27
+
28
+ # Get the matplotlib figure object
29
+ fig = None
30
+ if isinstance(obj, matplotlib.figure.Figure):
31
+ fig = obj
32
+ elif hasattr(obj, "_fig_mpl"):
33
+ fig = obj._fig_mpl
34
+ elif hasattr(obj, "figure"):
35
+ if isinstance(obj.figure, matplotlib.figure.Figure):
36
+ fig = obj.figure
37
+ elif hasattr(obj.figure, "_fig_mpl"):
38
+ fig = obj.figure._fig_mpl
39
+
40
+ if fig is None:
41
+ return
42
+
43
+ # Check if there are separate legend parameters stored
44
+ if not hasattr(fig, "_separate_legend_params"):
45
+ return
46
+
47
+ # Save each legend as a separate file
48
+ base_path = os.path.splitext(spath)[0]
49
+ ext = os.path.splitext(spath)[1]
50
+
51
+ for legend_params in fig._separate_legend_params:
52
+ # Create a new figure for the legend
53
+ legend_fig = plt.figure(figsize=legend_params["figsize"])
54
+ legend_ax = legend_fig.add_subplot(111)
55
+
56
+ # Create the legend
57
+ legend = legend_ax.legend(
58
+ legend_params["handles"],
59
+ legend_params["labels"],
60
+ loc="center",
61
+ frameon=legend_params["frameon"],
62
+ fancybox=legend_params["fancybox"],
63
+ shadow=legend_params["shadow"],
64
+ **legend_params["kwargs"],
65
+ )
66
+
67
+ # Remove axes
68
+ legend_ax.axis("off")
69
+
70
+ # Adjust layout to fit the legend
71
+ legend_fig.tight_layout()
72
+
73
+ # Save the legend figure
74
+ legend_filename = f"{base_path}_{legend_params['axis_id']}_legend{ext}"
75
+ save_image(legend_fig, legend_filename, **kwargs)
76
+
77
+ # Close the legend figure to free memory
78
+ plt.close(legend_fig)
79
+
80
+ if not dry_run and os.path.exists(legend_filename):
81
+ file_size = getsize(legend_filename)
82
+ file_size = readable_bytes(file_size)
83
+ print(
84
+ color_text(
85
+ f"\nSaved legend to: {legend_filename} ({file_size})",
86
+ c="yellow",
87
+ )
88
+ )
89
+
90
+
91
+ # EOF