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