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,657 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_FTS.py
4
+
5
+ """FTS Bundle Class - Main entry point for FTS bundles.
6
+
7
+ Structure (identical for all kinds):
8
+ - canonical/: Source of truth (spec.json, encoding.json, theme.json)
9
+ - payload/: Data files (empty for composites)
10
+ - artifacts/: Exports and cache
11
+ - children/: Embedded child bundles (empty for leaves)
12
+ """
13
+
14
+ import uuid
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
17
+
18
+ from ._children import ValidationError, embed_child, load_embedded_children
19
+ from ._dataclasses import DataInfo, Node, SizeMM
20
+ from ._loader import load_bundle_components
21
+ from ._validation import ValidationResult
22
+ from ._saver import compute_canonical_hash, compute_theme_hash, save_bundle_components, save_render_outputs
23
+ from ._storage import Storage, get_storage
24
+ from .._fig import Encoding, Theme
25
+ from .._stats import Stats
26
+
27
+ if TYPE_CHECKING:
28
+ from matplotlib.figure import Figure as MplFigure
29
+
30
+
31
+ class FTS:
32
+ """Figure-Table-Statistics Bundle - Self-contained figure/plot/stats package.
33
+
34
+ Attributes:
35
+ node: Node metadata (kind, children, layout, payload_schema, etc.)
36
+ encoding: Encoding specification (traces, channels)
37
+ theme: Theme specification (colors, fonts)
38
+ stats: Statistics (for kind=stats)
39
+ data_info: Data info metadata
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ path: Union[str, Path],
45
+ create: bool = False,
46
+ kind: str = "plot",
47
+ name: Optional[str] = None,
48
+ size_mm: Optional[Dict[str, float]] = None,
49
+ # Legacy support
50
+ node_type: Optional[str] = None,
51
+ ):
52
+ """Initialize FTS bundle.
53
+
54
+ Args:
55
+ path: Bundle path (directory or .zip file)
56
+ create: If True, create new bundle; if False, load existing
57
+ kind: Bundle kind (plot, figure, table, stats, group, collection)
58
+ name: Bundle name (default: stem of path)
59
+ size_mm: Figure size in mm (e.g., {"width": 170, "height": 85})
60
+ node_type: DEPRECATED - use 'kind' instead
61
+ """
62
+ self._path = Path(path)
63
+ self._is_zip = self._path.suffix == ".zip"
64
+ self._node: Optional[Node] = None
65
+ self._encoding: Optional[Encoding] = None
66
+ self._theme: Optional[Theme] = None
67
+ self._stats: Optional[Stats] = None
68
+ self._data_info: Optional[DataInfo] = None
69
+ self._dirty = False
70
+ self._storage: Optional[Storage] = None
71
+
72
+ # Handle legacy node_type parameter
73
+ if node_type is not None:
74
+ kind = node_type
75
+
76
+ if create:
77
+ self._create_new(kind, name, size_mm)
78
+ else:
79
+ self._load()
80
+
81
+ @property
82
+ def path(self) -> Path:
83
+ """Bundle path (directory or ZIP)."""
84
+ return self._path
85
+
86
+ @property
87
+ def is_zip(self) -> bool:
88
+ """Whether bundle is a ZIP file."""
89
+ return self._is_zip
90
+
91
+ @property
92
+ def bundle_type(self) -> str:
93
+ """Bundle kind (figure, plot, table, etc.)."""
94
+ return self._node.kind if self._node else "unknown"
95
+
96
+ @property
97
+ def is_dirty(self) -> bool:
98
+ """Whether bundle has unsaved changes."""
99
+ return self._dirty
100
+
101
+ @property
102
+ def storage(self) -> Storage:
103
+ """Get storage for this bundle."""
104
+ if self._storage is None:
105
+ self._storage = get_storage(self._path)
106
+ return self._storage
107
+
108
+ @property
109
+ def node(self) -> Optional[Node]:
110
+ """Node metadata."""
111
+ return self._node
112
+
113
+ @node.setter
114
+ def node(self, value: Union[Node, Dict[str, Any]]):
115
+ if isinstance(value, dict):
116
+ self._node = Node.from_dict(value)
117
+ else:
118
+ self._node = value
119
+ self._dirty = True
120
+
121
+ @property
122
+ def encoding(self) -> Optional[Encoding]:
123
+ """Encoding specification (typed object)."""
124
+ return self._encoding
125
+
126
+ @encoding.setter
127
+ def encoding(self, value: Union[Encoding, Dict[str, Any]]):
128
+ if isinstance(value, dict):
129
+ self._encoding = Encoding.from_dict(value)
130
+ else:
131
+ self._encoding = value
132
+ self._dirty = True
133
+
134
+ @property
135
+ def encoding_dict(self) -> Optional[Dict[str, Any]]:
136
+ """Encoding as dictionary (for serialization)."""
137
+ return self._encoding.to_dict() if self._encoding else None
138
+
139
+ @property
140
+ def theme(self) -> Optional[Theme]:
141
+ """Theme specification (typed object)."""
142
+ return self._theme
143
+
144
+ @theme.setter
145
+ def theme(self, value: Union[Theme, Dict[str, Any]]):
146
+ if isinstance(value, dict):
147
+ self._theme = Theme.from_dict(value)
148
+ else:
149
+ self._theme = value
150
+ self._dirty = True
151
+
152
+ @property
153
+ def theme_dict(self) -> Optional[Dict[str, Any]]:
154
+ """Theme as dictionary (for serialization)."""
155
+ return self._theme.to_dict() if self._theme else None
156
+
157
+ @property
158
+ def stats(self) -> Optional[Stats]:
159
+ """Statistics."""
160
+ return self._stats
161
+
162
+ @stats.setter
163
+ def stats(self, value: Union[Stats, Dict[str, Any]]):
164
+ if isinstance(value, dict):
165
+ self._stats = Stats.from_dict(value)
166
+ else:
167
+ self._stats = value
168
+ self._dirty = True
169
+
170
+ @property
171
+ def data_info(self) -> Optional[DataInfo]:
172
+ """Data info metadata."""
173
+ return self._data_info
174
+
175
+ @data_info.setter
176
+ def data_info(self, value: Union[DataInfo, Dict[str, Any]]):
177
+ if isinstance(value, dict):
178
+ self._data_info = DataInfo.from_dict(value)
179
+ else:
180
+ self._data_info = value
181
+ self._dirty = True
182
+
183
+ def _create_new(
184
+ self,
185
+ kind: str,
186
+ name: Optional[str],
187
+ size_mm: Optional[Dict[str, float]],
188
+ ):
189
+ """Create a new bundle."""
190
+ bundle_id = str(uuid.uuid4())
191
+ if name is None:
192
+ name = self._path.stem
193
+
194
+ # Determine payload_schema for leaf kinds
195
+ # Note: payload_schema is optional. For plots without data, it's None.
196
+ # For plots with data, from_matplotlib will set it.
197
+ payload_schema = None
198
+ if kind in Node.LEAF_KINDS and kind != "plot":
199
+ # Only auto-set for non-plot leaf kinds
200
+ payload_schema_map = {
201
+ "table": "scitex.fts.payload.table@1",
202
+ "stats": "scitex.fts.payload.stats@1",
203
+ }
204
+ payload_schema = payload_schema_map.get(kind)
205
+
206
+ self._node = Node(
207
+ id=bundle_id,
208
+ kind=kind,
209
+ name=name,
210
+ size_mm=SizeMM.from_dict(size_mm) if size_mm else None,
211
+ payload_schema=payload_schema,
212
+ )
213
+ self._encoding = Encoding()
214
+ self._theme = Theme()
215
+ self._stats = Stats()
216
+ self._dirty = True
217
+
218
+ def _load(self):
219
+ """Load existing bundle."""
220
+ if not self._path.exists():
221
+ raise FileNotFoundError(f"FTS bundle not found: {self._path}")
222
+
223
+ (
224
+ self._node,
225
+ self._encoding,
226
+ self._theme,
227
+ self._stats,
228
+ self._data_info,
229
+ ) = load_bundle_components(self._path)
230
+
231
+ def add_child(
232
+ self,
233
+ child: Union[str, Path, "FTS"],
234
+ row: int = 0,
235
+ col: int = 0,
236
+ label: Optional[str] = None,
237
+ row_span: int = 1,
238
+ col_span: int = 1,
239
+ **kwargs,
240
+ ) -> str:
241
+ """Add and embed a child bundle. Returns child_name in children/."""
242
+ if not self.node.is_composite_kind():
243
+ raise TypeError(f"kind={self.node.kind} cannot have children")
244
+
245
+ # Get child path
246
+ if isinstance(child, FTS):
247
+ child_path = child.path
248
+ else:
249
+ child_path = Path(child)
250
+
251
+ # Embed child into children/ directory
252
+ # Returns (child_name, child_id) tuple
253
+ child_name, child_id = embed_child(self.storage, child_path)
254
+
255
+ # Add to node.children
256
+ self._node.children.append(child_name)
257
+
258
+ # Initialize layout if needed
259
+ if self._node.layout is None:
260
+ self._node.layout = {"rows": 2, "cols": 2, "panels": []}
261
+
262
+ # Update grid size if needed
263
+ self._node.layout["rows"] = max(self._node.layout.get("rows", 1), row + row_span)
264
+ self._node.layout["cols"] = max(self._node.layout.get("cols", 1), col + col_span)
265
+
266
+ # Add to layout.panels
267
+ panel_info = {
268
+ "child": child_name,
269
+ "child_id": child_id, # Full UUID for identity tracking
270
+ "row": row,
271
+ "col": col,
272
+ "row_span": row_span,
273
+ "col_span": col_span,
274
+ **kwargs,
275
+ }
276
+ if label:
277
+ panel_info["label"] = label
278
+
279
+ self._node.layout["panels"].append(panel_info)
280
+ self._dirty = True
281
+
282
+ return child_name
283
+
284
+ def load_children(self) -> Dict[str, "FTS"]:
285
+ """Load embedded children. Returns dict: child_name -> FTS."""
286
+ return load_embedded_children(self._path)
287
+
288
+ def render(self) -> Optional["MplFigure"]:
289
+ """Render figure. Composite renders children, leaf renders from encoding."""
290
+ if self._node is None:
291
+ return None
292
+
293
+ if self._node.is_composite_kind():
294
+ return self._render_composite()
295
+ elif self._node.is_data_leaf_kind():
296
+ # Data kinds (plot, table, stats) need payload data
297
+ return self._render_from_encoding()
298
+ elif self._node.is_annotation_leaf_kind():
299
+ # Annotation kinds (text, shape) render from node params
300
+ return self._render_annotation()
301
+ elif self._node.is_image_leaf_kind():
302
+ # Image kinds render from payload image
303
+ return self._render_image()
304
+
305
+ return None
306
+
307
+ def _render_composite(self) -> Optional["MplFigure"]:
308
+ """Render composite figure with children."""
309
+ import scitex.plt as splt
310
+
311
+ size_mm = self._node.size_mm.to_dict() if self._node.size_mm else {"width": 170, "height": 100}
312
+
313
+ # Get background color from theme
314
+ bg_color = "#ffffff"
315
+ if self._theme and self._theme.colors:
316
+ bg_color = self._theme.colors.background or "#ffffff"
317
+
318
+ if not self._node.children:
319
+ # Empty container - render blank figure with specified size and background
320
+ fig, ax = splt.subplots(
321
+ figsize_mm=(size_mm.get("width", 170), size_mm.get("height", 100))
322
+ )
323
+ fig.set_facecolor(bg_color)
324
+ ax.set_facecolor(bg_color)
325
+ ax.set_axis_off()
326
+ return fig
327
+
328
+ from .._fig._composite import render_composite
329
+
330
+ children = self.load_children()
331
+
332
+ fig, geometry = render_composite(
333
+ children=children,
334
+ layout=self._node.layout or {"rows": 1, "cols": 1, "panels": []},
335
+ size_mm=size_mm,
336
+ theme=self._theme,
337
+ )
338
+
339
+ return fig
340
+
341
+ def _render_from_encoding(self) -> Optional["MplFigure"]:
342
+ """Render leaf figure from encoding + payload."""
343
+ if self._encoding is None:
344
+ return None
345
+
346
+ import scitex.plt as splt
347
+
348
+ size_mm = self._node.size_mm.to_dict() if self._node.size_mm else {"width": 85, "height": 85}
349
+
350
+ # Use scitex.plt for proper styling (3-4 ticks, etc.)
351
+ fig, ax = splt.subplots(
352
+ figsize_mm=(size_mm.get("width", 85), size_mm.get("height", 85))
353
+ )
354
+
355
+ # Load data from payload
356
+ data = self._load_payload_data()
357
+
358
+ # Render traces
359
+ from .._fig._backend._render import render_traces
360
+
361
+ traces = self._encoding.traces if self._encoding.traces else []
362
+ for trace in traces:
363
+ render_traces(ax, trace, data, self._theme)
364
+
365
+ # Apply labels from encoding axes config (if available)
366
+ # Note: Unit validation happens in scitex.plt via UnitAwareMixin.set_xlabel/set_ylabel
367
+ if self._encoding.axes:
368
+ if "x" in self._encoding.axes and self._encoding.axes["x"].title:
369
+ ax.set_xlabel(self._encoding.axes["x"].title)
370
+ if "y" in self._encoding.axes and self._encoding.axes["y"].title:
371
+ ax.set_ylabel(self._encoding.axes["y"].title)
372
+
373
+ fig.tight_layout()
374
+ return fig
375
+
376
+ def _load_payload_data(self) -> Optional["pd.DataFrame"]:
377
+ """Load data from payload/data.csv or legacy data/data.csv."""
378
+ import pandas as pd
379
+ from io import StringIO
380
+
381
+ # Try new path first, then legacy
382
+ for path in ["payload/data.csv", "data/data.csv"]:
383
+ if self.storage.exists(path):
384
+ csv_bytes = self.storage.read(path)
385
+ # Handle empty CSV files
386
+ if not csv_bytes.strip():
387
+ return None
388
+ return pd.read_csv(StringIO(csv_bytes.decode("utf-8")))
389
+ return None
390
+
391
+ def _render_annotation(self) -> Optional["MplFigure"]:
392
+ """Render annotation (text/shape) from node parameters."""
393
+ import scitex.plt as splt
394
+
395
+ size_mm = self._node.size_mm.to_dict() if self._node.size_mm else {"width": 85, "height": 85}
396
+
397
+ fig, ax = splt.subplots(
398
+ figsize_mm=(size_mm.get("width", 85), size_mm.get("height", 85))
399
+ )
400
+
401
+ # Get background color
402
+ bg_color = "#ffffff"
403
+ if self._theme and self._theme.colors:
404
+ bg_color = self._theme.colors.background or "#ffffff"
405
+ fig.set_facecolor(bg_color)
406
+ ax.set_facecolor(bg_color)
407
+ ax.set_axis_off()
408
+
409
+ if self._node.kind == "text":
410
+ # Render text annotation
411
+ text_obj = self._node.text
412
+ if text_obj:
413
+ text_content = text_obj.content or self._node.name or ""
414
+ kwargs = {"ha": text_obj.ha, "va": text_obj.va}
415
+ if text_obj.fontsize:
416
+ kwargs["fontsize"] = text_obj.fontsize
417
+ if text_obj.fontweight:
418
+ kwargs["fontweight"] = text_obj.fontweight
419
+ else:
420
+ text_content = self._node.name or ""
421
+ kwargs = {"ha": "center", "va": "center"}
422
+ ax.text(0.5, 0.5, text_content, transform=ax.transAxes, **kwargs)
423
+
424
+ elif self._node.kind == "shape":
425
+ # Render shape annotation
426
+ from .._kinds._shape import render_shape
427
+ shape_obj = self._node.shape
428
+ if shape_obj:
429
+ render_shape(
430
+ ax,
431
+ shape_type=shape_obj.shape_type,
432
+ x=0.2, y=0.2, width=0.6, height=0.6,
433
+ facecolor=shape_obj.color if shape_obj.fill else "none",
434
+ edgecolor=shape_obj.color,
435
+ linewidth=shape_obj.linewidth,
436
+ )
437
+
438
+ fig.tight_layout()
439
+ return fig
440
+
441
+ def _render_image(self) -> Optional["MplFigure"]:
442
+ """Render image from payload."""
443
+ import scitex.plt as splt
444
+ import numpy as np
445
+
446
+ size_mm = self._node.size_mm.to_dict() if self._node.size_mm else {"width": 85, "height": 85}
447
+
448
+ fig, ax = splt.subplots(
449
+ figsize_mm=(size_mm.get("width", 85), size_mm.get("height", 85))
450
+ )
451
+ ax.set_axis_off()
452
+
453
+ # Try to find image in payload
454
+ for ext in ["png", "jpg", "jpeg", "gif", "bmp"]:
455
+ path = f"payload/image.{ext}"
456
+ if self.storage.exists(path):
457
+ from PIL import Image
458
+ from io import BytesIO
459
+ img_bytes = self.storage.read(path)
460
+ img = Image.open(BytesIO(img_bytes))
461
+ ax.imshow(np.array(img))
462
+ break
463
+
464
+ fig.tight_layout()
465
+ return fig
466
+
467
+ def validate(self, level: str = "schema") -> ValidationResult:
468
+ """Validate bundle.
469
+
470
+ Args:
471
+ level: Validation level - "schema", "semantic", or "strict"
472
+
473
+ Returns:
474
+ ValidationResult with is_valid property and errors list
475
+ """
476
+ result = ValidationResult(level=level)
477
+
478
+ # Node logical validation
479
+ if self._node:
480
+ result.errors.extend(self._node.validate())
481
+
482
+ # Storage-level validation - check required payload files
483
+ if self._node and self._node.is_leaf_kind():
484
+ required_file = self._node.get_required_payload_file()
485
+ if required_file:
486
+ # Check both new structure (payload/) and legacy structure (data/)
487
+ # Legacy sio.save() uses data/data.csv, new FTS uses payload/data.csv
488
+ legacy_paths = {
489
+ "payload/data.csv": "data/data.csv",
490
+ "payload/table.csv": "data/table.csv",
491
+ "payload/stats.json": "stats/stats.json",
492
+ }
493
+ legacy_path = legacy_paths.get(required_file)
494
+ if not self.storage.exists(required_file):
495
+ if not legacy_path or not self.storage.exists(legacy_path):
496
+ result.errors.append(f"Missing required payload file: {required_file}")
497
+
498
+ # NOTE: For composite kinds, do NOT validate payload/ emptiness by listing files.
499
+ # Payload prohibition is enforced purely via payload_schema is None (in Node.validate).
500
+
501
+ # Recursively validate embedded children
502
+ if self._node and self._node.is_composite_kind() and self._node.children:
503
+ children = self.load_children()
504
+ for child_name, child in children.items():
505
+ child_result = child.validate(level)
506
+ result.errors.extend([f"{child_name}: {e}" for e in child_result.errors])
507
+ result.warnings.extend([f"{child_name}: {w}" for w in child_result.warnings])
508
+
509
+ # Schema validation for other components
510
+ if level in ("semantic", "strict"):
511
+ # Additional semantic validation
512
+ if self._encoding and self._node:
513
+ if self._node.is_composite_kind() and self._encoding.traces:
514
+ result.errors.append("Composite kinds should not have encoding traces")
515
+
516
+ return result
517
+
518
+ def save(
519
+ self,
520
+ path: Optional[Union[str, Path]] = None,
521
+ validate: bool = True,
522
+ validation_level: str = "schema",
523
+ render: bool = True,
524
+ ):
525
+ """Save bundle to disk.
526
+
527
+ Args:
528
+ path: Override save path
529
+ validate: Run validation before saving
530
+ validation_level: Validation level
531
+ render: Generate exports/cache (default True).
532
+ Set False for WIP saves (faster, spec/payload/children only).
533
+ """
534
+ if path:
535
+ self._path = Path(path)
536
+ self._is_zip = self._path.suffix == ".zip"
537
+ self._storage = None # Reset storage
538
+
539
+ # Validate before saving
540
+ if validate:
541
+ result = self.validate(level=validation_level)
542
+ if not result.is_valid:
543
+ raise ValidationError(f"Validation failed: {result.errors}")
544
+
545
+ # Update modified timestamp
546
+ if self._node:
547
+ self._node.touch()
548
+
549
+ # Save canonical files
550
+ save_bundle_components(
551
+ self._path,
552
+ node=self._node,
553
+ encoding=self._encoding,
554
+ theme=self._theme,
555
+ stats=self._stats,
556
+ data_info=self._data_info,
557
+ render=render,
558
+ )
559
+
560
+ # Render and save exports/cache (optional)
561
+ if render:
562
+ fig = self.render()
563
+ if fig:
564
+ source_hash = compute_canonical_hash(self.storage)
565
+ theme_hash = compute_theme_hash(self._theme)
566
+ # Extract figure dimensions in pixels
567
+ dpi = fig.get_dpi()
568
+ width_px = int(fig.get_figwidth() * dpi)
569
+ height_px = int(fig.get_figheight() * dpi)
570
+ geometry = {
571
+ "figure_px": [width_px, height_px],
572
+ }
573
+ save_render_outputs(
574
+ self.storage,
575
+ fig,
576
+ geometry=geometry,
577
+ source_hash=source_hash,
578
+ theme_hash=theme_hash,
579
+ )
580
+ import matplotlib.pyplot as plt
581
+ from matplotlib.figure import Figure as MplFigure
582
+
583
+ # Handle FigWrapper from scitex.plt
584
+ if isinstance(fig, MplFigure):
585
+ plt.close(fig)
586
+ elif hasattr(fig, "figure") and isinstance(fig.figure, MplFigure):
587
+ plt.close(fig.figure)
588
+ else:
589
+ plt.close(fig)
590
+
591
+ self._dirty = False
592
+
593
+ def to_dict(self) -> Dict[str, Any]:
594
+ """Convert bundle to dictionary."""
595
+ result = {
596
+ "path": str(self._path),
597
+ "is_zip": self._is_zip,
598
+ "kind": self.bundle_type,
599
+ }
600
+ if self._node:
601
+ result["node"] = self._node.to_dict()
602
+ if self._encoding:
603
+ result["encoding"] = self._encoding.to_dict()
604
+ if self._theme:
605
+ result["theme"] = self._theme.to_dict()
606
+ if self._stats:
607
+ result["stats"] = self._stats.to_dict()
608
+ if self._data_info:
609
+ result["data_info"] = self._data_info.to_dict()
610
+ return result
611
+
612
+ def __enter__(self) -> "FTS":
613
+ """Enter context manager."""
614
+ return self
615
+
616
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
617
+ """Exit context manager, auto-saving if dirty and no exception."""
618
+ if exc_type is None and self._dirty:
619
+ self.save()
620
+ return False
621
+
622
+ def __repr__(self) -> str:
623
+ dirty_marker = "*" if self._dirty else ""
624
+ kind = self._node.kind if self._node else "unknown"
625
+ return f"FTS({self._path!r}, kind={kind!r}){dirty_marker}"
626
+
627
+
628
+ # =============================================================================
629
+ # Factory Functions
630
+ # =============================================================================
631
+
632
+ # Import from_matplotlib from helper module (single source of truth)
633
+ from ._mpl_helpers import from_matplotlib
634
+
635
+
636
+ def load_bundle(path: Union[str, Path]) -> FTS:
637
+ """Load an existing FTS bundle."""
638
+ return FTS(path)
639
+
640
+
641
+ def create_bundle(
642
+ path: Union[str, Path],
643
+ kind: str = "plot",
644
+ name: Optional[str] = None,
645
+ size_mm: Optional[Dict[str, float]] = None,
646
+ # Legacy support
647
+ node_type: Optional[str] = None,
648
+ ) -> FTS:
649
+ """Create a new FTS bundle."""
650
+ if node_type is not None:
651
+ kind = node_type
652
+ return FTS(path, create=True, kind=kind, name=name, size_mm=size_mm)
653
+
654
+
655
+ __all__ = ["FTS", "load_bundle", "create_bundle", "from_matplotlib"]
656
+
657
+ # EOF
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/__init__.py
4
+
5
+ """FTS Bundle - Core bundle functionality."""
6
+
7
+ # FTS class
8
+ from ._FTS import FTS, create_bundle, from_matplotlib, load_bundle
9
+
10
+ # Core dataclasses users need
11
+ from ._dataclasses import BBox, DataInfo, Node, SizeMM
12
+
13
+ # Type enumeration
14
+ from ._utils import NodeType
15
+
16
+ # Error classes
17
+ from ._utils import BundleError, BundleNotFoundError, BundleValidationError
18
+
19
+ __all__ = [
20
+ # FTS
21
+ "FTS",
22
+ "load_bundle",
23
+ "create_bundle",
24
+ "from_matplotlib",
25
+ # Core dataclasses
26
+ "Node",
27
+ "BBox",
28
+ "SizeMM",
29
+ "DataInfo",
30
+ # Types
31
+ "NodeType",
32
+ # Errors
33
+ "BundleError",
34
+ "BundleNotFoundError",
35
+ "BundleValidationError",
36
+ ]
37
+
38
+ # EOF