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,241 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP tool definitions for SciTeX Capture.
4
+
5
+ This module contains the tool handler implementations that are
6
+ registered with the MCP server.
7
+ """
8
+
9
+ import asyncio
10
+ import base64
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ from scitex import capture
16
+
17
+ from .grid import draw_cursor_overlay, draw_grid_overlay
18
+
19
+
20
+ def get_capture_dir() -> Path:
21
+ """Get the screenshot capture directory."""
22
+ import os
23
+ import shutil
24
+
25
+ SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
26
+ new_dir = SCITEX_BASE_DIR / "capture"
27
+ old_dir = Path.home() / ".cache" / "cammy"
28
+
29
+ new_dir.mkdir(parents=True, exist_ok=True)
30
+
31
+ if old_dir.exists():
32
+ new_screenshots = list(new_dir.glob("*.jpg"))
33
+ if not new_screenshots or len(new_screenshots) == 0:
34
+ try:
35
+ for img in old_dir.glob("*.jpg"):
36
+ shutil.move(str(img), str(new_dir / img.name))
37
+ except Exception:
38
+ pass
39
+
40
+ return new_dir
41
+
42
+
43
+ class OverlayTools:
44
+ """Tools for adding overlays to screenshots."""
45
+
46
+ @staticmethod
47
+ async def add_cursor_overlay(
48
+ image_path: str,
49
+ cursor_x: Optional[int] = None,
50
+ cursor_y: Optional[int] = None,
51
+ output_path: Optional[str] = None,
52
+ monitor_offset_y: int = 1080,
53
+ ) -> Dict[str, Any]:
54
+ """Add cursor position marker overlay to a screenshot."""
55
+ try:
56
+ loop = asyncio.get_event_loop()
57
+
58
+ cursor_pos = None
59
+ if cursor_x is not None and cursor_y is not None:
60
+ cursor_pos = (cursor_x, cursor_y)
61
+
62
+ result_path = await loop.run_in_executor(
63
+ None,
64
+ lambda: draw_cursor_overlay(
65
+ filepath=image_path,
66
+ cursor_pos=cursor_pos,
67
+ output_path=output_path,
68
+ monitor_offset_y=monitor_offset_y,
69
+ ),
70
+ )
71
+
72
+ return {
73
+ "success": True,
74
+ "path": result_path,
75
+ "message": f"Cursor overlay added to {result_path}",
76
+ "cursor_position": cursor_pos,
77
+ "monitor_offset_y": monitor_offset_y,
78
+ }
79
+
80
+ except Exception as e:
81
+ return {"success": False, "error": str(e)}
82
+
83
+ @staticmethod
84
+ async def add_grid_overlay(
85
+ image_path: str,
86
+ grid_spacing: int = 100,
87
+ output_path: Optional[str] = None,
88
+ ) -> Dict[str, Any]:
89
+ """Add coordinate grid overlay to a screenshot."""
90
+ try:
91
+ loop = asyncio.get_event_loop()
92
+
93
+ result_path = await loop.run_in_executor(
94
+ None,
95
+ lambda: draw_grid_overlay(
96
+ filepath=image_path,
97
+ grid_spacing=grid_spacing,
98
+ output_path=output_path,
99
+ ),
100
+ )
101
+
102
+ return {
103
+ "success": True,
104
+ "path": result_path,
105
+ "message": f"Grid overlay ({grid_spacing}px) added to {result_path}",
106
+ "grid_spacing": grid_spacing,
107
+ }
108
+
109
+ except Exception as e:
110
+ return {"success": False, "error": str(e)}
111
+
112
+
113
+ class CaptureTools:
114
+ """Tools for screenshot capture operations."""
115
+
116
+ @staticmethod
117
+ async def capture_screenshot(
118
+ message=None,
119
+ monitor_id=0,
120
+ all=False,
121
+ app=None,
122
+ url=None,
123
+ quality=85,
124
+ return_base64=False,
125
+ ) -> Dict[str, Any]:
126
+ """Capture a screenshot."""
127
+ try:
128
+ loop = asyncio.get_event_loop()
129
+
130
+ def do_capture():
131
+ return capture.snap(
132
+ message=message,
133
+ quality=quality,
134
+ monitor_id=monitor_id,
135
+ all=all,
136
+ app=app,
137
+ url=url,
138
+ verbose=True,
139
+ )
140
+
141
+ path = await loop.run_in_executor(None, do_capture)
142
+
143
+ if not path:
144
+ return {"success": False, "error": "Failed to capture screenshot"}
145
+
146
+ category = "stderr" if "-stderr.jpg" in path else "stdout"
147
+
148
+ result = {
149
+ "success": True,
150
+ "path": path,
151
+ "category": category,
152
+ "message": f"Screenshot saved to {path}",
153
+ "timestamp": datetime.now().isoformat(),
154
+ }
155
+
156
+ if return_base64 and path:
157
+ with open(path, "rb") as f:
158
+ result["base64"] = base64.b64encode(f.read()).decode()
159
+
160
+ return result
161
+
162
+ except Exception as e:
163
+ return {"success": False, "error": str(e)}
164
+
165
+ @staticmethod
166
+ async def capture_window_tool(
167
+ window_handle: int, output_path: str = None, quality: int = 85
168
+ ) -> Dict[str, Any]:
169
+ """Capture a specific window by handle."""
170
+ try:
171
+ loop = asyncio.get_event_loop()
172
+ path = await loop.run_in_executor(
173
+ None, capture.capture_window, window_handle, output_path
174
+ )
175
+
176
+ if path:
177
+ return {
178
+ "success": True,
179
+ "path": path,
180
+ "window_handle": window_handle,
181
+ "message": f"Window captured to {path}",
182
+ }
183
+ else:
184
+ return {
185
+ "success": False,
186
+ "error": f"Failed to capture window {window_handle}",
187
+ }
188
+ except Exception as e:
189
+ return {"success": False, "error": str(e)}
190
+
191
+
192
+ class InfoTools:
193
+ """Tools for getting system information."""
194
+
195
+ @staticmethod
196
+ async def get_info() -> Dict[str, Any]:
197
+ """Enumerate all monitors and virtual desktops."""
198
+ try:
199
+ loop = asyncio.get_event_loop()
200
+ info = await loop.run_in_executor(None, capture.get_info)
201
+
202
+ return {
203
+ "success": True,
204
+ "monitors": info.get("Monitors", {}),
205
+ "virtual_desktops": info.get("VirtualDesktops", {}),
206
+ "windows": info.get("Windows", {}),
207
+ "timestamp": info.get("Timestamp", ""),
208
+ }
209
+ except Exception as e:
210
+ return {"success": False, "error": str(e)}
211
+
212
+ @staticmethod
213
+ async def list_windows() -> Dict[str, Any]:
214
+ """List all visible windows."""
215
+ try:
216
+ loop = asyncio.get_event_loop()
217
+ info = await loop.run_in_executor(None, capture.get_info)
218
+
219
+ windows = info.get("Windows", {})
220
+ window_list = windows.get("Details", [])
221
+
222
+ formatted_windows = []
223
+ for win in window_list:
224
+ formatted_windows.append(
225
+ {
226
+ "handle": win.get("Handle"),
227
+ "title": win.get("Title"),
228
+ "process_name": win.get("ProcessName"),
229
+ "process_id": win.get("ProcessId"),
230
+ }
231
+ )
232
+
233
+ return {
234
+ "success": True,
235
+ "windows": formatted_windows,
236
+ "count": len(formatted_windows),
237
+ "visible_count": windows.get("VisibleCount", 0),
238
+ "message": f"Found {len(formatted_windows)} windows",
239
+ }
240
+ except Exception as e:
241
+ return {"success": False, "error": str(e)}
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """MCP utility functions for SciTeX Capture."""
3
+
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ # Directory configuration
9
+ SCITEX_BASE_DIR = Path(os.getenv("SCITEX_DIR", Path.home() / ".scitex"))
10
+ SCITEX_CAPTURE_DIR = SCITEX_BASE_DIR / "capture"
11
+ LEGACY_CAPTURE_DIR = Path.home() / ".cache" / "cammy"
12
+
13
+
14
+ def get_capture_dir() -> Path:
15
+ """Get screenshot capture directory, migrating from legacy if needed."""
16
+ new_dir = SCITEX_CAPTURE_DIR
17
+ old_dir = LEGACY_CAPTURE_DIR
18
+
19
+ new_dir.mkdir(parents=True, exist_ok=True)
20
+
21
+ if old_dir.exists():
22
+ new_screenshots = list(new_dir.glob("*.jpg"))
23
+ if not new_screenshots:
24
+ try:
25
+ for img in old_dir.glob("*.jpg"):
26
+ shutil.move(str(img), str(new_dir / img.name))
27
+ except Exception:
28
+ pass
29
+
30
+ return new_dir
scitex/cli/convert.py ADDED
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2025-12-19 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/cli/convert.py
4
+
5
+ """
6
+ CLI commands for converting legacy bundle formats to unified .stx format.
7
+
8
+ Usage:
9
+ scitex convert old_figure.figz # Convert to old_figure.stx
10
+ scitex convert old_figure.figz output.stx # Convert with custom output
11
+ scitex convert --batch ./figures/*.figz # Batch convert
12
+ scitex convert --validate output.stx # Validate bundle
13
+ """
14
+
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import List, Optional
18
+
19
+ import click
20
+
21
+
22
+ @click.group()
23
+ def convert():
24
+ """Convert and validate SciTeX bundle files.
25
+
26
+ \b
27
+ Convert legacy formats (.figz, .pltz, .statsz) to unified .stx format.
28
+ Supports single file conversion, batch conversion, and validation.
29
+
30
+ \b
31
+ Examples:
32
+ scitex convert file old_figure.figz # Convert single file
33
+ scitex convert file old_figure.figz -o new.stx # Custom output name
34
+ scitex convert batch ./figures/*.figz # Batch convert
35
+ scitex convert validate output.stx # Validate bundle
36
+ """
37
+ pass
38
+
39
+
40
+ @convert.command("file")
41
+ @click.argument("input_path", type=click.Path(exists=True))
42
+ @click.option(
43
+ "-o",
44
+ "--output",
45
+ type=click.Path(),
46
+ help="Output path (default: same name with .stx extension)",
47
+ )
48
+ @click.option(
49
+ "--overwrite",
50
+ is_flag=True,
51
+ help="Overwrite output file if it exists",
52
+ )
53
+ @click.option(
54
+ "--dry-run",
55
+ is_flag=True,
56
+ help="Show what would be done without writing files",
57
+ )
58
+ def convert_file(
59
+ input_path: str, output: Optional[str], overwrite: bool, dry_run: bool
60
+ ):
61
+ """Convert a single legacy bundle to .stx format.
62
+
63
+ \b
64
+ Supported input formats:
65
+ .figz - Figure bundles
66
+ .pltz - Plot bundles
67
+ .statsz - Statistics bundles
68
+
69
+ \b
70
+ Examples:
71
+ scitex convert file old_figure.figz
72
+ scitex convert file plot.pltz -o converted_plot.stx
73
+ scitex convert file stats.statsz --dry-run
74
+ """
75
+ input_file = Path(input_path)
76
+
77
+ # Determine output path
78
+ if output:
79
+ output_file = Path(output)
80
+ else:
81
+ output_file = input_file.with_suffix(".stx")
82
+
83
+ # Check if already .stx
84
+ if input_file.suffix == ".stx":
85
+ click.secho(f"File is already in .stx format: {input_file}", fg="yellow")
86
+ return
87
+
88
+ # Validate input format
89
+ valid_extensions = (".figz", ".pltz", ".statsz")
90
+ if input_file.suffix not in valid_extensions:
91
+ click.secho(
92
+ f"Unsupported format: {input_file.suffix}. "
93
+ f"Supported: {', '.join(valid_extensions)}",
94
+ fg="red",
95
+ err=True,
96
+ )
97
+ sys.exit(1)
98
+
99
+ # Check output exists
100
+ if output_file.exists() and not overwrite:
101
+ click.secho(
102
+ f"Output file exists: {output_file}. Use --overwrite to replace.",
103
+ fg="red",
104
+ err=True,
105
+ )
106
+ sys.exit(1)
107
+
108
+ if dry_run:
109
+ click.echo(f"Would convert: {input_file} -> {output_file}")
110
+ return
111
+
112
+ # Perform conversion
113
+ try:
114
+ _convert_bundle(input_file, output_file)
115
+ click.secho(f"Converted: {input_file} -> {output_file}", fg="green")
116
+ except Exception as e:
117
+ click.secho(f"Error converting {input_file}: {e}", fg="red", err=True)
118
+ sys.exit(1)
119
+
120
+
121
+ @convert.command("batch")
122
+ @click.argument("pattern", nargs=-1, required=True)
123
+ @click.option(
124
+ "-o",
125
+ "--output-dir",
126
+ type=click.Path(),
127
+ help="Output directory (default: same as input)",
128
+ )
129
+ @click.option(
130
+ "--overwrite",
131
+ is_flag=True,
132
+ help="Overwrite existing files",
133
+ )
134
+ @click.option(
135
+ "--dry-run",
136
+ is_flag=True,
137
+ help="Show what would be done without writing files",
138
+ )
139
+ def convert_batch(
140
+ pattern: tuple, output_dir: Optional[str], overwrite: bool, dry_run: bool
141
+ ):
142
+ """Batch convert multiple legacy bundles to .stx format.
143
+
144
+ \b
145
+ Examples:
146
+ scitex convert batch ./figures/*.figz
147
+ scitex convert batch ./plots/*.pltz -o ./converted/
148
+ scitex convert batch ./**/*.figz ./**/*.pltz --dry-run
149
+ """
150
+ import glob
151
+
152
+ # Collect all files matching patterns
153
+ files: List[Path] = []
154
+ for pat in pattern:
155
+ matches = glob.glob(pat, recursive=True)
156
+ files.extend(Path(m) for m in matches)
157
+
158
+ # Filter to valid extensions
159
+ valid_extensions = (".figz", ".pltz", ".statsz")
160
+ files = [f for f in files if f.suffix in valid_extensions]
161
+
162
+ if not files:
163
+ click.secho("No matching files found.", fg="yellow")
164
+ return
165
+
166
+ click.echo(f"Found {len(files)} file(s) to convert")
167
+
168
+ # Determine output directory
169
+ out_dir = Path(output_dir) if output_dir else None
170
+ if out_dir and not dry_run:
171
+ out_dir.mkdir(parents=True, exist_ok=True)
172
+
173
+ # Convert each file
174
+ converted = 0
175
+ errors = 0
176
+ for input_file in files:
177
+ if out_dir:
178
+ output_file = out_dir / input_file.with_suffix(".stx").name
179
+ else:
180
+ output_file = input_file.with_suffix(".stx")
181
+
182
+ if output_file.exists() and not overwrite:
183
+ click.secho(f"Skipping (exists): {output_file}", fg="yellow")
184
+ continue
185
+
186
+ if dry_run:
187
+ click.echo(f"Would convert: {input_file} -> {output_file}")
188
+ converted += 1
189
+ continue
190
+
191
+ try:
192
+ _convert_bundle(input_file, output_file)
193
+ click.secho(
194
+ f"Converted: {input_file.name} -> {output_file.name}", fg="green"
195
+ )
196
+ converted += 1
197
+ except Exception as e:
198
+ click.secho(f"Error: {input_file}: {e}", fg="red", err=True)
199
+ errors += 1
200
+
201
+ # Summary
202
+ click.echo()
203
+ click.echo(f"Converted: {converted}, Errors: {errors}")
204
+
205
+
206
+ @convert.command("validate")
207
+ @click.argument("paths", nargs=-1, required=True, type=click.Path(exists=True))
208
+ @click.option(
209
+ "--verbose",
210
+ "-v",
211
+ is_flag=True,
212
+ help="Show detailed validation info",
213
+ )
214
+ def validate_bundles(paths: tuple, verbose: bool):
215
+ """Validate one or more .stx bundles.
216
+
217
+ \b
218
+ Checks:
219
+ - Valid ZIP structure
220
+ - spec.json present and valid
221
+ - Schema version
222
+ - Depth limits
223
+ - Circular references
224
+
225
+ \b
226
+ Examples:
227
+ scitex convert validate output.stx
228
+ scitex convert validate ./figures/*.stx --verbose
229
+ """
230
+ # Use FTS instead of deprecated io.bundle
231
+ try:
232
+ from scitex.fts import FTS as ZipBundle
233
+ def validate_stx_bundle(path):
234
+ try:
235
+ bundle = ZipBundle(path)
236
+ return bundle.validate(level="strict")
237
+ except Exception as e:
238
+ return {"valid": False, "errors": [str(e)]}
239
+ except ImportError:
240
+ click.echo("Error: scitex.fts not available", err=True)
241
+ return
242
+
243
+ valid = 0
244
+ invalid = 0
245
+
246
+ for path_str in paths:
247
+ path = Path(path_str)
248
+
249
+ try:
250
+ with ZipBundle(path, mode="r") as zb:
251
+ spec = zb.read_json("spec.json")
252
+
253
+ # Check schema
254
+ schema = spec.get("schema", {})
255
+ schema_name = schema.get("name", "unknown")
256
+ schema_version = schema.get("version", "unknown")
257
+ bundle_type = spec.get("type", "unknown")
258
+ bundle_id = spec.get("bundle_id", "missing")
259
+
260
+ if verbose:
261
+ click.echo(f"\n{path}:")
262
+ click.echo(f" Schema: {schema_name} v{schema_version}")
263
+ click.echo(f" Type: {bundle_type}")
264
+ click.echo(f" ID: {bundle_id}")
265
+ constraints = spec.get("constraints", {})
266
+ click.echo(f" Constraints: {constraints}")
267
+
268
+ # Validate structure
269
+ validate_stx_bundle(spec)
270
+
271
+ click.secho(f"VALID: {path}", fg="green")
272
+ valid += 1
273
+
274
+ except FileNotFoundError as e:
275
+ click.secho(f"INVALID: {path} - File not found: {e}", fg="red")
276
+ invalid += 1
277
+ except Exception as e:
278
+ click.secho(f"INVALID: {path} - {e}", fg="red")
279
+ invalid += 1
280
+
281
+ # Summary
282
+ click.echo()
283
+ click.echo(f"Valid: {valid}, Invalid: {invalid}")
284
+
285
+ if invalid > 0:
286
+ sys.exit(1)
287
+
288
+
289
+ @convert.command("info")
290
+ @click.argument("path", type=click.Path(exists=True))
291
+ def bundle_info(path: str):
292
+ """Show information about a bundle file.
293
+
294
+ \b
295
+ Displays:
296
+ - Format (stx vs legacy)
297
+ - Schema version
298
+ - Bundle type
299
+ - Contents summary
300
+
301
+ \b
302
+ Example:
303
+ scitex convert info figure.stx
304
+ """
305
+ try:
306
+ from scitex.fts import FTS as ZipBundle
307
+ except ImportError:
308
+ click.echo("Error: scitex.fts not available", err=True)
309
+ return
310
+
311
+ bundle_path = Path(path)
312
+
313
+ try:
314
+ with ZipBundle(bundle_path, mode="r") as zb:
315
+ spec = zb.read_json("spec.json")
316
+ files = zb.namelist()
317
+
318
+ # Basic info
319
+ click.echo(f"\nBundle: {bundle_path}")
320
+ click.echo(f"Extension: {bundle_path.suffix}")
321
+ click.echo(f"Size: {bundle_path.stat().st_size:,} bytes")
322
+
323
+ # Schema info
324
+ schema = spec.get("schema", {})
325
+ click.echo(
326
+ f"\nSchema: {schema.get('name', 'unknown')} v{schema.get('version', 'unknown')}"
327
+ )
328
+ click.echo(f"Type: {spec.get('type', 'unknown')}")
329
+ click.echo(f"Bundle ID: {spec.get('bundle_id', 'not set')}")
330
+
331
+ # Constraints
332
+ constraints = spec.get("constraints", {})
333
+ if constraints:
334
+ click.echo("\nConstraints:")
335
+ click.echo(f" allow_children: {constraints.get('allow_children', 'N/A')}")
336
+ click.echo(f" max_depth: {constraints.get('max_depth', 'N/A')}")
337
+
338
+ # Contents
339
+ click.echo(f"\nContents ({len(files)} files):")
340
+ for f in sorted(files)[:20]: # Show first 20
341
+ click.echo(f" {f}")
342
+ if len(files) > 20:
343
+ click.echo(f" ... and {len(files) - 20} more")
344
+
345
+ # Type-specific info
346
+ if spec.get("type") == "figure":
347
+ panels = spec.get("panels", [])
348
+ elements = spec.get("elements", [])
349
+ click.echo(f"\nPanels: {len(panels)}")
350
+ click.echo(f"Elements: {len(elements)}")
351
+ elif spec.get("type") == "plot":
352
+ click.echo(f"\nPlot type: {spec.get('plot_type', 'unknown')}")
353
+ elif spec.get("type") == "stats":
354
+ comparisons = spec.get("comparisons", [])
355
+ click.echo(f"\nComparisons: {len(comparisons)}")
356
+
357
+ except Exception as e:
358
+ click.secho(f"Error reading bundle: {e}", fg="red", err=True)
359
+ sys.exit(1)
360
+
361
+
362
+ def _convert_bundle(input_path: Path, output_path: Path) -> None:
363
+ """Convert a legacy bundle to .stx format.
364
+
365
+ Args:
366
+ input_path: Path to legacy bundle (.figz, .pltz, .statsz)
367
+ output_path: Path for output .stx bundle
368
+ """
369
+ import json
370
+ import tempfile
371
+ import zipfile
372
+
373
+ # Generate bundle ID and normalize spec - inline functions since io.bundle is deprecated
374
+ import uuid
375
+ def generate_bundle_id():
376
+ return str(uuid.uuid4())[:8]
377
+ def normalize_spec(spec):
378
+ return spec # FTS handles normalization internally
379
+
380
+ # Determine bundle type from extension
381
+ ext = input_path.suffix
382
+ type_map = {
383
+ ".figz": "figure",
384
+ ".pltz": "plot",
385
+ ".statsz": "stats",
386
+ }
387
+ bundle_type = type_map.get(ext)
388
+
389
+ # Read input bundle
390
+ with zipfile.ZipFile(input_path, "r") as zf:
391
+ # Read spec
392
+ spec_data = zf.read("spec.json")
393
+ spec = json.loads(spec_data)
394
+
395
+ # Normalize to v2.0.0
396
+ normalized_spec = normalize_spec(spec, bundle_type)
397
+
398
+ # Ensure bundle_id
399
+ if "bundle_id" not in normalized_spec:
400
+ normalized_spec["bundle_id"] = generate_bundle_id()
401
+
402
+ # Copy all files to new bundle
403
+ with tempfile.TemporaryDirectory() as tmpdir:
404
+ # Extract all
405
+ zf.extractall(tmpdir)
406
+
407
+ # Write updated spec
408
+ spec_path = Path(tmpdir) / "spec.json"
409
+ with open(spec_path, "w") as f:
410
+ json.dump(normalized_spec, f, indent=2)
411
+
412
+ # Create output bundle
413
+ output_path.parent.mkdir(parents=True, exist_ok=True)
414
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as out_zf:
415
+ for file_path in Path(tmpdir).rglob("*"):
416
+ if file_path.is_file():
417
+ arcname = file_path.relative_to(tmpdir)
418
+ out_zf.write(file_path, arcname)
419
+
420
+
421
+ # EOF