scanpath-studio 0.35.0__tar.gz → 0.37.0__tar.gz
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.
- {scanpath_studio-0.35.0/scanpath_studio.egg-info → scanpath_studio-0.37.0}/PKG-INFO +4 -2
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/README.md +1 -1
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/pyproject.toml +9 -2
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/__init__.py +14 -1
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/api.py +40 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/app.py +195 -9
- scanpath_studio-0.37.0/scanpath_studio/build_info.py +300 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/cli.py +61 -1
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/constants.py +19 -3
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/crash_report.py +4 -1
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/data.py +35 -3
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/datasets.py +19 -2
- scanpath_studio-0.37.0/scanpath_studio/desktop_update.py +1379 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/export.py +20 -3
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/metadata.py +31 -5
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/session_keys.py +5 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/styles.py +73 -16
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/tabs.py +211 -293
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/tour.py +2 -2
- scanpath_studio-0.37.0/scanpath_studio/updates.py +313 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/url_state.py +6 -1
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/utils.py +64 -11
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0/scanpath_studio.egg-info}/PKG-INFO +4 -2
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/SOURCES.txt +6 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/requires.txt +2 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/LICENSE +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/MANIFEST.in +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/NOTICE +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/__main__.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/aggregation.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/alignment.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/analysis_recipe.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/animation_export.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/annotations.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/assets/scanpath_studio_title_logo_dark.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/authoring.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/authoring_component.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/code_snippet.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/column_names.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/compare_source.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/computations.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/controls.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/data_health.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/dataset_table.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/debug_log.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/easter_egg.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/experimental_setup.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/export_status.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/eyegenbench.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/eyegenbench_geometry.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/fields.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/html_embed.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/illustration.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/loading.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/measures.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/menu.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/model_scanpaths.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/multipart.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/onestop_shard.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/persistence.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/plots.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/preprocessing.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/progress.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/fixations.csv +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/ia.csv +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/ia.parquet +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/similarity.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/streamlit_entry.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/synthetic.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/truncation_tooltip.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/update_sample_data.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/wizard.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/wizard_shell.py +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/top_level.txt +0 -0
- {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scanpath-studio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.37.0
|
|
4
4
|
Summary: Interactive workbench for eye-tracking-while-reading data: draw, replay and compare scanpaths over the text, and export figures and tables.
|
|
5
5
|
Author: Keren Gruteke Klein, Maya Grossman, Ella Lion, Deborah N. Jakobi, David R. Reich, Lena Jäger, Yevgeni Berzak
|
|
6
6
|
Author-email: Omer Shubi <lacclab.technion@gmail.com>
|
|
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
License-File: NOTICE
|
|
26
26
|
Requires-Dist: streamlit>=1.65.0
|
|
27
|
+
Requires-Dist: packaging>=26.3
|
|
27
28
|
Requires-Dist: pandas>=3.0
|
|
28
29
|
Requires-Dist: plotly>=7.1
|
|
29
30
|
Requires-Dist: numpy>=2.4
|
|
@@ -35,6 +36,7 @@ Requires-Dist: streamlit-sortables>=0.3.1
|
|
|
35
36
|
Requires-Dist: pillow>=12.3
|
|
36
37
|
Requires-Dist: imageio[ffmpeg]>=2.37
|
|
37
38
|
Requires-Dist: openpyxl>=3.1.5
|
|
39
|
+
Requires-Dist: truststore>=0.10
|
|
38
40
|
Requires-Dist: xlrd>=2.0.2
|
|
39
41
|
Provides-Extra: test
|
|
40
42
|
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
@@ -74,7 +76,7 @@ for a paper.
|
|
|
74
76
|
## Get started
|
|
75
77
|
|
|
76
78
|
- **As a desktop app**, the easiest way to work with your own data:
|
|
77
|
-
[Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.
|
|
79
|
+
[Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64-setup.exe) ·
|
|
78
80
|
[macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
|
|
79
81
|
[Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
|
|
80
82
|
- **With pip** (Python 3.11–3.14):
|
|
@@ -20,7 +20,7 @@ for a paper.
|
|
|
20
20
|
## Get started
|
|
21
21
|
|
|
22
22
|
- **As a desktop app**, the easiest way to work with your own data:
|
|
23
|
-
[Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.
|
|
23
|
+
[Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64-setup.exe) ·
|
|
24
24
|
[macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
|
|
25
25
|
[Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
|
|
26
26
|
- **With pip** (Python 3.11–3.14):
|
|
@@ -35,6 +35,8 @@ classifiers = [
|
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
37
|
"streamlit>=1.65.0",
|
|
38
|
+
# Build versions sort and compare as PEP 440 (`build_info.py`, `updates.py`, #139).
|
|
39
|
+
"packaging>=26.3",
|
|
38
40
|
"pandas>=3.0",
|
|
39
41
|
"plotly>=7.1",
|
|
40
42
|
"numpy>=2.4",
|
|
@@ -59,6 +61,10 @@ dependencies = [
|
|
|
59
61
|
# workbook is a shipped enrichment, so without it an upload raises and the
|
|
60
62
|
# questions column silently never appears (#BUG-41).
|
|
61
63
|
"openpyxl>=3.1.5",
|
|
64
|
+
# Dataset downloads verify HTTPS against the OS certificate store, so a
|
|
65
|
+
# python.org Python without its CA bundle, or a TLS-inspecting network,
|
|
66
|
+
# can still fetch OneStop / PoTeC / MultiplEYE (`datasets._open_url`).
|
|
67
|
+
"truststore>=0.10",
|
|
62
68
|
# The legacy Excel 97–2003 (.xls) engine, for the same readers (#DATA-53).
|
|
63
69
|
"xlrd>=2.0.2",
|
|
64
70
|
]
|
|
@@ -103,8 +109,9 @@ Issues = "https://github.com/lacclab/scanpath-studio/issues"
|
|
|
103
109
|
scanpath-studio = "scanpath_studio.cli:main"
|
|
104
110
|
|
|
105
111
|
[tool.setuptools.dynamic]
|
|
106
|
-
# Single source of truth for the
|
|
107
|
-
|
|
112
|
+
# Single source of truth for the release number: scanpath_studio.__release__
|
|
113
|
+
# (`__version__` is the exact build, worked out from it at runtime — #139).
|
|
114
|
+
version = { attr = "scanpath_studio.__release__" }
|
|
108
115
|
|
|
109
116
|
[tool.setuptools]
|
|
110
117
|
include-package-data = true
|
|
@@ -9,6 +9,7 @@ __all__ = [
|
|
|
9
9
|
"build_authored_scanpath",
|
|
10
10
|
"cache_status",
|
|
11
11
|
"check_data_health",
|
|
12
|
+
"check_for_updates",
|
|
12
13
|
"clear_cache",
|
|
13
14
|
"compare_scanpaths",
|
|
14
15
|
"figure_code",
|
|
@@ -32,8 +33,14 @@ __all__ = [
|
|
|
32
33
|
"render_parent_trial",
|
|
33
34
|
"save_figure",
|
|
34
35
|
"save_figure_layers",
|
|
36
|
+
"version_info",
|
|
35
37
|
]
|
|
36
|
-
|
|
38
|
+
# The release this tree descends from: the number `/release` bumps (with
|
|
39
|
+
# CITATION.cff), `pyproject.toml` builds with, and publish.yml checks a tag
|
|
40
|
+
# against. `__version__` is the exact build, worked out from it lazily
|
|
41
|
+
# (build_info.py, #139) — "0.35.0" at the release itself,
|
|
42
|
+
# "0.35.0.post3+g8f18219" three commits after it.
|
|
43
|
+
__release__ = "0.37.0"
|
|
37
44
|
|
|
38
45
|
# Public headless API (see api.py / datasets.py / eyegenbench.py). Resolved lazily so
|
|
39
46
|
# `import scanpath_studio` stays cheap and doesn't pull in pandas/plotly/
|
|
@@ -65,6 +72,12 @@ _API_EXPORTS = (
|
|
|
65
72
|
|
|
66
73
|
|
|
67
74
|
def __getattr__(name: str):
|
|
75
|
+
if name == "__version__":
|
|
76
|
+
from .build_info import build_info
|
|
77
|
+
|
|
78
|
+
# Worked out once (it may run `git describe`), then a plain attribute.
|
|
79
|
+
globals()["__version__"] = build_info().version
|
|
80
|
+
return globals()["__version__"]
|
|
68
81
|
if name in _API_EXPORTS:
|
|
69
82
|
from . import api
|
|
70
83
|
|
|
@@ -53,6 +53,7 @@ for _name in (
|
|
|
53
53
|
from . import column_names as _cn # noqa: E402
|
|
54
54
|
from . import data as _data # noqa: E402
|
|
55
55
|
from . import export as _export # noqa: E402
|
|
56
|
+
from .build_info import BuildInfo # noqa: E402
|
|
56
57
|
from .column_names import ColumnNames # noqa: E402
|
|
57
58
|
from .constants import ( # noqa: E402
|
|
58
59
|
DEFAULT_BACKGROUND_COLOR,
|
|
@@ -95,6 +96,7 @@ from .plots import ( # noqa: E402
|
|
|
95
96
|
replay_page,
|
|
96
97
|
split_scanpath_layers,
|
|
97
98
|
)
|
|
99
|
+
from .updates import UpdateCheck # noqa: E402
|
|
98
100
|
|
|
99
101
|
|
|
100
102
|
def build_authored_scanpath(
|
|
@@ -3802,3 +3804,41 @@ def clear_cache() -> dict:
|
|
|
3802
3804
|
|
|
3803
3805
|
clear_local_state()
|
|
3804
3806
|
return cache_status()
|
|
3807
|
+
|
|
3808
|
+
|
|
3809
|
+
def version_info() -> BuildInfo:
|
|
3810
|
+
"""Which build of Scanpath Studio this is — no network access.
|
|
3811
|
+
|
|
3812
|
+
``version`` is what ``scanpath_studio.__version__`` holds: the release itself
|
|
3813
|
+
(``"0.35.0"``), or between releases a PEP 440 version that sorts after it —
|
|
3814
|
+
``"0.35.0.post3+g8f18219"`` is three commits after v0.35.0, at commit
|
|
3815
|
+
``8f18219``, and it ends ``.dirty`` with uncommitted changes. ``release`` is
|
|
3816
|
+
the release it descends from (``scanpath_studio.__release__``), ``distance``
|
|
3817
|
+
the commits since (``None`` when unknown), ``commit``, ``dirty``, and
|
|
3818
|
+
``source`` — how it was worked out: ``"checkout"`` (``git describe``),
|
|
3819
|
+
``"stamp"`` (a desktop bundle's build stamp), ``"vcs"`` (a
|
|
3820
|
+
``pip install git+…``) or ``"release"``. ``describe()`` says it in a
|
|
3821
|
+
sentence. The same is in Help → About and ``scanpath-studio version``."""
|
|
3822
|
+
from .build_info import build_info
|
|
3823
|
+
|
|
3824
|
+
return build_info()
|
|
3825
|
+
|
|
3826
|
+
|
|
3827
|
+
def check_for_updates(timeout: float = 5.0) -> UpdateCheck:
|
|
3828
|
+
"""Ask GitHub whether a newer release than this build is out.
|
|
3829
|
+
|
|
3830
|
+
The one call here that uses the network, and only when made: it reads the
|
|
3831
|
+
latest release from ``api.github.com`` (drafts and pre-releases excluded)
|
|
3832
|
+
and compares it with [`version_info`][scanpath_studio.api.version_info]. It
|
|
3833
|
+
never raises. ``status`` is ``"up_to_date"``, ``"update_available"``,
|
|
3834
|
+
``"ahead"`` (a development build past the latest release) or ``"error"``
|
|
3835
|
+
(offline, no answer within ``timeout`` seconds, rate-limited, …), and
|
|
3836
|
+
``message`` says it in a sentence. With an update available, ``command`` is
|
|
3837
|
+
the shell command that updates this install (``pip install -U
|
|
3838
|
+
scanpath-studio``, ``uv tool upgrade scanpath-studio``, ``git pull``, …),
|
|
3839
|
+
``latest.url`` the release notes, and in the desktop app ``download`` the
|
|
3840
|
+
archive for this computer. The same check is Help → About → *Check for
|
|
3841
|
+
updates* and ``scanpath-studio version --check``."""
|
|
3842
|
+
from .updates import check_for_updates as _check
|
|
3843
|
+
|
|
3844
|
+
return _check(timeout)
|
|
@@ -63,8 +63,15 @@ if __package__ is None or __package__ == "":
|
|
|
63
63
|
if str(root) not in sys.path:
|
|
64
64
|
sys.path.insert(0, str(root))
|
|
65
65
|
|
|
66
|
-
from scanpath_studio import
|
|
66
|
+
from scanpath_studio import (
|
|
67
|
+
dataset_table,
|
|
68
|
+
desktop_update,
|
|
69
|
+
loading,
|
|
70
|
+
progress,
|
|
71
|
+
wizard_shell,
|
|
72
|
+
)
|
|
67
73
|
from scanpath_studio import metadata as metadata_mod
|
|
74
|
+
from scanpath_studio import updates as update_check
|
|
68
75
|
from scanpath_studio.annotations import (
|
|
69
76
|
filter_keys,
|
|
70
77
|
)
|
|
@@ -278,7 +285,7 @@ from scanpath_studio.session_keys import (
|
|
|
278
285
|
PARAM_CORPUS,
|
|
279
286
|
PARAM_DATASET,
|
|
280
287
|
)
|
|
281
|
-
from scanpath_studio.styles import get_app_css
|
|
288
|
+
from scanpath_studio.styles import get_app_css, widen_menu
|
|
282
289
|
from scanpath_studio.tabs import (
|
|
283
290
|
_EDITOR_KEY_NOISE,
|
|
284
291
|
_REMAP_DIRTY_KEY,
|
|
@@ -1337,6 +1344,12 @@ def _arm_about() -> None:
|
|
|
1337
1344
|
st.session_state["_about_dialog_requested"] = True
|
|
1338
1345
|
|
|
1339
1346
|
|
|
1347
|
+
#: #139 — the last *Check for updates* answer, shown under the button until the
|
|
1348
|
+
#: dialog is opened again. A plain session key, not in the recovery cache's
|
|
1349
|
+
#: allowlist, so it is never written to disk.
|
|
1350
|
+
_UPDATE_CHECK_KEY = "_about_update_check"
|
|
1351
|
+
|
|
1352
|
+
|
|
1340
1353
|
def maybe_show_about() -> None:
|
|
1341
1354
|
"""Open the About dialog if the ❓ Help menu button armed it.
|
|
1342
1355
|
|
|
@@ -1346,6 +1359,7 @@ def maybe_show_about() -> None:
|
|
|
1346
1359
|
rerun (including the ~10 s plot embeds).
|
|
1347
1360
|
"""
|
|
1348
1361
|
if st.session_state.pop("_about_dialog_requested", False):
|
|
1362
|
+
st.session_state.pop(_UPDATE_CHECK_KEY, None)
|
|
1349
1363
|
_about_dialog()
|
|
1350
1364
|
|
|
1351
1365
|
|
|
@@ -1353,7 +1367,7 @@ def maybe_show_about() -> None:
|
|
|
1353
1367
|
@guarded()
|
|
1354
1368
|
def _about_dialog() -> None:
|
|
1355
1369
|
"""The About modal: version, authors, links, citation, AI-assistance note."""
|
|
1356
|
-
from scanpath_studio import __version__
|
|
1370
|
+
from scanpath_studio import __release__, __version__
|
|
1357
1371
|
|
|
1358
1372
|
# The button that opened this sits inside the ❓ Help popover, whose open
|
|
1359
1373
|
# state is client-side — without this it floats on top of the modal.
|
|
@@ -1370,15 +1384,17 @@ def _about_dialog() -> None:
|
|
|
1370
1384
|
"month = jun,\n"
|
|
1371
1385
|
"title = {{Scanpath Studio}},\n"
|
|
1372
1386
|
f"url = {{{CITATION['url']}}},\n"
|
|
1373
|
-
f"version = {{{
|
|
1387
|
+
f"version = {{{__release__}}},\n"
|
|
1374
1388
|
"year = {2026}\n"
|
|
1375
1389
|
"}"
|
|
1376
1390
|
)
|
|
1391
|
+
st.markdown(
|
|
1392
|
+
f"**Scanpath Studio** v{__version__} — interactive visualization of eye "
|
|
1393
|
+
"movements in reading."
|
|
1394
|
+
)
|
|
1395
|
+
_render_build_and_updates()
|
|
1377
1396
|
st.markdown(
|
|
1378
1397
|
f"""
|
|
1379
|
-
**Scanpath Studio** v{__version__} — interactive visualization of eye
|
|
1380
|
-
movements in reading.
|
|
1381
|
-
|
|
1382
1398
|
Developed by [Omer Shubi](https://omershubi.github.io/),
|
|
1383
1399
|
[Keren Gruteke Klein](https://kerengruteke.github.io/),
|
|
1384
1400
|
[Maya Grossman](https://www.linkedin.com/in/maya-harram-32b547292/),
|
|
@@ -1444,6 +1460,171 @@ and feature requests to [an issue]({CITATION["url"]}/issues) ↗.
|
|
|
1444
1460
|
st.rerun()
|
|
1445
1461
|
|
|
1446
1462
|
|
|
1463
|
+
def _update_check_offered() -> bool:
|
|
1464
|
+
"""#139: *Check for updates* only where updating means something — a local
|
|
1465
|
+
run or the desktop app, i.e. a server on loopback alone. The hosted demo
|
|
1466
|
+
runs the `stable` branch, and its visitors have nothing to update."""
|
|
1467
|
+
return server_bound_to_loopback()
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
@st.cache_data(ttl=600, show_spinner=False)
|
|
1471
|
+
def _latest_release_cached() -> update_check.Release:
|
|
1472
|
+
"""GitHub's latest release, kept ten minutes: a second click, or a second
|
|
1473
|
+
session on this machine, doesn't spend another of the 60 anonymous requests
|
|
1474
|
+
an hour. A failure raises, and `st.cache_data` keeps no failed result."""
|
|
1475
|
+
return update_check.latest_release()
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def _build_info():
|
|
1479
|
+
"""This process's build (a seam the About tests pin)."""
|
|
1480
|
+
from scanpath_studio.build_info import build_info
|
|
1481
|
+
|
|
1482
|
+
return build_info()
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def _render_last_update(last: desktop_update.UpdateResult, current: str) -> None:
|
|
1486
|
+
"""#385: the outcome the update helper left behind, across the restart.
|
|
1487
|
+
|
|
1488
|
+
Said only of the build that is running: a record that names neither
|
|
1489
|
+
``current`` version — a later install by hand — says nothing.
|
|
1490
|
+
"""
|
|
1491
|
+
because = f": {last.reason}" if last.reason else ""
|
|
1492
|
+
if last.status == "updated":
|
|
1493
|
+
if last.version == current:
|
|
1494
|
+
st.caption(f"Updated from v{last.previous}.")
|
|
1495
|
+
elif last.previous == current:
|
|
1496
|
+
# This build is the one that stayed.
|
|
1497
|
+
st.warning(
|
|
1498
|
+
f"The update to v{last.version} didn't go through{because}. "
|
|
1499
|
+
f"This is still v{last.previous}.",
|
|
1500
|
+
icon=ICONS["warning"],
|
|
1501
|
+
)
|
|
1502
|
+
elif last.version == current:
|
|
1503
|
+
# A double failure kept the new version's files in place.
|
|
1504
|
+
st.warning(
|
|
1505
|
+
f"The update to v{last.version} didn't finish cleanly{because}. If "
|
|
1506
|
+
f"something misbehaves, download v{last.version} again from the "
|
|
1507
|
+
"release page.",
|
|
1508
|
+
icon=ICONS["warning"],
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def _render_build_and_updates() -> None:
|
|
1513
|
+
"""#139: which build this is, and — on a local run — *Check for updates*."""
|
|
1514
|
+
info = _build_info()
|
|
1515
|
+
if info.version != info.release:
|
|
1516
|
+
st.caption(info.describe())
|
|
1517
|
+
# #385: how the desktop app's last update ended — None anywhere else.
|
|
1518
|
+
last = desktop_update.last_result()
|
|
1519
|
+
if last is not None:
|
|
1520
|
+
_render_last_update(last, info.version)
|
|
1521
|
+
if not _update_check_offered():
|
|
1522
|
+
return
|
|
1523
|
+
if st.button(
|
|
1524
|
+
"Check for updates",
|
|
1525
|
+
icon=ICONS["update"],
|
|
1526
|
+
key="about_check_updates",
|
|
1527
|
+
help="Asks GitHub for the latest release — the only time the app goes "
|
|
1528
|
+
"online for this.",
|
|
1529
|
+
):
|
|
1530
|
+
# One opaque request of at most 5 s inside a dialog: a spinner, not a
|
|
1531
|
+
# UX-165 loading card.
|
|
1532
|
+
with st.spinner("Asking GitHub…"):
|
|
1533
|
+
st.session_state[_UPDATE_CHECK_KEY] = update_check.check_for_updates(
|
|
1534
|
+
latest=_latest_release_cached
|
|
1535
|
+
)
|
|
1536
|
+
result = st.session_state.get(_UPDATE_CHECK_KEY)
|
|
1537
|
+
if result is not None:
|
|
1538
|
+
_render_update_result(result)
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
def _render_update_result(result: update_check.UpdateCheck) -> None:
|
|
1542
|
+
"""One *Check for updates* answer: the sentence, then what to do about it."""
|
|
1543
|
+
if result.status == "up_to_date":
|
|
1544
|
+
st.success(result.message, icon=ICONS["success"])
|
|
1545
|
+
return
|
|
1546
|
+
if result.status == "ahead":
|
|
1547
|
+
st.info(result.message, icon=ICONS["info"])
|
|
1548
|
+
return
|
|
1549
|
+
if result.status == "error":
|
|
1550
|
+
st.warning(result.message, icon=ICONS["warning"])
|
|
1551
|
+
return
|
|
1552
|
+
st.info(result.message, icon=ICONS["update"])
|
|
1553
|
+
if result.install_kind == "desktop":
|
|
1554
|
+
_render_desktop_update(result)
|
|
1555
|
+
elif result.command:
|
|
1556
|
+
st.code(result.command, language="bash")
|
|
1557
|
+
st.caption("Then restart the app.")
|
|
1558
|
+
if result.latest is not None:
|
|
1559
|
+
st.markdown(f"[What's new in v{result.latest.version}]({result.latest.url}) ↗")
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
def _render_desktop_update(result: update_check.UpdateCheck) -> None:
|
|
1563
|
+
"""#385: **Update & restart** when this app can update itself, and the
|
|
1564
|
+
download either way — beside the button, or instead of it with the reason."""
|
|
1565
|
+
install = desktop_update.current_install()
|
|
1566
|
+
reason = desktop_update.refusal(result, install)
|
|
1567
|
+
clicked = False
|
|
1568
|
+
if reason is None:
|
|
1569
|
+
clicked = st.button(
|
|
1570
|
+
"Update & restart",
|
|
1571
|
+
type="primary",
|
|
1572
|
+
icon=ICONS["update"],
|
|
1573
|
+
key="about_update_restart",
|
|
1574
|
+
help="Downloads the new version, checks and tests it, then restarts "
|
|
1575
|
+
"into it. Your datasets and settings come back with it.",
|
|
1576
|
+
)
|
|
1577
|
+
else:
|
|
1578
|
+
st.caption(reason)
|
|
1579
|
+
if result.download is not None:
|
|
1580
|
+
label = (
|
|
1581
|
+
"Download instead"
|
|
1582
|
+
if reason is None
|
|
1583
|
+
else f"Download {result.download.name} ({human_size(result.download.size)})"
|
|
1584
|
+
)
|
|
1585
|
+
st.link_button(label, result.download.url, icon=ICONS["download"])
|
|
1586
|
+
if clicked:
|
|
1587
|
+
_run_desktop_update(result, install)
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def _stop_desktop_update(task_key: tuple) -> None:
|
|
1591
|
+
"""#385: Cancel on the update card — the download stops at its next chunk."""
|
|
1592
|
+
progress.cancel(task_key)
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _run_desktop_update(
|
|
1596
|
+
result: update_check.UpdateCheck, install: desktop_update.Install
|
|
1597
|
+
) -> None:
|
|
1598
|
+
"""Download, check and test under a card, then hand over to the helper and quit."""
|
|
1599
|
+
task_key = ("desktop_update", loading.session_id())
|
|
1600
|
+
try:
|
|
1601
|
+
with loading.card(
|
|
1602
|
+
st.empty(),
|
|
1603
|
+
key="desktop_update",
|
|
1604
|
+
title=f"Updating to v{result.latest.version}",
|
|
1605
|
+
steps=desktop_update.STEPS,
|
|
1606
|
+
step_list=True,
|
|
1607
|
+
task_key=task_key,
|
|
1608
|
+
cancel=loading.Cancel(
|
|
1609
|
+
"Cancel update", _stop_desktop_update, args=(task_key,)
|
|
1610
|
+
),
|
|
1611
|
+
):
|
|
1612
|
+
plan = desktop_update.prepare(result, install)
|
|
1613
|
+
desktop_update.start_swap(plan)
|
|
1614
|
+
except desktop_update.UpdateFailed as error:
|
|
1615
|
+
message = str(error)
|
|
1616
|
+
if "nothing was changed" not in message.lower():
|
|
1617
|
+
message += " Nothing was changed."
|
|
1618
|
+
st.error(message, icon=ICONS["error"])
|
|
1619
|
+
return
|
|
1620
|
+
st.success(
|
|
1621
|
+
f"Restarting into v{plan.version}. A new window opens when it's ready "
|
|
1622
|
+
"(on Windows that can take a minute or two); you can close this one.",
|
|
1623
|
+
icon=ICONS["update"],
|
|
1624
|
+
)
|
|
1625
|
+
desktop_update.exit_soon()
|
|
1626
|
+
|
|
1627
|
+
|
|
1447
1628
|
# --- Public-dataset access UI (directory + expected files + download) --------
|
|
1448
1629
|
# Shared by the per-corpus loaders below. Each corpus shows the on-disk layout
|
|
1449
1630
|
# it expects (so a user who already downloaded the data knows what to drop
|
|
@@ -5240,6 +5421,8 @@ def render_data_source_picker(host=None) -> None:
|
|
|
5240
5421
|
key="data_source_picker",
|
|
5241
5422
|
on_change=_on_data_source_pick,
|
|
5242
5423
|
)
|
|
5424
|
+
# The menu opens as wide as the longest dataset name.
|
|
5425
|
+
widen_menu("data_source_picker", [_entry_label(entry) for entry in entries])
|
|
5243
5426
|
# The help icon sits in the label row, right-aligned over +, not beside the
|
|
5244
5427
|
# label: the bottom-aligned row keeps + level with the picker, so the icon
|
|
5245
5428
|
# lands on the label's line.
|
|
@@ -6214,11 +6397,14 @@ def apply_editor_restore() -> None:
|
|
|
6214
6397
|
return
|
|
6215
6398
|
if snapshot.get("owner") != st.session_state.get(metadata_mod.OWNER_KEY):
|
|
6216
6399
|
return
|
|
6217
|
-
|
|
6400
|
+
changed = _metadata_grains_changed(snapshot)
|
|
6401
|
+
if changed:
|
|
6402
|
+
# The uploader still holds the cancelled edit's file in the browser.
|
|
6403
|
+
metadata_mod.reset_uploads(st.session_state)
|
|
6404
|
+
for grain in changed:
|
|
6218
6405
|
before = snapshot["grains"][grain]
|
|
6219
6406
|
key, raw, file = metadata_mod.grain_keys(grain)
|
|
6220
6407
|
for name in (
|
|
6221
|
-
f"{grain}_metadata_upload",
|
|
6222
6408
|
f"{grain}_metadata_id_column",
|
|
6223
6409
|
f"{grain}_metadata_keep_fields",
|
|
6224
6410
|
):
|