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.
Files changed (154) hide show
  1. {scanpath_studio-0.35.0/scanpath_studio.egg-info → scanpath_studio-0.37.0}/PKG-INFO +4 -2
  2. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/README.md +1 -1
  3. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/pyproject.toml +9 -2
  4. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/__init__.py +14 -1
  5. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/api.py +40 -0
  6. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/app.py +195 -9
  7. scanpath_studio-0.37.0/scanpath_studio/build_info.py +300 -0
  8. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/cli.py +61 -1
  9. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/constants.py +19 -3
  10. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/crash_report.py +4 -1
  11. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/data.py +35 -3
  12. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/datasets.py +19 -2
  13. scanpath_studio-0.37.0/scanpath_studio/desktop_update.py +1379 -0
  14. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/export.py +20 -3
  15. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/metadata.py +31 -5
  16. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/session_keys.py +5 -0
  17. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/styles.py +73 -16
  18. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/tabs.py +211 -293
  19. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/tour.py +2 -2
  20. scanpath_studio-0.37.0/scanpath_studio/updates.py +313 -0
  21. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/url_state.py +6 -1
  22. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/utils.py +64 -11
  23. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0/scanpath_studio.egg-info}/PKG-INFO +4 -2
  24. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/SOURCES.txt +6 -0
  25. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/requires.txt +2 -0
  26. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/LICENSE +0 -0
  27. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/MANIFEST.in +0 -0
  28. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/NOTICE +0 -0
  29. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/__main__.py +0 -0
  30. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/aggregation.py +0 -0
  31. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/alignment.py +0 -0
  32. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/analysis_recipe.py +0 -0
  33. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/animation_export.py +0 -0
  34. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/annotations.py +0 -0
  35. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
  36. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/assets/scanpath_studio_title_logo_dark.png +0 -0
  37. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/authoring.py +0 -0
  38. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/authoring_component.py +0 -0
  39. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/code_snippet.py +0 -0
  40. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/column_names.py +0 -0
  41. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/compare_source.py +0 -0
  42. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/computations.py +0 -0
  43. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/controls.py +0 -0
  44. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/data_health.py +0 -0
  45. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/dataset_table.py +0 -0
  46. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/debug_log.py +0 -0
  47. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/easter_egg.py +0 -0
  48. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/experimental_setup.py +0 -0
  49. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/export_status.py +0 -0
  50. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/eyegenbench.py +0 -0
  51. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/eyegenbench_geometry.py +0 -0
  52. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/fields.py +0 -0
  53. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/html_embed.py +0 -0
  54. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/illustration.py +0 -0
  55. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/loading.py +0 -0
  56. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/measures.py +0 -0
  57. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/menu.py +0 -0
  58. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/model_scanpaths.py +0 -0
  59. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/multipart.py +0 -0
  60. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/onestop_shard.py +0 -0
  61. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/persistence.py +0 -0
  62. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/plots.py +0 -0
  63. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/preprocessing.py +0 -0
  64. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/progress.py +0 -0
  65. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/fixations.csv +0 -0
  66. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
  67. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/ia.csv +0 -0
  68. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/ia.parquet +0 -0
  69. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
  70. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
  71. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
  72. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
  73. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
  74. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
  75. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
  76. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
  77. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
  78. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
  79. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
  80. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
  81. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
  82. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
  83. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
  84. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
  85. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
  86. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
  87. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
  88. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
  89. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
  90. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
  91. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
  92. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
  93. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
  94. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
  95. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
  96. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
  97. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
  98. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
  99. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
  100. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
  101. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
  102. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
  103. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
  104. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
  105. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
  106. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
  107. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
  108. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
  109. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
  110. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
  111. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
  112. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
  113. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
  114. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
  115. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
  116. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
  117. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
  118. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
  119. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
  120. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
  121. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
  122. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
  123. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
  124. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
  125. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
  126. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
  127. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
  128. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
  129. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
  130. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
  131. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
  132. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
  133. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
  134. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
  135. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
  136. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
  137. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
  138. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
  139. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
  140. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
  141. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
  142. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
  143. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
  144. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/similarity.py +0 -0
  145. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/streamlit_entry.py +0 -0
  146. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/synthetic.py +0 -0
  147. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/truncation_tooltip.py +0 -0
  148. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/update_sample_data.py +0 -0
  149. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/wizard.py +0 -0
  150. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio/wizard_shell.py +0 -0
  151. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
  152. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
  153. {scanpath_studio-0.35.0 → scanpath_studio-0.37.0}/scanpath_studio.egg-info/top_level.txt +0 -0
  154. {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.35.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.zip) ·
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.zip) ·
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 version: scanpath_studio/__version__
107
- version = { attr = "scanpath_studio.__version__" }
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
- __version__ = "0.35.0"
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 dataset_table, loading, progress, wizard_shell
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 = {{{__version__}}},\n"
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
- for grain in _metadata_grains_changed(snapshot):
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
  ):