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,76 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_types.py
4
+
5
+ """FTS Node type constants and constraints."""
6
+
7
+ from typing import Any, Dict
8
+
9
+
10
+ class NodeType:
11
+ """Node type constants for FTS bundles.
12
+
13
+ The type is stored in node.json["type"].
14
+
15
+ Usage:
16
+ from scitex.fts import NodeType
17
+
18
+ if bundle.node.type == NodeType.FIGURE:
19
+ ...
20
+
21
+ Note:
22
+ Tables are treated as structured figures in academic contexts.
23
+ FTS handles figures, tables, and statistics in a unified way.
24
+ """
25
+
26
+ FIGURE = "figure"
27
+ PLOT = "plot"
28
+ TABLE = "table" # Structured data presentation (demographics, results, etc.)
29
+ STATS = "stats"
30
+ IMAGE = "image"
31
+ TEXT = "text"
32
+ SHAPE = "shape"
33
+ SYMBOL = "symbol"
34
+ COMMENT = "comment"
35
+ EQUATION = "equation"
36
+
37
+
38
+ # Legacy alias (deprecated)
39
+ BundleType = NodeType
40
+
41
+
42
+ # Type-specific default constraints
43
+ TYPE_DEFAULTS: Dict[str, Dict[str, Any]] = {
44
+ "figure": {"allow_children": True, "max_depth": 3},
45
+ "plot": {"allow_children": False, "max_depth": 1},
46
+ "table": {"allow_children": False, "max_depth": 1}, # Tables are leaf nodes
47
+ "stats": {"allow_children": False, "max_depth": 1},
48
+ "image": {"allow_children": False, "max_depth": 1},
49
+ "text": {"allow_children": False, "max_depth": 1},
50
+ "shape": {"allow_children": False, "max_depth": 1},
51
+ "symbol": {"allow_children": False, "max_depth": 1},
52
+ "comment": {"allow_children": False, "max_depth": 1},
53
+ "equation": {"allow_children": False, "max_depth": 1},
54
+ }
55
+
56
+
57
+ def get_default_constraints(node_type: str) -> Dict[str, Any]:
58
+ """Get default constraints for a node type.
59
+
60
+ Args:
61
+ node_type: Type string (figure, plot, stats, etc.)
62
+
63
+ Returns:
64
+ Dict with allow_children and max_depth.
65
+ """
66
+ return TYPE_DEFAULTS.get(node_type, {"allow_children": False, "max_depth": 1})
67
+
68
+
69
+ __all__ = [
70
+ "NodeType",
71
+ "BundleType",
72
+ "TYPE_DEFAULTS",
73
+ "get_default_constraints",
74
+ ]
75
+
76
+ # EOF
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_validation.py
4
+
5
+ """
6
+ FTS Multi-Level Validation System.
7
+
8
+ Three validation levels:
9
+ 1. schema - Always ON (init/load/save) - Fast, structural checks
10
+ 2. semantic - On demand (save/export) - Cross-reference consistency
11
+ 3. strict - Explicit (CI/publication) - Full scientific rigor
12
+
13
+ Usage:
14
+ result = fts.validate(level="schema") # Fast, default at init
15
+ result = fts.validate(level="semantic") # Cross-ref checks
16
+ result = fts.validate(level="strict") # Full validation
17
+
18
+ if result.has_errors:
19
+ raise FTSValidationError(result)
20
+ """
21
+
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List, Literal, Optional, Union
26
+
27
+ # Schema directory (now in _schemas/)
28
+ SCHEMA_DIR = Path(__file__).parent.parent / "_schemas"
29
+
30
+ # Schema versions
31
+ SCHEMA_VERSION = "1.0.0"
32
+
33
+ # Loaded schemas (cached)
34
+ _SCHEMAS: Dict[str, Dict[str, Any]] = {}
35
+
36
+ # Validation levels
37
+ ValidationLevel = Literal["schema", "semantic", "strict"]
38
+
39
+
40
+ @dataclass
41
+ class ValidationResult:
42
+ """Result of FTS validation.
43
+
44
+ Attributes
45
+ ----------
46
+ level : str
47
+ Validation level used
48
+ errors : list
49
+ Critical errors (must fix)
50
+ warnings : list
51
+ Non-critical issues (should fix)
52
+ """
53
+
54
+ level: str = "schema"
55
+ errors: List[str] = field(default_factory=list)
56
+ warnings: List[str] = field(default_factory=list)
57
+
58
+ @property
59
+ def has_errors(self) -> bool:
60
+ """True if validation failed with errors."""
61
+ return len(self.errors) > 0
62
+
63
+ @property
64
+ def has_warnings(self) -> bool:
65
+ """True if there are warnings."""
66
+ return len(self.warnings) > 0
67
+
68
+ @property
69
+ def is_valid(self) -> bool:
70
+ """True if no errors (warnings OK)."""
71
+ return not self.has_errors
72
+
73
+ def __str__(self) -> str:
74
+ if self.is_valid and not self.has_warnings:
75
+ return f"ValidationResult(level={self.level}, valid=True)"
76
+ parts = [f"ValidationResult(level={self.level}"]
77
+ if self.errors:
78
+ parts.append(f", errors={len(self.errors)}")
79
+ if self.warnings:
80
+ parts.append(f", warnings={len(self.warnings)}")
81
+ parts.append(")")
82
+ return "".join(parts)
83
+
84
+ def raise_if_invalid(self):
85
+ """Raise BundleValidationError if has errors."""
86
+ if self.has_errors:
87
+ from ._utils import BundleValidationError
88
+
89
+ raise BundleValidationError(
90
+ f"Validation failed ({len(self.errors)} errors): {self.errors[0]}"
91
+ )
92
+
93
+
94
+ def load_schema(name: str) -> Dict[str, Any]:
95
+ """Load a JSON schema by name."""
96
+ if name not in _SCHEMAS:
97
+ schema_path = SCHEMA_DIR / f"{name}.schema.json"
98
+ if not schema_path.exists():
99
+ raise FileNotFoundError(f"Schema not found: {schema_path}")
100
+ with open(schema_path) as f:
101
+ _SCHEMAS[name] = json.load(f)
102
+ return _SCHEMAS[name]
103
+
104
+
105
+ # =============================================================================
106
+ # Level 1: Schema Validation (Always ON)
107
+ # =============================================================================
108
+
109
+
110
+ def validate_schema(data: Dict[str, Any], schema_name: str) -> List[str]:
111
+ """Validate data against JSON schema.
112
+
113
+ Fast structural validation:
114
+ - Required fields present
115
+ - Types correct
116
+ - Enum values valid
117
+ - bbox in range [0, 1]
118
+ """
119
+ errors = []
120
+
121
+ try:
122
+ schema = load_schema(schema_name)
123
+ except FileNotFoundError as e:
124
+ return [str(e)]
125
+
126
+ # Check required fields
127
+ required = schema.get("required", [])
128
+ for fld in required:
129
+ if fld not in data:
130
+ errors.append(f"Missing required field: {fld}")
131
+
132
+ # Check property types
133
+ properties = schema.get("properties", {})
134
+ for fld, value in data.items():
135
+ if fld in properties:
136
+ prop_schema = properties[fld]
137
+ expected_type = prop_schema.get("type")
138
+
139
+ if expected_type:
140
+ type_map = {
141
+ "string": str,
142
+ "number": (int, float),
143
+ "integer": int,
144
+ "boolean": bool,
145
+ "array": list,
146
+ "object": dict,
147
+ }
148
+ expected = type_map.get(expected_type)
149
+ if expected and not isinstance(value, expected):
150
+ errors.append(
151
+ f"Field '{fld}' should be {expected_type}, "
152
+ f"got {type(value).__name__}"
153
+ )
154
+
155
+ # Check enum values
156
+ if "enum" in prop_schema and value not in prop_schema["enum"]:
157
+ errors.append(
158
+ f"Field '{fld}' must be one of {prop_schema['enum']}, got '{value}'"
159
+ )
160
+
161
+ # Special: bbox range validation
162
+ if "bbox" in data and isinstance(data["bbox"], dict):
163
+ bbox = data["bbox"]
164
+ for key in ["x", "y", "width", "height"]:
165
+ if key in bbox:
166
+ val = bbox[key]
167
+ if isinstance(val, (int, float)) and not (0 <= val <= 1):
168
+ errors.append(f"bbox.{key} must be in range [0, 1], got {val}")
169
+
170
+ return errors
171
+
172
+
173
+ def validate_node(data: Dict[str, Any]) -> List[str]:
174
+ """Validate node.json data."""
175
+ return validate_schema(data, "node")
176
+
177
+
178
+ def validate_encoding(data: Dict[str, Any]) -> List[str]:
179
+ """Validate encoding.json data."""
180
+ return validate_schema(data, "encoding")
181
+
182
+
183
+ def validate_theme(data: Dict[str, Any]) -> List[str]:
184
+ """Validate theme.json data."""
185
+ return validate_schema(data, "theme")
186
+
187
+
188
+ def validate_stats(data: Dict[str, Any]) -> List[str]:
189
+ """Validate stats.json data."""
190
+ return validate_schema(data, "stats")
191
+
192
+
193
+ def validate_data_info(data: Dict[str, Any]) -> List[str]:
194
+ """Validate data_info.json data."""
195
+ return validate_schema(data, "data_info")
196
+
197
+
198
+ # =============================================================================
199
+ # Level 2: Semantic Validation (On demand)
200
+ # =============================================================================
201
+
202
+
203
+ def validate_semantic(
204
+ node: Optional[Dict] = None,
205
+ encoding: Optional[Dict] = None,
206
+ theme: Optional[Dict] = None,
207
+ stats: Optional[Dict] = None,
208
+ data_info: Optional[Dict] = None,
209
+ ) -> List[str]:
210
+ """Validate semantic consistency across bundle components.
211
+
212
+ Cross-reference checks:
213
+ - encoding columns exist in data_info
214
+ - stats references valid data
215
+ - scale/normalization consistency
216
+ """
217
+ errors = []
218
+
219
+ # Check encoding references data_info columns
220
+ if encoding and data_info:
221
+ columns = set()
222
+ if "columns" in data_info:
223
+ columns = {c.get("name") for c in data_info["columns"] if c.get("name")}
224
+
225
+ traces = encoding.get("traces", [])
226
+ for trace in traces:
227
+ for channel in ["x", "y", "color", "size"]:
228
+ if channel in trace:
229
+ col = trace[channel].get("column")
230
+ if col and columns and col not in columns:
231
+ errors.append(
232
+ f"Encoding references unknown column '{col}' "
233
+ f"(available: {sorted(columns)})"
234
+ )
235
+
236
+ # Check stats references valid data
237
+ if stats and data_info:
238
+ analyses = stats.get("analyses", [])
239
+ columns = set()
240
+ if "columns" in data_info:
241
+ columns = {c.get("name") for c in data_info["columns"] if c.get("name")}
242
+
243
+ for analysis in analyses:
244
+ data_ref = analysis.get("data_ref", {})
245
+ for key in ["group_column", "value_column"]:
246
+ col = data_ref.get(key)
247
+ if col and columns and col not in columns:
248
+ errors.append(f"Stats references unknown column '{col}'")
249
+
250
+ return errors
251
+
252
+
253
+ # =============================================================================
254
+ # Level 3: Strict Validation (Explicit call)
255
+ # =============================================================================
256
+
257
+
258
+ def validate_strict(
259
+ node: Optional[Dict] = None,
260
+ encoding: Optional[Dict] = None,
261
+ theme: Optional[Dict] = None,
262
+ stats: Optional[Dict] = None,
263
+ data_info: Optional[Dict] = None,
264
+ ) -> List[str]:
265
+ """Strict validation for publication/CI.
266
+
267
+ All semantic checks plus:
268
+ - Units must be specified
269
+ - Provenance must be complete
270
+ - All metadata fields populated
271
+ """
272
+ errors = []
273
+
274
+ # Include all semantic errors
275
+ errors.extend(
276
+ validate_semantic(
277
+ node=node,
278
+ encoding=encoding,
279
+ theme=theme,
280
+ stats=stats,
281
+ data_info=data_info,
282
+ )
283
+ )
284
+
285
+ # Units must be specified in data_info
286
+ if data_info:
287
+ columns = data_info.get("columns", [])
288
+ for col in columns:
289
+ if col.get("dtype") in ["float64", "int64", "number"]:
290
+ if not col.get("unit"):
291
+ errors.append(f"Column '{col.get('name')}' missing unit specification")
292
+
293
+ # Provenance must be present
294
+ if data_info and not data_info.get("source"):
295
+ errors.append("data_info.source (provenance) is required for publication")
296
+
297
+ # Node must have all metadata
298
+ if node:
299
+ if not node.get("name"):
300
+ errors.append("node.name is required for publication")
301
+ if not node.get("created_at"):
302
+ errors.append("node.created_at is required for publication")
303
+
304
+ return errors
305
+
306
+
307
+ # =============================================================================
308
+ # Unified Validation Entry Point
309
+ # =============================================================================
310
+
311
+
312
+ def validate(
313
+ data: Dict[str, Any],
314
+ schema_name: str,
315
+ level: ValidationLevel = "schema",
316
+ ) -> ValidationResult:
317
+ """Validate data at specified level.
318
+
319
+ Parameters
320
+ ----------
321
+ data : dict
322
+ Data to validate
323
+ schema_name : str
324
+ Schema name: node, encoding, theme, stats, data_info
325
+ level : str
326
+ Validation level: schema, semantic, strict
327
+
328
+ Returns
329
+ -------
330
+ ValidationResult
331
+ Validation result with errors and warnings
332
+ """
333
+ result = ValidationResult(level=level)
334
+
335
+ # Level 1: Schema validation (always)
336
+ schema_errors = validate_schema(data, schema_name)
337
+ result.errors.extend(schema_errors)
338
+
339
+ return result
340
+
341
+
342
+ def validate_bundle(
343
+ bundle_path: Union[str, Path],
344
+ level: ValidationLevel = "schema",
345
+ ) -> Dict[str, ValidationResult]:
346
+ """Validate all JSON files in a bundle at specified level.
347
+
348
+ Parameters
349
+ ----------
350
+ bundle_path : str or Path
351
+ Path to bundle directory or ZIP
352
+ level : str
353
+ Validation level: schema, semantic, strict
354
+
355
+ Returns
356
+ -------
357
+ dict
358
+ Dictionary mapping filename to ValidationResult
359
+ """
360
+ import zipfile
361
+
362
+ bundle_path = Path(bundle_path)
363
+ results = {}
364
+
365
+ def validate_json_file(name: str, content: str) -> ValidationResult:
366
+ """Validate a JSON file content."""
367
+ result = ValidationResult(level=level)
368
+ try:
369
+ data = json.loads(content)
370
+ except json.JSONDecodeError as e:
371
+ result.errors.append(f"Invalid JSON: {e}")
372
+ return result
373
+
374
+ # Determine schema from filename
375
+ schema_map = {
376
+ "node.json": "node",
377
+ "encoding.json": "encoding",
378
+ "theme.json": "theme",
379
+ }
380
+
381
+ for suffix, schema in schema_map.items():
382
+ if name.endswith(suffix):
383
+ result.errors.extend(validate_schema(data, schema))
384
+ break
385
+ else:
386
+ if name.endswith("stats.json"):
387
+ result.errors.extend(validate_schema(data, "stats"))
388
+ elif name.endswith("data_info.json"):
389
+ result.errors.extend(validate_schema(data, "data_info"))
390
+
391
+ return result
392
+
393
+ if bundle_path.is_file() and bundle_path.suffix == ".zip":
394
+ with zipfile.ZipFile(bundle_path, "r") as zf:
395
+ for name in zf.namelist():
396
+ if name.endswith(".json"):
397
+ content = zf.read(name).decode("utf-8")
398
+ results[name] = validate_json_file(name, content)
399
+ elif bundle_path.is_dir():
400
+ for json_file in bundle_path.rglob("*.json"):
401
+ rel_path = json_file.relative_to(bundle_path)
402
+ with open(json_file) as f:
403
+ content = f.read()
404
+ results[str(rel_path)] = validate_json_file(str(rel_path), content)
405
+
406
+ return results
407
+
408
+
409
+ __all__ = [
410
+ # Version/paths
411
+ "SCHEMA_VERSION",
412
+ "SCHEMA_DIR",
413
+ # Result class
414
+ "ValidationResult",
415
+ "ValidationLevel",
416
+ # Schema loading
417
+ "load_schema",
418
+ # Level 1: Schema
419
+ "validate_schema",
420
+ "validate_node",
421
+ "validate_encoding",
422
+ "validate_theme",
423
+ "validate_stats",
424
+ "validate_data_info",
425
+ # Level 2: Semantic
426
+ "validate_semantic",
427
+ # Level 3: Strict
428
+ "validate_strict",
429
+ # Unified
430
+ "validate",
431
+ "validate_bundle",
432
+ ]
433
+
434
+ # EOF
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_zipbundle.py
4
+
5
+ """ZipBundle - Simple ZIP file wrapper for FTS bundles."""
6
+
7
+ import json
8
+ import zipfile
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional, Union
11
+
12
+ __all__ = ["ZipBundle"]
13
+
14
+
15
+ class ZipBundle:
16
+ """Simple ZIP file wrapper for reading/writing bundle data.
17
+
18
+ Provides a context manager interface for working with ZIP files.
19
+
20
+ Usage:
21
+ # Read mode
22
+ with ZipBundle("bundle.zip", mode="r") as bundle:
23
+ spec = bundle.read_json("spec.json")
24
+ data = bundle.read_bytes("data.csv")
25
+
26
+ # Write/append mode
27
+ with ZipBundle("bundle.zip", mode="a") as bundle:
28
+ bundle.write_json("spec.json", {"type": "plot"})
29
+ bundle.write_bytes("data.csv", csv_bytes)
30
+ """
31
+
32
+ def __init__(self, path: Union[str, Path], mode: str = "r"):
33
+ """Initialize ZipBundle.
34
+
35
+ Args:
36
+ path: Path to ZIP file.
37
+ mode: File mode ('r' for read, 'w' for write, 'a' for append).
38
+ """
39
+ self.path = Path(path)
40
+ self.mode = mode
41
+ self._zf: Optional[zipfile.ZipFile] = None
42
+
43
+ def __enter__(self) -> "ZipBundle":
44
+ """Enter context manager."""
45
+ self._zf = zipfile.ZipFile(self.path, self.mode)
46
+ return self
47
+
48
+ def __exit__(self, exc_type, exc_val, exc_tb):
49
+ """Exit context manager."""
50
+ if self._zf:
51
+ self._zf.close()
52
+ self._zf = None
53
+
54
+ def read_json(self, name: str) -> Dict[str, Any]:
55
+ """Read JSON file from ZIP.
56
+
57
+ Args:
58
+ name: File name within ZIP.
59
+
60
+ Returns:
61
+ Parsed JSON data.
62
+ """
63
+ if not self._zf:
64
+ raise RuntimeError("ZipBundle not open")
65
+
66
+ # Try direct path first
67
+ try:
68
+ data = self._zf.read(name)
69
+ return json.loads(data.decode("utf-8"))
70
+ except KeyError:
71
+ pass
72
+
73
+ # Try with .d/ directory prefix
74
+ for item in self._zf.namelist():
75
+ if item.endswith(f"/{name}") or item == name:
76
+ data = self._zf.read(item)
77
+ return json.loads(data.decode("utf-8"))
78
+
79
+ raise FileNotFoundError(f"File not found in ZIP: {name}")
80
+
81
+ def read_bytes(self, name: str) -> bytes:
82
+ """Read raw bytes from ZIP.
83
+
84
+ Args:
85
+ name: File name within ZIP.
86
+
87
+ Returns:
88
+ Raw file bytes.
89
+ """
90
+ if not self._zf:
91
+ raise RuntimeError("ZipBundle not open")
92
+
93
+ # Try direct path first
94
+ try:
95
+ return self._zf.read(name)
96
+ except KeyError:
97
+ pass
98
+
99
+ # Try with .d/ directory prefix
100
+ for item in self._zf.namelist():
101
+ if item.endswith(f"/{name}") or item == name:
102
+ return self._zf.read(item)
103
+
104
+ raise FileNotFoundError(f"File not found in ZIP: {name}")
105
+
106
+ def write_json(self, name: str, data: Dict[str, Any], indent: int = 2) -> None:
107
+ """Write JSON file to ZIP.
108
+
109
+ Args:
110
+ name: File name within ZIP.
111
+ data: Data to write.
112
+ indent: JSON indentation level.
113
+ """
114
+ if not self._zf:
115
+ raise RuntimeError("ZipBundle not open")
116
+
117
+ json_str = json.dumps(data, indent=indent)
118
+ self._zf.writestr(name, json_str.encode("utf-8"))
119
+
120
+ def write_bytes(self, name: str, data: bytes) -> None:
121
+ """Write raw bytes to ZIP.
122
+
123
+ Args:
124
+ name: File name within ZIP.
125
+ data: Data to write.
126
+ """
127
+ if not self._zf:
128
+ raise RuntimeError("ZipBundle not open")
129
+
130
+ self._zf.writestr(name, data)
131
+
132
+ def namelist(self) -> list:
133
+ """List files in ZIP.
134
+
135
+ Returns:
136
+ List of file names.
137
+ """
138
+ if not self._zf:
139
+ raise RuntimeError("ZipBundle not open")
140
+ return self._zf.namelist()
141
+
142
+ def exists(self, name: str) -> bool:
143
+ """Check if file exists in ZIP.
144
+
145
+ Args:
146
+ name: File name to check.
147
+
148
+ Returns:
149
+ True if file exists.
150
+ """
151
+ if not self._zf:
152
+ raise RuntimeError("ZipBundle not open")
153
+
154
+ if name in self._zf.namelist():
155
+ return True
156
+
157
+ # Check with .d/ directory prefix
158
+ for item in self._zf.namelist():
159
+ if item.endswith(f"/{name}"):
160
+ return True
161
+
162
+ return False
163
+
164
+
165
+ # EOF
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_fig/__init__.py
4
+
5
+ """FTS Figure - Encoding and Theme dataclasses."""
6
+
7
+ # Public dataclasses
8
+ from ._dataclasses import (
9
+ ENCODING_VERSION,
10
+ THEME_VERSION,
11
+ Encoding,
12
+ Theme,
13
+ )
14
+
15
+ __all__ = [
16
+ "ENCODING_VERSION",
17
+ "THEME_VERSION",
18
+ "Encoding",
19
+ "Theme",
20
+ ]
21
+
22
+ # EOF