scitex 2.8.1__py3-none-any.whl → 2.10.2__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.
- scitex/__init__.py +15 -7
- scitex/__version__.py +1 -2
- scitex/_install_guide.py +250 -0
- scitex/_optional_deps.py +206 -39
- scitex/ai/_gen_ai/_Groq.py +2 -4
- scitex/ai/_gen_ai/_OpenAI.py +5 -2
- scitex/ai/_gen_ai/_Perplexity.py +20 -6
- scitex/audio/__init__.py +24 -15
- scitex/audio/_cross_process_lock.py +139 -0
- scitex/audio/_mcp_handlers.py +256 -0
- scitex/audio/_mcp_tool_schemas.py +203 -0
- scitex/audio/engines/elevenlabs_engine.py +5 -2
- scitex/audio/mcp_server.py +98 -457
- scitex/bridge/__init__.py +30 -19
- scitex/bridge/_figrecipe.py +245 -0
- scitex/bridge/_helpers.py +2 -1
- scitex/bridge/_plt_vis.py +23 -10
- scitex/bridge/_stats_plt.py +18 -5
- scitex/bridge/_stats_vis.py +16 -2
- scitex/browser/__init__.py +84 -44
- scitex/browser/automation/__init__.py +5 -1
- scitex/browser/core/BrowserMixin.py +17 -4
- scitex/browser/core/__init__.py +11 -2
- scitex/browser/remote/CaptchaHandler.py +1 -1
- scitex/browser/remote/ZenRowsAPIClient.py +1 -1
- scitex/capture/grid.py +487 -0
- scitex/capture/mcp_handlers.py +401 -0
- scitex/capture/mcp_tool_defs.py +192 -0
- scitex/capture/mcp_tools.py +241 -0
- scitex/capture/mcp_utils.py +30 -0
- scitex/cli/convert.py +421 -0
- scitex/cli/main.py +6 -4
- scitex/datetime/__init__.py +46 -0
- scitex/datetime/_linspace.py +100 -0
- scitex/datetime/_normalize_timestamp.py +306 -0
- scitex/db/_delete_duplicates.py +4 -4
- scitex/db/_sqlite3/_delete_duplicates.py +11 -2
- scitex/dev/plt/__init__.py +61 -62
- scitex/dev/plt/demo_plotters/__init__.py +0 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axhline.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axhspan.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axvline.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_axvspan.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_bar.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_barh.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_boxplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_contour.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_contourf.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_errorbar.py +30 -0
- scitex/dev/plt/demo_plotters/plot_mpl_eventplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_fill.py +30 -0
- scitex/dev/plt/demo_plotters/plot_mpl_fill_between.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_hexbin.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_hist.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_hist2d.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_imshow.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_pcolormesh.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_pie.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_plot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_quiver.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_scatter.py +28 -0
- scitex/dev/plt/demo_plotters/plot_mpl_stackplot.py +31 -0
- scitex/dev/plt/demo_plotters/plot_mpl_stem.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_step.py +29 -0
- scitex/dev/plt/demo_plotters/plot_mpl_violinplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_sns_barplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_boxplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_heatmap.py +28 -0
- scitex/dev/plt/demo_plotters/plot_sns_histplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_kdeplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_lineplot.py +31 -0
- scitex/dev/plt/demo_plotters/plot_sns_scatterplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_stripplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_swarmplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_sns_violinplot.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_bar.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_barh.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_box.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_boxplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_conf_mat.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_contour.py +31 -0
- scitex/dev/plt/demo_plotters/plot_stx_ecdf.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_errorbar.py +30 -0
- scitex/dev/plt/demo_plotters/plot_stx_fill_between.py +31 -0
- scitex/dev/plt/demo_plotters/plot_stx_fillv.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_heatmap.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_image.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_imshow.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_joyplot.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_kde.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_line.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_mean_ci.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_mean_std.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_median_iqr.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_raster.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_rectangle.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_scatter.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_shaded_line.py +29 -0
- scitex/dev/plt/demo_plotters/plot_stx_violin.py +28 -0
- scitex/dev/plt/demo_plotters/plot_stx_violinplot.py +28 -0
- scitex/dev/plt/mpl/get_dir_ax.py +46 -0
- scitex/dev/plt/mpl/get_signatures.py +176 -0
- scitex/dev/plt/mpl/get_signatures_details.py +522 -0
- scitex/dict/_pop_keys.py +1 -7
- scitex/dsp/__init__.py +15 -10
- scitex/dsp/add_noise.py +5 -2
- scitex/dsp/example.py +35 -22
- scitex/dsp/filt.py +8 -3
- scitex/dsp/reference.py +3 -2
- scitex/dsp/utils/__init__.py +2 -1
- scitex/dsp/utils/_differential_bandpass_filters.py +14 -4
- scitex/dt/__init__.py +39 -2
- scitex/errors.py +82 -521
- scitex/fig/__init__.py +4 -4
- scitex/fig/editor/edit/panel_loader.py +1 -1
- scitex/fig/io/_bundle.py +7 -7
- scitex/fts/README.md +262 -0
- scitex/fts/TODO.md +66 -0
- scitex/fts/__init__.py +90 -0
- scitex/fts/_bundle/README_IN_BUNDLE.md +102 -0
- scitex/fts/_bundle/_FTS.py +657 -0
- scitex/fts/_bundle/__init__.py +38 -0
- scitex/fts/_bundle/_children.py +216 -0
- scitex/fts/_bundle/_conversion/__init__.py +15 -0
- scitex/fts/_bundle/_conversion/_bundle2dict.py +44 -0
- scitex/fts/_bundle/_conversion/_dict2bundle.py +50 -0
- scitex/fts/_bundle/_dataclasses/_Axes.py +57 -0
- scitex/fts/_bundle/_dataclasses/_BBox.py +54 -0
- scitex/fts/_bundle/_dataclasses/_ColumnDef.py +72 -0
- scitex/fts/_bundle/_dataclasses/_DataFormat.py +40 -0
- scitex/fts/_bundle/_dataclasses/_DataInfo.py +135 -0
- scitex/fts/_bundle/_dataclasses/_DataSource.py +44 -0
- scitex/fts/_bundle/_dataclasses/_Node.py +319 -0
- scitex/fts/_bundle/_dataclasses/_NodeRefs.py +45 -0
- scitex/fts/_bundle/_dataclasses/_SizeMM.py +38 -0
- scitex/fts/_bundle/_dataclasses/__init__.py +35 -0
- scitex/fts/_bundle/_extractors/__init__.py +32 -0
- scitex/fts/_bundle/_extractors/_extract_bar.py +131 -0
- scitex/fts/_bundle/_extractors/_extract_line.py +71 -0
- scitex/fts/_bundle/_extractors/_extract_scatter.py +79 -0
- scitex/fts/_bundle/_loader.py +134 -0
- scitex/fts/_bundle/_mpl_helpers.py +389 -0
- scitex/fts/_bundle/_saver.py +269 -0
- scitex/fts/_bundle/_storage.py +200 -0
- scitex/fts/_bundle/_utils/__init__.py +55 -0
- scitex/fts/_bundle/_utils/_const.py +26 -0
- scitex/fts/_bundle/_utils/_errors.py +73 -0
- scitex/fts/_bundle/_utils/_generate.py +21 -0
- scitex/fts/_bundle/_utils/_types.py +76 -0
- scitex/fts/_bundle/_validation.py +434 -0
- scitex/fts/_bundle/_zipbundle.py +165 -0
- scitex/fts/_fig/__init__.py +22 -0
- scitex/fts/_fig/_backend/__init__.py +53 -0
- scitex/fts/_fig/_backend/_export.py +165 -0
- scitex/fts/_fig/_backend/_parser.py +188 -0
- scitex/fts/_fig/_backend/_render.py +538 -0
- scitex/fts/_fig/_composite.py +345 -0
- scitex/fts/_fig/_dataclasses/_ChannelEncoding.py +46 -0
- scitex/fts/_fig/_dataclasses/_Encoding.py +82 -0
- scitex/fts/_fig/_dataclasses/_Theme.py +441 -0
- scitex/fts/_fig/_dataclasses/_TraceEncoding.py +52 -0
- scitex/fts/_fig/_dataclasses/__init__.py +47 -0
- scitex/fts/_fig/_editor/__init__.py +14 -0
- scitex/fts/_fig/_editor/_cui/__init__.py +33 -0
- scitex/fts/_fig/_editor/_cui/_backend_detector.py +39 -0
- scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +366 -0
- scitex/fts/_fig/_editor/_cui/_editor_launcher.py +175 -0
- scitex/fts/_fig/_editor/_cui/_manual_handler.py +52 -0
- scitex/fts/_fig/_editor/_cui/_panel_loader.py +246 -0
- scitex/fts/_fig/_editor/_cui/_path_resolver.py +66 -0
- scitex/fts/_fig/_editor/_defaults.py +300 -0
- scitex/fts/_fig/_editor/_gui/__init__.py +11 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +20 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_bbox.py +1339 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_core.py +1688 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +664 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_renderer.py +853 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +79 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +41 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +16 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +85 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +217 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +93 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +57 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +112 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +59 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +212 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/canvas.css +176 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +190 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +59 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +45 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +95 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +101 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +138 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +31 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +7 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +56 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +78 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +314 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +107 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +54 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +172 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +258 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +48 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +71 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +288 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +143 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +245 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +992 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +339 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +286 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +371 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +293 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +426 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +152 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +265 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +184 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +57 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +100 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +34 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +124 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +851 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +4932 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +1657 -0
- scitex/fts/_fig/_editor/_gui/_flask_editor.py +36 -0
- scitex/fts/_fig/_models/_Annotations.py +115 -0
- scitex/fts/_fig/_models/_Axes.py +152 -0
- scitex/fts/_fig/_models/_Figure.py +138 -0
- scitex/fts/_fig/_models/_Guides.py +104 -0
- scitex/fts/_fig/_models/_Plot.py +123 -0
- scitex/fts/_fig/_models/_Styles.py +245 -0
- scitex/fts/_fig/_models/__init__.py +80 -0
- scitex/fts/_fig/_models/_plot_types/__init__.py +156 -0
- scitex/fts/_fig/_models/_plot_types/_bar.py +43 -0
- scitex/fts/_fig/_models/_plot_types/_box.py +38 -0
- scitex/fts/_fig/_models/_plot_types/_distribution.py +36 -0
- scitex/fts/_fig/_models/_plot_types/_errorbar.py +60 -0
- scitex/fts/_fig/_models/_plot_types/_histogram.py +30 -0
- scitex/fts/_fig/_models/_plot_types/_image.py +61 -0
- scitex/fts/_fig/_models/_plot_types/_line.py +57 -0
- scitex/fts/_fig/_models/_plot_types/_scatter.py +30 -0
- scitex/fts/_fig/_models/_plot_types/_seaborn.py +121 -0
- scitex/fts/_fig/_models/_plot_types/_violin.py +36 -0
- scitex/fts/_fig/_utils/__init__.py +129 -0
- scitex/fts/_fig/_utils/_auto_layout.py +127 -0
- scitex/fts/_fig/_utils/_calc_bounds.py +111 -0
- scitex/fts/_fig/_utils/_const_sizes.py +48 -0
- scitex/fts/_fig/_utils/_convert_coords.py +77 -0
- scitex/fts/_fig/_utils/_get_template.py +178 -0
- scitex/fts/_fig/_utils/_normalize.py +73 -0
- scitex/fts/_fig/_utils/_plot_layout.py +397 -0
- scitex/fts/_fig/_utils/_validate.py +197 -0
- scitex/fts/_kinds/__init__.py +45 -0
- scitex/fts/_kinds/_figure/__init__.py +19 -0
- scitex/fts/_kinds/_figure/_composite.py +345 -0
- scitex/fts/_kinds/_plot/__init__.py +25 -0
- scitex/fts/_kinds/_plot/_backend/__init__.py +53 -0
- scitex/fts/_kinds/_plot/_backend/_export.py +165 -0
- scitex/fts/_kinds/_plot/_backend/_parser.py +188 -0
- scitex/fts/_kinds/_plot/_backend/_render.py +538 -0
- scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +46 -0
- scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +82 -0
- scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +441 -0
- scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +52 -0
- scitex/fts/_kinds/_plot/_dataclasses/__init__.py +47 -0
- scitex/fts/_kinds/_plot/_models/_Annotations.py +115 -0
- scitex/fts/_kinds/_plot/_models/_Axes.py +152 -0
- scitex/fts/_kinds/_plot/_models/_Figure.py +138 -0
- scitex/fts/_kinds/_plot/_models/_Guides.py +104 -0
- scitex/fts/_kinds/_plot/_models/_Plot.py +123 -0
- scitex/fts/_kinds/_plot/_models/_Styles.py +245 -0
- scitex/fts/_kinds/_plot/_models/__init__.py +80 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +156 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +43 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +38 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +36 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +60 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +30 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +61 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +57 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +30 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +121 -0
- scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +36 -0
- scitex/fts/_kinds/_plot/_utils/__init__.py +129 -0
- scitex/fts/_kinds/_plot/_utils/_auto_layout.py +127 -0
- scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +111 -0
- scitex/fts/_kinds/_plot/_utils/_const_sizes.py +48 -0
- scitex/fts/_kinds/_plot/_utils/_convert_coords.py +77 -0
- scitex/fts/_kinds/_plot/_utils/_get_template.py +178 -0
- scitex/fts/_kinds/_plot/_utils/_normalize.py +73 -0
- scitex/fts/_kinds/_plot/_utils/_plot_layout.py +397 -0
- scitex/fts/_kinds/_plot/_utils/_validate.py +197 -0
- scitex/fts/_kinds/_shape/__init__.py +141 -0
- scitex/fts/_kinds/_stats/__init__.py +56 -0
- scitex/fts/_kinds/_stats/_dataclasses/_Stats.py +423 -0
- scitex/fts/_kinds/_stats/_dataclasses/__init__.py +48 -0
- scitex/fts/_kinds/_table/__init__.py +72 -0
- scitex/fts/_kinds/_table/_latex/__init__.py +93 -0
- scitex/fts/_kinds/_table/_latex/_editor/__init__.py +11 -0
- scitex/fts/_kinds/_table/_latex/_editor/_app.py +725 -0
- scitex/fts/_kinds/_table/_latex/_export.py +279 -0
- scitex/fts/_kinds/_table/_latex/_figure_exporter.py +153 -0
- scitex/fts/_kinds/_table/_latex/_stats_formatter.py +274 -0
- scitex/fts/_kinds/_table/_latex/_table_exporter.py +362 -0
- scitex/fts/_kinds/_table/_latex/_utils.py +369 -0
- scitex/fts/_kinds/_table/_latex/_validator.py +445 -0
- scitex/fts/_kinds/_text/__init__.py +77 -0
- scitex/fts/_schemas/data_info.schema.json +75 -0
- scitex/fts/_schemas/encoding.schema.json +90 -0
- scitex/fts/_schemas/node.schema.json +145 -0
- scitex/fts/_schemas/render_manifest.schema.json +62 -0
- scitex/fts/_schemas/stats.schema.json +132 -0
- scitex/fts/_schemas/theme.schema.json +141 -0
- scitex/fts/_stats/__init__.py +48 -0
- scitex/fts/_stats/_dataclasses/_Stats.py +423 -0
- scitex/fts/_stats/_dataclasses/__init__.py +48 -0
- scitex/fts/_tables/__init__.py +65 -0
- scitex/fts/_tables/_latex/__init__.py +93 -0
- scitex/fts/_tables/_latex/_editor/__init__.py +11 -0
- scitex/fts/_tables/_latex/_editor/_app.py +725 -0
- scitex/fts/_tables/_latex/_export.py +279 -0
- scitex/fts/_tables/_latex/_figure_exporter.py +153 -0
- scitex/fts/_tables/_latex/_stats_formatter.py +274 -0
- scitex/fts/_tables/_latex/_table_exporter.py +362 -0
- scitex/fts/_tables/_latex/_utils.py +369 -0
- scitex/fts/_tables/_latex/_validator.py +445 -0
- scitex/gen/__init__.py +66 -25
- scitex/gen/misc.py +28 -0
- scitex/io/__init__.py +47 -32
- scitex/io/_load.py +87 -36
- scitex/io/_load_modules/__init__.py +10 -7
- scitex/io/_load_modules/_pandas.py +6 -1
- scitex/io/_save.py +299 -1556
- scitex/io/_save_modules/__init__.py +76 -19
- scitex/io/_save_modules/_figure_utils.py +90 -0
- scitex/io/_save_modules/_image_csv.py +497 -0
- scitex/io/_save_modules/_legends.py +91 -0
- scitex/io/_save_modules/_pltz_bundle.py +356 -0
- scitex/io/_save_modules/_pltz_stx.py +536 -0
- scitex/io/_save_modules/_stx_bundle.py +104 -0
- scitex/io/_save_modules/_symlink.py +96 -0
- scitex/io/_save_modules/_yaml.py +1 -1
- scitex/io/_save_modules/_zarr.py +64 -18
- scitex/io/bundle/README.md +212 -0
- scitex/io/bundle/__init__.py +110 -0
- scitex/io/{_bundle.py → bundle/_core.py} +168 -97
- scitex/io/bundle/_nested.py +713 -0
- scitex/io/bundle/_types.py +74 -0
- scitex/io/{_zip_bundle.py → bundle/_zip.py} +93 -45
- scitex/io/utils/h5_to_zarr.py +1 -1
- scitex/logging/__init__.py +108 -13
- scitex/logging/_errors.py +508 -0
- scitex/logging/_formatters.py +30 -6
- scitex/logging/_warnings.py +261 -0
- scitex/plt/__init__.py +4 -1
- scitex/plt/_figrecipe.py +236 -0
- scitex/plt/_subplots/_AxisWrapper.py +6 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +112 -1
- scitex/plt/_subplots/_FigWrapper.py +15 -0
- scitex/plt/_subplots/_SubplotsWrapper.py +125 -489
- scitex/plt/_subplots/_export_as_csv.py +11 -0
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +2 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_pcolormesh.py +66 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stackplot.py +62 -0
- scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +208 -0
- scitex/plt/_subplots/_fonts.py +71 -0
- scitex/plt/_subplots/_mm_layout.py +282 -0
- scitex/plt/gallery/__init__.py +99 -2
- scitex/plt/styles/_plot_postprocess.py +3 -1
- scitex/plt/utils/_configure_mpl.py +16 -19
- scitex/repro/_RandomStateManager.py +13 -8
- scitex/resource/__init__.py +19 -1
- scitex/resource/_utils/_get_env_info.py +13 -25
- scitex/schema/__init__.py +149 -160
- scitex/schema/_encoding.py +273 -0
- scitex/schema/_figure_elements.py +406 -0
- scitex/schema/_theme.py +360 -0
- scitex/schema/_validation.py +0 -98
- scitex/scholar/__init__.py +56 -14
- scitex/scholar/auth/ScholarAuthManager.py +1 -1
- scitex/scholar/auth/__init__.py +11 -2
- scitex/scholar/auth/providers/BaseAuthenticator.py +1 -1
- scitex/scholar/auth/providers/EZProxyAuthenticator.py +1 -1
- scitex/scholar/auth/providers/OpenAthensAuthenticator.py +1 -1
- scitex/scholar/auth/providers/ShibbolethAuthenticator.py +1 -1
- scitex/scholar/config/ScholarConfig.py +1 -1
- scitex/scholar/core/Scholar.py +1 -1
- scitex/session/_decorator.py +18 -16
- scitex/session/_lifecycle.py +9 -11
- scitex/session/template.py +9 -8
- scitex/sh/test_sh.py +72 -0
- scitex/sh/test_sh_simple.py +61 -0
- scitex/stats/__init__.py +221 -97
- scitex/stats/_schema.py +21 -22
- scitex/stats/descriptive/_circular.py +212 -351
- scitex/stats/descriptive/_describe.py +81 -132
- scitex/stats/descriptive/_nan.py +205 -433
- scitex/stats/descriptive/_real.py +127 -141
- scitex/str/_format_plot_text.py +5 -5
- scitex/str/_latex.py +26 -84
- scitex/str/_latex_fallback.py +53 -47
- scitex/web/_search_pubmed.py +5 -4
- scitex/writer/tests/test_diff_between.py +451 -0
- scitex/writer/tests/test_document_section.py +311 -0
- scitex/writer/tests/test_document_workflow.py +393 -0
- scitex/writer/tests/test_writer.py +361 -0
- scitex/writer/tests/test_writer_integration.py +303 -0
- {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/METADATA +368 -183
- {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/RECORD +412 -97
- scitex/scholar/docs/to_claude/guidelines/examples/mgmt/ARCHITECTURE_EXAMPLE.md +0 -905
- scitex/scholar/docs/to_claude/guidelines/examples/mgmt/BULLETIN_BOARD_EXAMPLE.md +0 -99
- scitex/scholar/docs/to_claude/guidelines/examples/mgmt/PROJECT_DESCRIPTION_EXAMPLE.md +0 -96
- {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/WHEEL +0 -0
- {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.8.1.dist-info → scitex-2.10.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-20
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_saver.py
|
|
4
|
+
|
|
5
|
+
"""FTS Bundle saving utilities.
|
|
6
|
+
|
|
7
|
+
Bundle structure (IDENTICAL for all kinds):
|
|
8
|
+
bundle.zip/
|
|
9
|
+
├── canonical/ # Source of truth (editable, human-readable)
|
|
10
|
+
│ ├── spec.json # {kind, children, layout, payload_schema, ...}
|
|
11
|
+
│ ├── encoding.json # Data-to-visual mappings
|
|
12
|
+
│ ├── theme.json # Visual aesthetics
|
|
13
|
+
│ ├── data_info.json # Column metadata
|
|
14
|
+
│ └── runtime.json # Runtime configuration
|
|
15
|
+
├── payload/ # ALWAYS exists (empty for kind=figure)
|
|
16
|
+
│ ├── data.csv # Source data (for kind=plot)
|
|
17
|
+
│ └── stats.json # Statistics (for kind=stats)
|
|
18
|
+
├── artifacts/ # Derived (can be deleted and regenerated)
|
|
19
|
+
│ ├── cache/
|
|
20
|
+
│ │ ├── geometry_px.json
|
|
21
|
+
│ │ ├── hitmap.png
|
|
22
|
+
│ │ ├── hitmap.svg
|
|
23
|
+
│ │ └── render_manifest.json
|
|
24
|
+
│ └── exports/
|
|
25
|
+
│ ├── figure.png
|
|
26
|
+
│ ├── figure.svg
|
|
27
|
+
│ └── figure.pdf
|
|
28
|
+
└── children/ # ALWAYS exists (empty for kind=plot)
|
|
29
|
+
└── {child_id}.zip
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import io
|
|
34
|
+
import json
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
37
|
+
|
|
38
|
+
from ._storage import Storage, get_storage
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from ._dataclasses import DataInfo, Node
|
|
42
|
+
from .._fig import Encoding, Theme
|
|
43
|
+
from .._stats import Stats
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_bundle_components(
|
|
47
|
+
path: Path,
|
|
48
|
+
node: Optional["Node"] = None,
|
|
49
|
+
encoding: Optional["Encoding"] = None,
|
|
50
|
+
theme: Optional["Theme"] = None,
|
|
51
|
+
stats: Optional["Stats"] = None,
|
|
52
|
+
data_info: Optional["DataInfo"] = None,
|
|
53
|
+
render: bool = True,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Save all bundle components to storage.
|
|
56
|
+
|
|
57
|
+
Uses the new canonical/artifacts/payload/children structure.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Bundle path (directory or ZIP)
|
|
61
|
+
node: Node metadata (saved to canonical/spec.json)
|
|
62
|
+
encoding: Encoding specification (saved to canonical/encoding.json)
|
|
63
|
+
theme: Theme specification (saved to canonical/theme.json)
|
|
64
|
+
stats: Statistics (saved to payload/stats.json for kind=stats)
|
|
65
|
+
data_info: Data info metadata (saved to canonical/data_info.json)
|
|
66
|
+
render: Whether to generate exports/cache (default True)
|
|
67
|
+
"""
|
|
68
|
+
storage = get_storage(path)
|
|
69
|
+
|
|
70
|
+
# Collect all files to write
|
|
71
|
+
files = {}
|
|
72
|
+
|
|
73
|
+
# === ALWAYS create all directories and placeholder files ===
|
|
74
|
+
# Use .keep files as directory markers for ZIP compatibility
|
|
75
|
+
files["canonical/.keep"] = ""
|
|
76
|
+
files["payload/.keep"] = ""
|
|
77
|
+
files["payload/data.csv"] = "" # Empty CSV for consistency
|
|
78
|
+
files["artifacts/.keep"] = ""
|
|
79
|
+
files["artifacts/exports/.keep"] = ""
|
|
80
|
+
files["artifacts/cache/.keep"] = ""
|
|
81
|
+
files["children/.keep"] = ""
|
|
82
|
+
|
|
83
|
+
# canonical/ - Source of truth
|
|
84
|
+
if node:
|
|
85
|
+
files["canonical/spec.json"] = json.dumps(node.to_dict(), indent=2)
|
|
86
|
+
|
|
87
|
+
if encoding:
|
|
88
|
+
files["canonical/encoding.json"] = encoding.to_json()
|
|
89
|
+
|
|
90
|
+
if theme:
|
|
91
|
+
files["canonical/theme.json"] = theme.to_json()
|
|
92
|
+
|
|
93
|
+
if data_info:
|
|
94
|
+
files["canonical/data_info.json"] = data_info.to_json()
|
|
95
|
+
|
|
96
|
+
# payload/ - Data files (for leaf kinds)
|
|
97
|
+
if stats and stats.analyses:
|
|
98
|
+
files["payload/stats.json"] = stats.to_json()
|
|
99
|
+
|
|
100
|
+
# Write files, preserving any existing children/ files
|
|
101
|
+
# For ZIP, we need to merge with existing content
|
|
102
|
+
if hasattr(storage, 'write_all_preserve'):
|
|
103
|
+
storage.write_all_preserve(files)
|
|
104
|
+
else:
|
|
105
|
+
# Fallback: write each file individually (preserves existing)
|
|
106
|
+
for name, data in files.items():
|
|
107
|
+
if isinstance(data, str):
|
|
108
|
+
data = data.encode("utf-8")
|
|
109
|
+
storage.write(name, data)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def save_render_outputs(
|
|
113
|
+
storage: Storage,
|
|
114
|
+
figure: Any, # matplotlib.figure.Figure
|
|
115
|
+
geometry: Dict,
|
|
116
|
+
source_hash: str,
|
|
117
|
+
theme_hash: str,
|
|
118
|
+
renderer_version: str = "1.0.0",
|
|
119
|
+
dpi: int = 300,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Save artifacts/exports/ and artifacts/cache/.
|
|
122
|
+
|
|
123
|
+
Same structure for both kind=plot and kind=figure.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
storage: Bundle storage
|
|
127
|
+
figure: Matplotlib figure to save
|
|
128
|
+
geometry: Geometry data (hit areas, element positions)
|
|
129
|
+
source_hash: Hash of canonical/ for cache invalidation
|
|
130
|
+
theme_hash: Hash of effective theme (after parent overrides)
|
|
131
|
+
renderer_version: Renderer version for cache invalidation
|
|
132
|
+
dpi: Output DPI for raster formats
|
|
133
|
+
"""
|
|
134
|
+
# Save artifacts/exports/
|
|
135
|
+
for fmt in ["png", "svg", "pdf"]:
|
|
136
|
+
buf = io.BytesIO()
|
|
137
|
+
figure.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight")
|
|
138
|
+
storage.write(f"artifacts/exports/figure.{fmt}", buf.getvalue())
|
|
139
|
+
|
|
140
|
+
# Save artifacts/cache/geometry_px.json
|
|
141
|
+
# Add coordinate space declaration
|
|
142
|
+
geometry_with_space = {
|
|
143
|
+
"space": "figure_px", # Coordinate space declaration
|
|
144
|
+
**geometry,
|
|
145
|
+
}
|
|
146
|
+
storage.write(
|
|
147
|
+
"artifacts/cache/geometry_px.json",
|
|
148
|
+
json.dumps(geometry_with_space, indent=2).encode(),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Generate and save hitmaps
|
|
152
|
+
hitmap_png, hitmap_svg = generate_hitmap(figure, geometry, dpi)
|
|
153
|
+
if hitmap_png:
|
|
154
|
+
storage.write("artifacts/cache/hitmap.png", hitmap_png)
|
|
155
|
+
if hitmap_svg:
|
|
156
|
+
storage.write("artifacts/cache/hitmap.svg", hitmap_svg)
|
|
157
|
+
|
|
158
|
+
# Save render manifest (includes cache invalidation keys)
|
|
159
|
+
manifest = {
|
|
160
|
+
"dpi": dpi,
|
|
161
|
+
"formats": ["png", "svg", "pdf"],
|
|
162
|
+
# Cache invalidation keys
|
|
163
|
+
"canonical_hash": source_hash,
|
|
164
|
+
"effective_theme_hash": theme_hash,
|
|
165
|
+
"renderer_version": renderer_version,
|
|
166
|
+
}
|
|
167
|
+
storage.write(
|
|
168
|
+
"artifacts/cache/render_manifest.json",
|
|
169
|
+
json.dumps(manifest, indent=2).encode(),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def generate_hitmap(
|
|
174
|
+
figure: Any, # matplotlib.figure.Figure
|
|
175
|
+
geometry: Dict,
|
|
176
|
+
dpi: int = 300,
|
|
177
|
+
) -> tuple:
|
|
178
|
+
"""Generate hitmap.png and hitmap.svg for click detection.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
(hitmap_png_bytes, hitmap_svg_bytes) - bytes or None if failed
|
|
182
|
+
"""
|
|
183
|
+
# Basic implementation - generate colored regions for hit testing
|
|
184
|
+
# This is a simplified version; full implementation would color-code elements
|
|
185
|
+
try:
|
|
186
|
+
import io
|
|
187
|
+
|
|
188
|
+
import matplotlib.pyplot as plt
|
|
189
|
+
import numpy as np
|
|
190
|
+
|
|
191
|
+
# Create hitmap figure with same dimensions
|
|
192
|
+
fig_size = figure.get_size_inches()
|
|
193
|
+
hitmap_fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
|
|
194
|
+
ax.set_xlim(0, 1)
|
|
195
|
+
ax.set_ylim(0, 1)
|
|
196
|
+
ax.axis("off")
|
|
197
|
+
|
|
198
|
+
# Draw colored regions for each element in geometry
|
|
199
|
+
elements = geometry.get("elements", [])
|
|
200
|
+
for i, elem in enumerate(elements):
|
|
201
|
+
if "bbox" in elem:
|
|
202
|
+
bbox = elem["bbox"]
|
|
203
|
+
color = plt.cm.tab20(i % 20)
|
|
204
|
+
ax.add_patch(
|
|
205
|
+
plt.Rectangle(
|
|
206
|
+
(bbox.get("x", 0), bbox.get("y", 0)),
|
|
207
|
+
bbox.get("width", 0.1),
|
|
208
|
+
bbox.get("height", 0.1),
|
|
209
|
+
facecolor=color,
|
|
210
|
+
edgecolor="none",
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Save as PNG
|
|
215
|
+
png_buf = io.BytesIO()
|
|
216
|
+
hitmap_fig.savefig(png_buf, format="png", dpi=dpi, bbox_inches="tight")
|
|
217
|
+
png_bytes = png_buf.getvalue()
|
|
218
|
+
|
|
219
|
+
# Save as SVG
|
|
220
|
+
svg_buf = io.BytesIO()
|
|
221
|
+
hitmap_fig.savefig(svg_buf, format="svg", bbox_inches="tight")
|
|
222
|
+
svg_bytes = svg_buf.getvalue()
|
|
223
|
+
|
|
224
|
+
plt.close(hitmap_fig)
|
|
225
|
+
return png_bytes, svg_bytes
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
# If hitmap generation fails, return None (non-critical)
|
|
229
|
+
return None, None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def compute_canonical_hash(storage: Storage) -> str:
|
|
233
|
+
"""Compute hash of canonical/ directory for cache invalidation."""
|
|
234
|
+
hasher = hashlib.sha256()
|
|
235
|
+
|
|
236
|
+
canonical_files = [
|
|
237
|
+
"canonical/spec.json",
|
|
238
|
+
"canonical/encoding.json",
|
|
239
|
+
"canonical/theme.json",
|
|
240
|
+
"canonical/data_info.json",
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
for filepath in canonical_files:
|
|
244
|
+
if storage.exists(filepath):
|
|
245
|
+
data = storage.read(filepath)
|
|
246
|
+
hasher.update(data)
|
|
247
|
+
|
|
248
|
+
return hasher.hexdigest()[:16]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def compute_theme_hash(theme: Optional["Theme"]) -> str:
|
|
252
|
+
"""Compute hash of theme for cache invalidation."""
|
|
253
|
+
if theme is None:
|
|
254
|
+
return "default"
|
|
255
|
+
|
|
256
|
+
hasher = hashlib.sha256()
|
|
257
|
+
hasher.update(theme.to_json().encode())
|
|
258
|
+
return hasher.hexdigest()[:16]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = [
|
|
262
|
+
"save_bundle_components",
|
|
263
|
+
"save_render_outputs",
|
|
264
|
+
"generate_hitmap",
|
|
265
|
+
"compute_canonical_hash",
|
|
266
|
+
"compute_theme_hash",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
# EOF
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-20
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_storage.py
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Storage Abstraction for FTS Bundles.
|
|
7
|
+
|
|
8
|
+
Provides a unified interface for reading/writing bundle contents
|
|
9
|
+
regardless of whether the bundle is a ZIP file or directory.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
storage = get_storage(Path("bundle.zip")) # or Path("bundle/")
|
|
13
|
+
data = storage.read("node.json")
|
|
14
|
+
storage.write("encoding.json", json_bytes)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import zipfile
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import List, Optional, Union
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Storage(ABC):
|
|
25
|
+
"""Abstract storage interface for FTS bundles."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, path: Path):
|
|
28
|
+
self._path = path
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def path(self) -> Path:
|
|
32
|
+
"""Storage path."""
|
|
33
|
+
return self._path
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def exists(self, name: str) -> bool:
|
|
37
|
+
"""Check if file exists in storage."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def read(self, name: str) -> bytes:
|
|
42
|
+
"""Read file contents as bytes."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def write(self, name: str, data: bytes) -> None:
|
|
47
|
+
"""Write data to file."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def list(self) -> List[str]:
|
|
52
|
+
"""List all files in storage."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def read_json(self, name: str) -> Optional[dict]:
|
|
56
|
+
"""Read and parse JSON file."""
|
|
57
|
+
if not self.exists(name):
|
|
58
|
+
return None
|
|
59
|
+
data = self.read(name)
|
|
60
|
+
return json.loads(data.decode("utf-8"))
|
|
61
|
+
|
|
62
|
+
def write_json(self, name: str, obj: dict, indent: int = 2) -> None:
|
|
63
|
+
"""Write object as JSON file."""
|
|
64
|
+
data = json.dumps(obj, indent=indent).encode("utf-8")
|
|
65
|
+
self.write(name, data)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ZipStorage(Storage):
|
|
69
|
+
"""Storage implementation for ZIP files.
|
|
70
|
+
|
|
71
|
+
ZIP files are structured with a top-level directory named after the bundle.
|
|
72
|
+
For example, my_figure.zip contains:
|
|
73
|
+
my_figure/
|
|
74
|
+
node.json
|
|
75
|
+
encoding.json
|
|
76
|
+
theme.json
|
|
77
|
+
This ensures `unzip my_figure.zip` creates a my_figure/ directory.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def _prefix(self) -> str:
|
|
82
|
+
"""Top-level directory name inside the ZIP (bundle stem)."""
|
|
83
|
+
return self._path.stem + "/"
|
|
84
|
+
|
|
85
|
+
def _prefixed(self, name: str) -> str:
|
|
86
|
+
"""Add prefix to file path."""
|
|
87
|
+
return self._prefix + name
|
|
88
|
+
|
|
89
|
+
def _unprefixed(self, name: str) -> str:
|
|
90
|
+
"""Remove prefix from file path."""
|
|
91
|
+
if name.startswith(self._prefix):
|
|
92
|
+
return name[len(self._prefix) :]
|
|
93
|
+
return name
|
|
94
|
+
|
|
95
|
+
def exists(self, name: str) -> bool:
|
|
96
|
+
if not self._path.exists():
|
|
97
|
+
return False
|
|
98
|
+
with zipfile.ZipFile(self._path, "r") as zf:
|
|
99
|
+
# Check both prefixed and unprefixed for backwards compatibility
|
|
100
|
+
return self._prefixed(name) in zf.namelist() or name in zf.namelist()
|
|
101
|
+
|
|
102
|
+
def read(self, name: str) -> bytes:
|
|
103
|
+
with zipfile.ZipFile(self._path, "r") as zf:
|
|
104
|
+
# Try prefixed first, fall back to unprefixed for backwards compatibility
|
|
105
|
+
prefixed = self._prefixed(name)
|
|
106
|
+
if prefixed in zf.namelist():
|
|
107
|
+
return zf.read(prefixed)
|
|
108
|
+
return zf.read(name)
|
|
109
|
+
|
|
110
|
+
def write(self, name: str, data: bytes) -> None:
|
|
111
|
+
# ZIP files need special handling - read existing, add/update, rewrite
|
|
112
|
+
existing = {}
|
|
113
|
+
prefixed_name = self._prefixed(name)
|
|
114
|
+
if self._path.exists():
|
|
115
|
+
with zipfile.ZipFile(self._path, "r") as zf:
|
|
116
|
+
for item in zf.namelist():
|
|
117
|
+
if item != prefixed_name:
|
|
118
|
+
existing[item] = zf.read(item)
|
|
119
|
+
|
|
120
|
+
with zipfile.ZipFile(self._path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
121
|
+
for item_name, item_data in existing.items():
|
|
122
|
+
zf.writestr(item_name, item_data)
|
|
123
|
+
zf.writestr(prefixed_name, data)
|
|
124
|
+
|
|
125
|
+
def list(self) -> List[str]:
|
|
126
|
+
if not self._path.exists():
|
|
127
|
+
return []
|
|
128
|
+
with zipfile.ZipFile(self._path, "r") as zf:
|
|
129
|
+
# Return unprefixed names for API consistency
|
|
130
|
+
return [self._unprefixed(n) for n in zf.namelist() if not n.endswith("/")]
|
|
131
|
+
|
|
132
|
+
def write_all(self, files: dict) -> None:
|
|
133
|
+
"""Write multiple files at once (more efficient for ZIP).
|
|
134
|
+
|
|
135
|
+
Files are stored under a top-level directory matching the bundle name.
|
|
136
|
+
"""
|
|
137
|
+
with zipfile.ZipFile(self._path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
138
|
+
for name, data in files.items():
|
|
139
|
+
if isinstance(data, str):
|
|
140
|
+
data = data.encode("utf-8")
|
|
141
|
+
zf.writestr(self._prefixed(name), data)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class DirStorage(Storage):
|
|
145
|
+
"""Storage implementation for directories."""
|
|
146
|
+
|
|
147
|
+
def exists(self, name: str) -> bool:
|
|
148
|
+
return (self._path / name).exists()
|
|
149
|
+
|
|
150
|
+
def read(self, name: str) -> bytes:
|
|
151
|
+
with open(self._path / name, "rb") as f:
|
|
152
|
+
return f.read()
|
|
153
|
+
|
|
154
|
+
def write(self, name: str, data: bytes) -> None:
|
|
155
|
+
file_path = self._path / name
|
|
156
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
with open(file_path, "wb") as f:
|
|
158
|
+
f.write(data)
|
|
159
|
+
|
|
160
|
+
def list(self) -> List[str]:
|
|
161
|
+
if not self._path.exists():
|
|
162
|
+
return []
|
|
163
|
+
result = []
|
|
164
|
+
for item in self._path.rglob("*"):
|
|
165
|
+
if item.is_file():
|
|
166
|
+
result.append(str(item.relative_to(self._path)))
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def write_all(self, files: dict) -> None:
|
|
170
|
+
"""Write multiple files."""
|
|
171
|
+
self._path.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
for name, data in files.items():
|
|
173
|
+
if isinstance(data, str):
|
|
174
|
+
data = data.encode("utf-8")
|
|
175
|
+
self.write(name, data)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_storage(path: Union[str, Path]) -> Storage:
|
|
179
|
+
"""Get appropriate storage for path.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
path: Bundle path (directory or .zip file)
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Storage instance (ZipStorage or DirStorage)
|
|
186
|
+
"""
|
|
187
|
+
path = Path(path)
|
|
188
|
+
if path.suffix == ".zip":
|
|
189
|
+
return ZipStorage(path)
|
|
190
|
+
return DirStorage(path)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
__all__ = [
|
|
194
|
+
"Storage",
|
|
195
|
+
"ZipStorage",
|
|
196
|
+
"DirStorage",
|
|
197
|
+
"get_storage",
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
# EOF
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-20
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/__init__.py
|
|
4
|
+
|
|
5
|
+
"""FTS Bundle utilities - types, constants, errors, and helpers."""
|
|
6
|
+
|
|
7
|
+
# Constants
|
|
8
|
+
from ._const import EXTENSIONS, SCHEMA_NAME, SCHEMA_VERSION, ZIP_EXTENSION
|
|
9
|
+
|
|
10
|
+
# Types
|
|
11
|
+
from ._types import (
|
|
12
|
+
TYPE_DEFAULTS,
|
|
13
|
+
BundleType,
|
|
14
|
+
NodeType,
|
|
15
|
+
get_default_constraints,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Errors
|
|
19
|
+
from ._errors import (
|
|
20
|
+
BundleError,
|
|
21
|
+
BundleNotFoundError,
|
|
22
|
+
BundleValidationError,
|
|
23
|
+
CircularReferenceError,
|
|
24
|
+
ConstraintError,
|
|
25
|
+
DepthLimitError,
|
|
26
|
+
NestedBundleNotFoundError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Generation
|
|
30
|
+
from ._generate import generate_bundle_id
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Constants
|
|
34
|
+
"ZIP_EXTENSION",
|
|
35
|
+
"EXTENSIONS",
|
|
36
|
+
"SCHEMA_NAME",
|
|
37
|
+
"SCHEMA_VERSION",
|
|
38
|
+
# Types
|
|
39
|
+
"NodeType",
|
|
40
|
+
"BundleType",
|
|
41
|
+
"TYPE_DEFAULTS",
|
|
42
|
+
"get_default_constraints",
|
|
43
|
+
# Errors
|
|
44
|
+
"BundleError",
|
|
45
|
+
"BundleValidationError",
|
|
46
|
+
"BundleNotFoundError",
|
|
47
|
+
"NestedBundleNotFoundError",
|
|
48
|
+
"CircularReferenceError",
|
|
49
|
+
"DepthLimitError",
|
|
50
|
+
"ConstraintError",
|
|
51
|
+
# Generation
|
|
52
|
+
"generate_bundle_id",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# EOF
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-20
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_const.py
|
|
4
|
+
|
|
5
|
+
"""FTS Bundle constants."""
|
|
6
|
+
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
|
|
9
|
+
# Bundle extension (ZIP for portability, or directory)
|
|
10
|
+
ZIP_EXTENSION: str = ".zip"
|
|
11
|
+
|
|
12
|
+
# Supported formats: .zip files or directories (no extension)
|
|
13
|
+
EXTENSIONS: Tuple[str, ...] = (".zip",)
|
|
14
|
+
|
|
15
|
+
# Schema constants
|
|
16
|
+
SCHEMA_NAME = "scitex.fts"
|
|
17
|
+
SCHEMA_VERSION = "1.0.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ZIP_EXTENSION",
|
|
21
|
+
"EXTENSIONS",
|
|
22
|
+
"SCHEMA_NAME",
|
|
23
|
+
"SCHEMA_VERSION",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# EOF
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-20
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_errors.py
|
|
4
|
+
|
|
5
|
+
"""FTS Bundle error classes."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BundleError(Exception):
|
|
9
|
+
"""Base exception for bundle operations."""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BundleValidationError(BundleError, ValueError):
|
|
15
|
+
"""Error raised when bundle validation fails."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BundleNotFoundError(BundleError, FileNotFoundError):
|
|
21
|
+
"""Error raised when a bundle is not found."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NestedBundleNotFoundError(BundleNotFoundError):
|
|
27
|
+
"""Error raised when a nested bundle or file within it is not found."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CircularReferenceError(BundleValidationError):
|
|
33
|
+
"""Error raised when circular reference is detected in bundle hierarchy.
|
|
34
|
+
|
|
35
|
+
This occurs when a bundle references itself directly or indirectly
|
|
36
|
+
through its children, detected via bundle_id tracking.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DepthLimitError(BundleValidationError):
|
|
43
|
+
"""Error raised when bundle nesting exceeds max_depth constraint.
|
|
44
|
+
|
|
45
|
+
The max_depth is defined per bundle type in TYPE_DEFAULTS.
|
|
46
|
+
Default is 3 for figures, 1 for leaf types.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ConstraintError(BundleValidationError):
|
|
53
|
+
"""Error raised when bundle violates its type constraints.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
- Leaf type (plot, stats) has children
|
|
57
|
+
- Unknown type specified
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"BundleError",
|
|
65
|
+
"BundleValidationError",
|
|
66
|
+
"BundleNotFoundError",
|
|
67
|
+
"NestedBundleNotFoundError",
|
|
68
|
+
"CircularReferenceError",
|
|
69
|
+
"DepthLimitError",
|
|
70
|
+
"ConstraintError",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# EOF
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: 2025-12-20
|
|
3
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/fsb/_bundle/_utils/_generate.py
|
|
4
|
+
|
|
5
|
+
"""FTS Bundle ID generation utilities."""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_bundle_id() -> str:
|
|
11
|
+
"""Generate a unique bundle ID (UUID4).
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
A new UUID4 string.
|
|
15
|
+
"""
|
|
16
|
+
return str(uuid.uuid4())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["generate_bundle_id"]
|
|
20
|
+
|
|
21
|
+
# EOF
|