napari-track-edit 1.0.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 (209) hide show
  1. napari_track_edit-1.0.0/.github/CODEOWNERS +9 -0
  2. napari_track_edit-1.0.0/.github/dependabot.yml +10 -0
  3. napari_track_edit-1.0.0/.github/workflows/benchmark-report.yml +63 -0
  4. napari_track_edit-1.0.0/.github/workflows/benchmark_pr.py +142 -0
  5. napari_track_edit-1.0.0/.github/workflows/benchmarks.yml +141 -0
  6. napari_track_edit-1.0.0/.github/workflows/deploy.yml +35 -0
  7. napari_track_edit-1.0.0/.github/workflows/docs.yml +47 -0
  8. napari_track_edit-1.0.0/.github/workflows/download-sources.yml +42 -0
  9. napari_track_edit-1.0.0/.github/workflows/make_bundle_app.yml +17 -0
  10. napari_track_edit-1.0.0/.github/workflows/test.yml +112 -0
  11. napari_track_edit-1.0.0/.gitignore +99 -0
  12. napari_track_edit-1.0.0/.pre-commit-config.yaml +23 -0
  13. napari_track_edit-1.0.0/CITATION.cff +37 -0
  14. napari_track_edit-1.0.0/DEVELOPER.md +20 -0
  15. napari_track_edit-1.0.0/LICENSE +28 -0
  16. napari_track_edit-1.0.0/PKG-INFO +142 -0
  17. napari_track_edit-1.0.0/README.md +83 -0
  18. napari_track_edit-1.0.0/docs/Makefile +20 -0
  19. napari_track_edit-1.0.0/docs/source/_key_bindings.md +57 -0
  20. napari_track_edit-1.0.0/docs/source/_static/custom.css +95 -0
  21. napari_track_edit-1.0.0/docs/source/conf.py +74 -0
  22. napari_track_edit-1.0.0/docs/source/editing.rst +213 -0
  23. napari_track_edit-1.0.0/docs/source/features.rst +54 -0
  24. napari_track_edit-1.0.0/docs/source/getting_started.rst +125 -0
  25. napari_track_edit-1.0.0/docs/source/groups.rst +53 -0
  26. napari_track_edit-1.0.0/docs/source/images/adding_updating_seg_node.gif +0 -0
  27. napari_track_edit-1.0.0/docs/source/images/all_objects.png +0 -0
  28. napari_track_edit-1.0.0/docs/source/images/conflicting_edge.jpg +0 -0
  29. napari_track_edit-1.0.0/docs/source/images/connect_break.gif +0 -0
  30. napari_track_edit-1.0.0/docs/source/images/editing_edges.png +0 -0
  31. napari_track_edit-1.0.0/docs/source/images/editing_nodes.png +0 -0
  32. napari_track_edit-1.0.0/docs/source/images/groups.png +0 -0
  33. napari_track_edit-1.0.0/docs/source/images/lineage_view.png +0 -0
  34. napari_track_edit-1.0.0/docs/source/images/lineages.png +0 -0
  35. napari_track_edit-1.0.0/docs/source/images/logo_transparent.png +0 -0
  36. napari_track_edit-1.0.0/docs/source/images/main_widget_startup.png +0 -0
  37. napari_track_edit-1.0.0/docs/source/images/manual_editing.png +0 -0
  38. napari_track_edit-1.0.0/docs/source/images/sample1.jpg +0 -0
  39. napari_track_edit-1.0.0/docs/source/images/sample2.jpg +0 -0
  40. napari_track_edit-1.0.0/docs/source/images/save_and_export.png +0 -0
  41. napari_track_edit-1.0.0/docs/source/images/size_feature.jpg +0 -0
  42. napari_track_edit-1.0.0/docs/source/images/swap_edges.png +0 -0
  43. napari_track_edit-1.0.0/docs/source/images/tracking_parameters.png +0 -0
  44. napari_track_edit-1.0.0/docs/source/images/tracking_result_2D.jpg +0 -0
  45. napari_track_edit-1.0.0/docs/source/images/tracking_result_3D.jpg +0 -0
  46. napari_track_edit-1.0.0/docs/source/images/tracks_list.png +0 -0
  47. napari_track_edit-1.0.0/docs/source/index.rst +52 -0
  48. napari_track_edit-1.0.0/docs/source/key_bindings.rst +7 -0
  49. napari_track_edit-1.0.0/docs/source/napari-track-edit_tutorial.css +49 -0
  50. napari_track_edit-1.0.0/docs/source/napari-track-edit_tutorial.md +256 -0
  51. napari_track_edit-1.0.0/docs/source/saving_loading.rst +212 -0
  52. napari_track_edit-1.0.0/docs/source/tracking.rst +180 -0
  53. napari_track_edit-1.0.0/docs/source/viewing.rst +188 -0
  54. napari_track_edit-1.0.0/justfile +11 -0
  55. napari_track_edit-1.0.0/meetings/2026-04-29/generate_timeline.py +182 -0
  56. napari_track_edit-1.0.0/meetings/2026-04-29/meeting-notes.md +46 -0
  57. napari_track_edit-1.0.0/meetings/2026-04-29/timeline.png +0 -0
  58. napari_track_edit-1.0.0/meetings/2026-04-29/timeline.svg +2922 -0
  59. napari_track_edit-1.0.0/meetings/2026-04-29/timeline.yaml +86 -0
  60. napari_track_edit-1.0.0/meetings/2026-04-29/tracksdata_merged.png +0 -0
  61. napari_track_edit-1.0.0/pyproject.toml +190 -0
  62. napari_track_edit-1.0.0/scripts/hela_example_tracks.csv +8602 -0
  63. napari_track_edit-1.0.0/scripts/load_external_points.py +38 -0
  64. napari_track_edit-1.0.0/scripts/run_hela.py +36 -0
  65. napari_track_edit-1.0.0/scripts/view_external_tracks.py +48 -0
  66. napari_track_edit-1.0.0/setup.cfg +4 -0
  67. napari_track_edit-1.0.0/src/napari_track_edit/__init__.py +12 -0
  68. napari_track_edit-1.0.0/src/napari_track_edit/__main__.py +66 -0
  69. napari_track_edit-1.0.0/src/napari_track_edit/_version.py +24 -0
  70. napari_track_edit-1.0.0/src/napari_track_edit/_wgpu_setup.py +32 -0
  71. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/__init__.py +1 -0
  72. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/editing_selection_menu.py +312 -0
  73. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/feature_widget.py +407 -0
  74. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/group_widget.py +23 -0
  75. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/layer_dropdown.py +239 -0
  76. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/main_app.py +140 -0
  77. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/menu_manager.py +250 -0
  78. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/track_list_widget.py +31 -0
  79. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/tracking_from_scratch_widget.py +114 -0
  80. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/tracking_widget.py +25 -0
  81. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/visualization_widget.py +399 -0
  82. napari_track_edit-1.0.0/src/napari_track_edit/application_menus/welcome_widget.py +199 -0
  83. napari_track_edit-1.0.0/src/napari_track_edit/data_views/__init__.py +7 -0
  84. napari_track_edit-1.0.0/src/napari_track_edit/data_views/colormap.py +499 -0
  85. napari_track_edit-1.0.0/src/napari_track_edit/data_views/dims_utils.py +120 -0
  86. napari_track_edit-1.0.0/src/napari_track_edit/data_views/keybindings_config.py +197 -0
  87. napari_track_edit-1.0.0/src/napari_track_edit/data_views/lazy_array_wrapper.py +93 -0
  88. napari_track_edit-1.0.0/src/napari_track_edit/data_views/node_type.py +15 -0
  89. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/__init__.py +0 -0
  90. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/__init__.py +0 -0
  91. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/click_utils.py +57 -0
  92. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/contour_labels.py +378 -0
  93. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/out_of_slice_points.py +131 -0
  94. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/track_graph.py +207 -0
  95. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/track_labels.py +645 -0
  96. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/track_points.py +417 -0
  97. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/layers/tracks_layer_group.py +224 -0
  98. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/ortho_views.py +463 -0
  99. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/table/__init__.py +0 -0
  100. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/table/custom_table_widget.py +607 -0
  101. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/__init__.py +0 -0
  102. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/flip_axes_widget.py +31 -0
  103. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/navigation_widget.py +189 -0
  104. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/tree_plot_fpl.py +1267 -0
  105. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/tree_view_feature_widget.py +132 -0
  106. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/tree_view_mode_widget.py +60 -0
  107. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/tree_view_options_widget.py +54 -0
  108. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/tree_widget.py +523 -0
  109. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views/tree_view/tree_widget_utils.py +432 -0
  110. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views_coordinator/__init__.py +0 -0
  111. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views_coordinator/groups.py +510 -0
  112. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views_coordinator/node_selection_history.py +283 -0
  113. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views_coordinator/tracks_list.py +554 -0
  114. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views_coordinator/tracks_viewer.py +814 -0
  115. napari_track_edit-1.0.0/src/napari_track_edit/data_views/views_coordinator/user_dialogs.py +178 -0
  116. napari_track_edit-1.0.0/src/napari_track_edit/download_progress.py +70 -0
  117. napari_track_edit-1.0.0/src/napari_track_edit/example_data.py +493 -0
  118. napari_track_edit-1.0.0/src/napari_track_edit/import_export/__init__.py +1 -0
  119. napari_track_edit-1.0.0/src/napari_track_edit/import_export/geff_io.py +75 -0
  120. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/__init__.py +0 -0
  121. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/csv_dimension_widget.py +54 -0
  122. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/csv_import_widget.py +97 -0
  123. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/export_dialog.py +382 -0
  124. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/geff_import_utils.py +62 -0
  125. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/geff_import_widget.py +105 -0
  126. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/import_dialog.py +482 -0
  127. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/prop_map_widget.py +585 -0
  128. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/scale_widget.py +119 -0
  129. napari_track_edit-1.0.0/src/napari_track_edit/import_export/menus/segmentation_widgets.py +443 -0
  130. napari_track_edit-1.0.0/src/napari_track_edit/import_export/sql_io.py +402 -0
  131. napari_track_edit-1.0.0/src/napari_track_edit/launcher.py +8 -0
  132. napari_track_edit-1.0.0/src/napari_track_edit/motile/__init__.py +0 -0
  133. napari_track_edit-1.0.0/src/napari_track_edit/motile/backend/__init__.py +3 -0
  134. napari_track_edit-1.0.0/src/napari_track_edit/motile/backend/motile_run.py +416 -0
  135. napari_track_edit-1.0.0/src/napari_track_edit/motile/backend/solve.py +645 -0
  136. napari_track_edit-1.0.0/src/napari_track_edit/motile/backend/solver_params.py +90 -0
  137. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/__init__.py +1 -0
  138. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/motile_widget.py +226 -0
  139. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/param_values.py +76 -0
  140. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/params_editor.py +309 -0
  141. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/params_viewer.py +131 -0
  142. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/run_editor.py +252 -0
  143. napari_track_edit-1.0.0/src/napari_track_edit/motile/menus/run_viewer.py +159 -0
  144. napari_track_edit-1.0.0/src/napari_track_edit/napari.yaml +91 -0
  145. napari_track_edit-1.0.0/src/napari_track_edit/resources/logo.png +0 -0
  146. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/PKG-INFO +142 -0
  147. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/SOURCES.txt +207 -0
  148. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/dependency_links.txt +1 -0
  149. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/entry_points.txt +5 -0
  150. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/requires.txt +37 -0
  151. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/scm_file_list.json +202 -0
  152. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/scm_version.json +8 -0
  153. napari_track_edit-1.0.0/src/napari_track_edit.egg-info/top_level.txt +1 -0
  154. napari_track_edit-1.0.0/tests/application_menus/test_editing_menu.py +198 -0
  155. napari_track_edit-1.0.0/tests/application_menus/test_feature_widget.py +468 -0
  156. napari_track_edit-1.0.0/tests/application_menus/test_layer_dropdown.py +422 -0
  157. napari_track_edit-1.0.0/tests/application_menus/test_main_app.py +64 -0
  158. napari_track_edit-1.0.0/tests/application_menus/test_menu_manager.py +246 -0
  159. napari_track_edit-1.0.0/tests/application_menus/test_selection_widget.py +161 -0
  160. napari_track_edit-1.0.0/tests/application_menus/test_track_list_widget.py +15 -0
  161. napari_track_edit-1.0.0/tests/application_menus/test_tracking_from_scratch_widget.py +116 -0
  162. napari_track_edit-1.0.0/tests/application_menus/test_tracking_widget.py +81 -0
  163. napari_track_edit-1.0.0/tests/application_menus/test_welcome_widget.py +28 -0
  164. napari_track_edit-1.0.0/tests/benchmarks/bench_data_model.py +39 -0
  165. napari_track_edit-1.0.0/tests/benchmarks/bench_ui_actions.py +290 -0
  166. napari_track_edit-1.0.0/tests/benchmarks/conftest.py +62 -0
  167. napari_track_edit-1.0.0/tests/benchmarks/synthetic_data.py +307 -0
  168. napari_track_edit-1.0.0/tests/conftest.py +334 -0
  169. napari_track_edit-1.0.0/tests/data_views/conftest.py +28 -0
  170. napari_track_edit-1.0.0/tests/data_views/test_center_view.py +673 -0
  171. napari_track_edit-1.0.0/tests/data_views/test_color_feature.py +435 -0
  172. napari_track_edit-1.0.0/tests/data_views/test_colormap.py +866 -0
  173. napari_track_edit-1.0.0/tests/data_views/test_dims_utils.py +152 -0
  174. napari_track_edit-1.0.0/tests/data_views/test_force_operations.py +511 -0
  175. napari_track_edit-1.0.0/tests/data_views/test_keybindings.py +82 -0
  176. napari_track_edit-1.0.0/tests/data_views/test_lazy_array_wrapper.py +201 -0
  177. napari_track_edit-1.0.0/tests/data_views/test_link_clipping_planes.py +125 -0
  178. napari_track_edit-1.0.0/tests/data_views/test_ortho_views.py +354 -0
  179. napari_track_edit-1.0.0/tests/data_views/test_track_labels.py +670 -0
  180. napari_track_edit-1.0.0/tests/data_views/test_track_points.py +500 -0
  181. napari_track_edit-1.0.0/tests/data_views/test_visualization_widget.py +504 -0
  182. napari_track_edit-1.0.0/tests/data_views/views/layers/test_click_utils.py +200 -0
  183. napari_track_edit-1.0.0/tests/data_views/views/layers/test_out_of_slice_points.py +186 -0
  184. napari_track_edit-1.0.0/tests/data_views/views/layers/test_paint_atoms.py +201 -0
  185. napari_track_edit-1.0.0/tests/data_views/views/layers/test_paint_event_parsing.py +561 -0
  186. napari_track_edit-1.0.0/tests/data_views/views/layers/test_track_graph.py +47 -0
  187. napari_track_edit-1.0.0/tests/data_views/views/table/test_table_widget.py +295 -0
  188. napari_track_edit-1.0.0/tests/data_views/views/tree_view/test_navigation_widget.py +606 -0
  189. napari_track_edit-1.0.0/tests/data_views/views/tree_view/test_tree_widget.py +595 -0
  190. napari_track_edit-1.0.0/tests/data_views/views/tree_view/test_tree_widget_utils.py +207 -0
  191. napari_track_edit-1.0.0/tests/data_views/views_coordinator/test_groups.py +559 -0
  192. napari_track_edit-1.0.0/tests/data_views/views_coordinator/test_merge_horizontally.py +162 -0
  193. napari_track_edit-1.0.0/tests/data_views/views_coordinator/test_selection_history.py +351 -0
  194. napari_track_edit-1.0.0/tests/data_views/views_coordinator/test_tracks_list.py +914 -0
  195. napari_track_edit-1.0.0/tests/data_views/views_coordinator/test_tracks_viewer.py +1182 -0
  196. napari_track_edit-1.0.0/tests/import_export/hela_example_tracks.csv +8602 -0
  197. napari_track_edit-1.0.0/tests/import_export/test_export_dialog.py +763 -0
  198. napari_track_edit-1.0.0/tests/import_export/test_geff_scale_widget.py +103 -0
  199. napari_track_edit-1.0.0/tests/import_export/test_import_dialog.py +1203 -0
  200. napari_track_edit-1.0.0/tests/import_export/test_segmentation_widgets.py +581 -0
  201. napari_track_edit-1.0.0/tests/import_export/test_sql_io.py +545 -0
  202. napari_track_edit-1.0.0/tests/motile/backend/test_motile_run.py +159 -0
  203. napari_track_edit-1.0.0/tests/motile/backend/test_solver.py +164 -0
  204. napari_track_edit-1.0.0/tests/motile/menus/test_motile_widget.py +297 -0
  205. napari_track_edit-1.0.0/tests/motile/menus/test_params_editor.py +244 -0
  206. napari_track_edit-1.0.0/tests/motile/menus/test_run_editor.py +229 -0
  207. napari_track_edit-1.0.0/tests/motile/menus/test_run_viewer.py +235 -0
  208. napari_track_edit-1.0.0/tests/test_cli.py +71 -0
  209. napari_track_edit-1.0.0/tests/test_example_data.py +333 -0
@@ -0,0 +1,9 @@
1
+ # Caroline is the default reviewer for all code
2
+ * @cmalinmayor
3
+
4
+ # Anniek is the tree widget expert
5
+ /src/napari_track_edit/data_views/views/tree_view/* @anniekstok
6
+
7
+
8
+ # Anniek is also the import/export expert
9
+ /src/napari_track_edit/import_export/* @anniekstok
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
7
+ groups:
8
+ dependencies:
9
+ patterns:
10
+ - "*"
@@ -0,0 +1,63 @@
1
+ name: Benchmark
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ deployments: write
11
+ contents: write
12
+
13
+ jobs:
14
+ benchmark:
15
+ name: Report benchmarks on gh-pages
16
+ runs-on: ubuntu-latest
17
+ env:
18
+ UV_NO_SYNC: "1"
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ # The Ubuntu runner image ships broken Microsoft apt repos whose signature
22
+ # is periodically invalid ("Clearsigned file isn't valid, got 'NOSPLIT'"),
23
+ # which fails the apt-get update inside setup-qt-libs. We don't need them.
24
+ - name: Remove broken Microsoft apt sources (ubuntu runner image)
25
+ run: |
26
+ sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list \
27
+ /etc/apt/sources.list.d/azure-cli.list \
28
+ /etc/apt/sources.list.d/azure-cli.sources
29
+ - uses: tlambert03/setup-qt-libs@v1
30
+
31
+ # wgpu (via pygfx/fastplotlib, used by the tree view) needs a real Vulkan/GL
32
+ # adapter; Xvfb (below) only provides X11/GLX, not Vulkan. lavapipe/llvmpipe
33
+ # give it a software one, matching how pygfx/wgpu-py run their own Linux CI.
34
+ - name: Install llvmpipe and lavapipe for offscreen wgpu rendering
35
+ run: |
36
+ sudo apt-get update -y -qq
37
+ sudo apt-get install -y libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
38
+
39
+ - name: Set up Python
40
+ uses: astral-sh/setup-uv@v7
41
+ with:
42
+ python-version: "3.13"
43
+ enable-cache: true
44
+
45
+ - name: Install dependencies
46
+ run: uv sync --no-dev --group testing
47
+
48
+ # tests/benchmarks is ignored by default pytest config; `-o addopts=` clears
49
+ # that. GUI benchmarks need a real GL context, hence aganders3/headless-gui.
50
+ - name: Run benchmarks
51
+ uses: aganders3/headless-gui@v2
52
+ with:
53
+ run: uv run pytest tests/benchmarks -o addopts= --benchmark-json output.json
54
+
55
+ - name: Store benchmark results
56
+ uses: benchmark-action/github-action-benchmark@v1
57
+ with:
58
+ name: napari_track_edit benchmarks (pytest-benchmark)
59
+ tool: "pytest"
60
+ output-file-path: output.json
61
+ benchmark-data-dir-path: benchmarks
62
+ github-token: ${{ secrets.GITHUB_TOKEN }}
63
+ auto-push: true
@@ -0,0 +1,142 @@
1
+ """Compare two pytest-benchmark JSON files and produce a markdown table.
2
+
3
+ Usage:
4
+ python benchmark_pr.py <old.json> <new.json> <output.md> [header]
5
+
6
+ The table reports mean ± standard deviation for human readability, but the
7
+ pass/fail gate compares the *median*. Median is robust to a single outlier round
8
+ in either direction: `min` is biased down by an accidentally-fast run, `mean` up
9
+ by an unlucky-slow one (a transient OS/GC spike). With >=3 rounds the median
10
+ ignores one such spike, which is what keeps the noisy single-action benchmarks
11
+ from tripping the gate spuriously.
12
+
13
+ Exits with code 1 if any benchmark's median regresses by more than
14
+ REGRESSION_THRESHOLD percent.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ import pandas as pd
22
+
23
+ REGRESSION_THRESHOLD = 50 # percent
24
+ # Benchmarks with a median below this floor are dominated by timer resolution and OS
25
+ # scheduling jitter rather than the code being measured, so a "regression" on them is
26
+ # noise, not signal: skip the gate (still shown in the report) unless the benchmark is
27
+ # slow on at least one side, which means it should have been caught by the floor already.
28
+ NOISE_FLOOR_SECONDS = 0.02
29
+
30
+
31
+ def load_stats(path):
32
+ with open(path) as f:
33
+ data = json.load(f)
34
+
35
+ commit = data["commit_info"]["id"]
36
+
37
+ rows = []
38
+ for d in data["benchmarks"]:
39
+ s = d["stats"]
40
+ rows.append(
41
+ {
42
+ "Benchmark": d["name"],
43
+ "median": s["median"],
44
+ "mean": s["mean"],
45
+ "stddev": s["stddev"],
46
+ }
47
+ )
48
+
49
+ return commit, pd.DataFrame(rows)
50
+
51
+
52
+ def _fmt(mean, std):
53
+ return f"{mean:.5f} ± {std:.5f}"
54
+
55
+
56
+ def _write(out_file, report):
57
+ with open(out_file, "w") as f:
58
+ f.write(report)
59
+ print(report) # noqa: T201
60
+
61
+
62
+ def make_report(old_path, new_path, out_file, header=None):
63
+ new_commit, new_df = load_stats(new_path)
64
+
65
+ # No baseline available (e.g. the first PR introducing benchmarks, or a run on
66
+ # a base commit that predates the suite). Report HEAD-only numbers and pass.
67
+ if not os.path.exists(old_path) or os.path.getsize(old_path) == 0:
68
+ df = pd.DataFrame(
69
+ {
70
+ "Benchmark": new_df["Benchmark"],
71
+ f"Median (s) HEAD {new_commit}": new_df["median"].map("{:.5f}".format),
72
+ f"Mean ± SD (s) HEAD {new_commit}": [
73
+ _fmt(m, s)
74
+ for m, s in zip(new_df["mean"], new_df["stddev"], strict=True)
75
+ ],
76
+ }
77
+ )
78
+ # disable_numparse: see note on the comparison table below.
79
+ report = df.to_markdown(index=False, disable_numparse=True)
80
+ note = "_No baseline found; showing HEAD results only (no comparison)._"
81
+ report = f"{note}\n\n{report}"
82
+ if header:
83
+ report = f"## {header}\n\n{report}"
84
+ _write(out_file, report)
85
+ return
86
+
87
+ old_commit, old_df = load_stats(old_path)
88
+
89
+ # Merge on benchmark name (drops benchmarks present on only one side).
90
+ merged = old_df.merge(new_df, on="Benchmark", suffixes=("_old", "_new"))
91
+
92
+ # Gate on the median (see module docstring).
93
+ pct_change = (
94
+ 100 * (merged["median_new"] - merged["median_old"]) / merged["median_old"]
95
+ )
96
+
97
+ # A benchmark below the floor on both sides is dominated by timer resolution and OS
98
+ # scheduling jitter rather than the code being measured, so a "regression" on it is
99
+ # noise, not signal: exclude it from the gate (still shown in the report).
100
+ above_floor = (merged["median_old"] >= NOISE_FLOOR_SECONDS) | (
101
+ merged["median_new"] >= NOISE_FLOOR_SECONDS
102
+ )
103
+
104
+ # Median columns first: they drive the gate, so "Median Change" is verifiable
105
+ # straight from them. Mean ± SD follows as a noise indicator (a wide SD or a mean
106
+ # far from the median flags a spiky benchmark whose median we're rightly trusting).
107
+ df = pd.DataFrame(
108
+ {
109
+ "Benchmark": merged["Benchmark"],
110
+ f"Median (s) BASE {old_commit}": merged["median_old"].map("{:.5f}".format),
111
+ f"Median (s) HEAD {new_commit}": merged["median_new"].map("{:.5f}".format),
112
+ "Median Change": pct_change.map("{:+.2f}%".format),
113
+ f"Mean ± SD (s) BASE {old_commit}": [
114
+ _fmt(m, s)
115
+ for m, s in zip(merged["mean_old"], merged["stddev_old"], strict=True)
116
+ ],
117
+ f"Mean ± SD (s) HEAD {new_commit}": [
118
+ _fmt(m, s)
119
+ for m, s in zip(merged["mean_new"], merged["stddev_new"], strict=True)
120
+ ],
121
+ }
122
+ )
123
+
124
+ # disable_numparse: keep our fixed-precision strings exactly (tabulate otherwise
125
+ # re-parses numeric-looking cells and strips trailing zeros, so medians would show
126
+ # inconsistent precision next to the mean ± SD column).
127
+ report = df.to_markdown(index=False, disable_numparse=True)
128
+ if header:
129
+ report = f"## {header}\n\n{report}"
130
+ _write(out_file, report)
131
+
132
+ # Fail if any benchmark above the noise floor regressed beyond threshold
133
+ if (above_floor & (pct_change > REGRESSION_THRESHOLD)).any():
134
+ print( # noqa: T201
135
+ f"\nFAILED: median regression exceeds {REGRESSION_THRESHOLD}% threshold"
136
+ )
137
+ sys.exit(1)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ header = sys.argv[4] if len(sys.argv) > 4 else None
142
+ make_report(sys.argv[1], sys.argv[2], sys.argv[3], header)
@@ -0,0 +1,141 @@
1
+ name: benchmarks
2
+
3
+ on:
4
+ pull_request: {}
5
+ workflow_dispatch:
6
+
7
+ concurrency:
8
+ group: ${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ # Needed for peter-evans/commit-comment to post the benchmark report on the commit.
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ benchmark:
17
+ name: Benchmark (${{ matrix.platform }})
18
+ runs-on: ${{ matrix.platform }}
19
+ if: github.event_name == 'pull_request'
20
+ # Healthy runs finish in ~10 minutes; without this a hang (e.g. a wgpu/tree-widget
21
+ # deadlock) burns the full 360-minute default before GitHub cancels it.
22
+ timeout-minutes: 30
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ platform: [ubuntu-latest, macos-latest, windows-latest]
27
+ env:
28
+ # Stop `uv run` from auto-resyncing (which would drop the `testing` group).
29
+ # Each benchmark step below runs its own explicit `uv sync` after checkout so
30
+ # base and head are each measured against *their own* locked dependencies.
31
+ UV_NO_SYNC: "1"
32
+
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+ with:
36
+ fetch-depth: 50
37
+ # The Ubuntu runner image ships broken Microsoft apt repos whose signature
38
+ # is periodically invalid ("Clearsigned file isn't valid, got 'NOSPLIT'"),
39
+ # which fails the apt-get update inside setup-qt-libs. We don't need them.
40
+ - name: Remove broken Microsoft apt sources (ubuntu runner image)
41
+ if: runner.os == 'Linux'
42
+ run: |
43
+ sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list \
44
+ /etc/apt/sources.list.d/azure-cli.list \
45
+ /etc/apt/sources.list.d/azure-cli.sources
46
+ - uses: tlambert03/setup-qt-libs@v1
47
+
48
+ # wgpu (via pygfx/fastplotlib, used by the tree view) needs a real Vulkan/GL
49
+ # adapter; Xvfb (below) only provides X11/GLX, not Vulkan. lavapipe/llvmpipe
50
+ # give it a software one, matching how pygfx/wgpu-py run their own Linux CI.
51
+ - name: Install llvmpipe and lavapipe for offscreen wgpu rendering
52
+ if: runner.os == 'Linux'
53
+ run: |
54
+ sudo apt-get update -y -qq
55
+ sudo apt-get install -y libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
56
+
57
+ - name: Set up Python
58
+ uses: astral-sh/setup-uv@v7
59
+ with:
60
+ python-version: "3.13"
61
+ enable-cache: true
62
+
63
+ - name: Set Windows resolution
64
+ if: runner.os == 'Windows'
65
+ run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
66
+ shell: powershell
67
+
68
+ - name: Install Windows OpenGL
69
+ if: runner.os == 'Windows'
70
+ run: |
71
+ git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
72
+ powershell gl-ci-helpers/appveyor/install_opengl.ps1
73
+ if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}
74
+ shell: powershell
75
+
76
+ # Same-runner base-vs-head comparison (running baseline fresh every PR) turned
77
+ # out not to fix cross-run noise either -- swings as large as the ones it was
78
+ # meant to prevent still showed up within a single same-runner run. Since running
79
+ # baseline live buys no real stability, cache it per base-SHA instead: the first
80
+ # PR against a given base commit produces it, every later PR against that same
81
+ # base restores it instead of re-running -- half the benchmark cost with no
82
+ # measurement downside.
83
+ - name: Retrieve cached baseline if available
84
+ uses: actions/cache/restore@v5
85
+ id: cache_baseline
86
+ with:
87
+ path: baseline.json
88
+ key: benchmark-${{ matrix.platform }}-${{ github.event.pull_request.base.sha }}
89
+
90
+ # The benchmark suite lives under tests/benchmarks, which the default pytest
91
+ # config ignores; `-o addopts=` clears that so it is collected here. The GUI
92
+ # benchmarks need a real GL context, hence aganders3/headless-gui.
93
+ #
94
+ # Runs its own `uv sync` after checkout so base is measured against its OWN
95
+ # locked dependencies, not head's. This is what makes dependency-bump PRs
96
+ # (funtracks/tracksdata/...) measure the real base-vs-head delta: without this,
97
+ # base code would run against head's deps -- an apples-to-oranges comparison
98
+ # that reports fictional regressions whenever a dependency changes a shared
99
+ # contract. `git checkout -f` discards any uv.lock churn from a previous sync.
100
+ # Tolerant on purpose: the base commit may predate the benchmark suite, in which
101
+ # case there is no baseline and the report falls back to HEAD-only numbers.
102
+ - name: Run baseline benchmark if not in cache
103
+ if: steps.cache_baseline.outputs.cache-hit != 'true'
104
+ continue-on-error: true
105
+ uses: aganders3/headless-gui@v2
106
+ with:
107
+ run: |
108
+ git checkout -f ${{ github.event.pull_request.base.sha }}
109
+ uv sync --no-dev --group testing
110
+ uv run pytest tests/benchmarks -o addopts= --benchmark-json baseline.json
111
+
112
+ - name: Cache baseline results
113
+ uses: actions/cache/save@v5
114
+ if: steps.cache_baseline.outputs.cache-hit != 'true' && hashFiles('baseline.json') != ''
115
+ with:
116
+ path: baseline.json
117
+ key: benchmark-${{ matrix.platform }}-${{ github.event.pull_request.base.sha }}
118
+
119
+ - name: Run benchmark on PR head
120
+ uses: aganders3/headless-gui@v2
121
+ with:
122
+ run: |
123
+ git checkout -f ${{ github.event.pull_request.head.sha }}
124
+ uv sync --no-dev --group testing
125
+ uv run pytest tests/benchmarks -o addopts= --benchmark-json pr.json
126
+
127
+ - name: Generate report
128
+ id: report
129
+ run: |
130
+ uv run --with pandas --with tabulate python .github/workflows/benchmark_pr.py baseline.json pr.json report.md "Benchmark: ${{ matrix.platform }}"
131
+ continue-on-error: true
132
+
133
+ - name: Comment on commit with report
134
+ uses: peter-evans/commit-comment@v4
135
+ if: github.event.pull_request.head.repo.fork == false
136
+ with:
137
+ body-path: report.md
138
+
139
+ - name: Fail if regression detected
140
+ if: steps.report.outcome == 'failure'
141
+ run: exit 1
@@ -0,0 +1,35 @@
1
+ name: Deploy
2
+
3
+ on:
4
+ push:
5
+ tags: ["*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build-n-publish:
10
+ name: Build and publish Python 🐍 distributions 📦 to PyPI
11
+ runs-on: ubuntu-latest
12
+ if: contains(github.ref, 'tags')
13
+ permissions:
14
+ id-token: write
15
+ contents: write
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ - uses: actions/setup-python@v7
19
+ with:
20
+ python-version: "3.x"
21
+ - name: Build a binary wheel and a source tarball
22
+ run: |
23
+ python -m pip install -U pip
24
+ python -m pip install -U setuptools setuptools_scm build
25
+ python -m build --sdist --wheel --outdir dist/
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
28
+ with:
29
+ skip-existing: true
30
+ - name: Create release
31
+ uses: softprops/action-gh-release@v3
32
+ with:
33
+ draft: ${{ contains(github.ref_name, '-dev') }}
34
+ prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-a') || contains(github.ref_name, '-b') }}
35
+ generate_release_notes: true
@@ -0,0 +1,47 @@
1
+ name: Docs build
2
+
3
+ # build the documentation whenever there are new commits on main
4
+ on:
5
+ push:
6
+ branches:
7
+ - main
8
+ workflow_dispatch:
9
+
10
+ # security: restrict permissions for CI jobs.
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ # Build the documentation and upload the static HTML files as an artifact.
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ - name: Set up Python 3.11
21
+ uses: astral-sh/setup-uv@v7
22
+ with:
23
+ python-version: '3.11'
24
+ enable-cache: true
25
+ - name: Install dependencies
26
+ run: uv sync
27
+ - name: Build docs
28
+ run: |
29
+ cd docs/
30
+ uv run make html
31
+ - uses: actions/upload-pages-artifact@v5
32
+ with:
33
+ path: docs/build/html
34
+
35
+ # Deploy the artifact to GitHub pages.
36
+ deploy:
37
+ needs: build
38
+ runs-on: ubuntu-latest
39
+ permissions:
40
+ pages: write
41
+ id-token: write
42
+ environment:
43
+ name: github-pages
44
+ url: ${{ steps.deployment.outputs.page_url }}
45
+ steps:
46
+ - id: deployment
47
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,42 @@
1
+ name: download sources
2
+
3
+ # Checks that the example data the app downloads (Zenodo, Google Drive, CTC) is
4
+ # still available. These servers are flaky, so the check is kept out of the normal
5
+ # test run (see the `network` marker in pyproject.toml) and runs monthly instead.
6
+ on:
7
+ schedule:
8
+ - cron: "0 6 1 * *" # 06:00 UTC on the 1st of every month
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ download-sources:
13
+ name: Download sources are reachable
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 30
16
+ env:
17
+ # disable resyncing on uv run
18
+ UV_NO_SYNC: "1"
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ # The Ubuntu runner image ships broken Microsoft apt repos whose signature
22
+ # is periodically invalid, which fails the apt-get update inside setup-qt-libs.
23
+ - name: Remove broken Microsoft apt sources (ubuntu runner image)
24
+ run: |
25
+ sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list \
26
+ /etc/apt/sources.list.d/azure-cli.list \
27
+ /etc/apt/sources.list.d/azure-cli.sources
28
+ - uses: tlambert03/setup-qt-libs@v1
29
+
30
+ - name: Set up Python
31
+ uses: astral-sh/setup-uv@v7
32
+ with:
33
+ python-version: "3.13"
34
+ enable-cache: true
35
+
36
+ - name: Install dependencies
37
+ run: uv sync --no-dev --group testing
38
+
39
+ - name: Test
40
+ uses: aganders3/headless-gui@v2
41
+ with:
42
+ run: uv run pytest --color=yes -m network tests
@@ -0,0 +1,17 @@
1
+ name: bundleApp
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - released
7
+ - published
8
+ # motile_tracker_bundle_app will be triggered when the release is changed from draft to published
9
+
10
+ jobs:
11
+ packaging:
12
+ permissions:
13
+ contents: write
14
+ uses: funkelab/motile_tracker_bundled_app/.github/workflows/bundle_app.yml@main
15
+ secrets: inherit
16
+ with:
17
+ event_name: ${{ github.event_name }}
@@ -0,0 +1,112 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"] # Push events to matching v*, i.e. v1.0, v20.15.10
7
+ pull_request: {}
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ test:
16
+ name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.resolution }}
17
+ runs-on: ${{ matrix.platform }}
18
+ # Healthy runs finish in 5-12 minutes; without this a hang (e.g. a wgpu/tree-widget
19
+ # deadlock) burns the full 360-minute default before GitHub cancels it.
20
+ timeout-minutes: 30
21
+ env:
22
+ # disable resyncing on uv run
23
+ UV_NO_SYNC: "1"
24
+ strategy:
25
+ fail-fast: false
26
+ # OS-specific bugs are almost always Qt/GL-driver related and Python-version-
27
+ # agnostic, while version-specific bugs are language/stdlib related and OS-
28
+ # agnostic -- the two axes rarely interact, so we don't need their full cross:
29
+ # - lowest-direct (oldest allowed deps) on all platforms, but only at our
30
+ # floor Python (3.11, see requires-python) -- those dependency versions
31
+ # predate 3.12/3.13 anyway, so testing them there too is redundant.
32
+ # - highest deps on all platforms, but only at our newest Python (3.13).
33
+ # - the remaining Python version (3.12) only on ubuntu (cheapest runner), as
34
+ # a spot-check rather than a full platform cross.
35
+ matrix:
36
+ include:
37
+ - platform: ubuntu-latest
38
+ python-version: "3.11"
39
+ resolution: lowest-direct
40
+ - platform: macos-latest
41
+ python-version: "3.11"
42
+ resolution: lowest-direct
43
+ - platform: windows-latest
44
+ python-version: "3.11"
45
+ resolution: lowest-direct
46
+ - platform: ubuntu-latest
47
+ python-version: "3.12"
48
+ resolution: highest
49
+ - platform: ubuntu-latest
50
+ python-version: "3.13"
51
+ resolution: highest
52
+ - platform: macos-latest
53
+ python-version: "3.13"
54
+ resolution: highest
55
+ - platform: windows-latest
56
+ python-version: "3.13"
57
+ resolution: highest
58
+
59
+ steps:
60
+ - uses: actions/checkout@v7
61
+ # The Ubuntu runner image ships broken Microsoft apt repos whose signature
62
+ # is periodically invalid ("Clearsigned file isn't valid, got 'NOSPLIT'"),
63
+ # which fails the apt-get update inside setup-qt-libs. We don't need them.
64
+ - name: Remove broken Microsoft apt sources (ubuntu runner image)
65
+ if: runner.os == 'Linux'
66
+ run: |
67
+ sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list \
68
+ /etc/apt/sources.list.d/azure-cli.list \
69
+ /etc/apt/sources.list.d/azure-cli.sources
70
+ - uses: tlambert03/setup-qt-libs@v1
71
+
72
+ # wgpu (via pygfx/fastplotlib, used by the tree view) needs a real Vulkan/GL
73
+ # adapter; Xvfb (below) only provides X11/GLX, not Vulkan. lavapipe/llvmpipe
74
+ # give it a software one, matching how pygfx/wgpu-py run their own Linux CI.
75
+ - name: Install llvmpipe and lavapipe for offscreen wgpu rendering
76
+ if: runner.os == 'Linux'
77
+ run: |
78
+ sudo apt-get update -y -qq
79
+ sudo apt-get install -y libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
80
+
81
+ - name: Set up Python ${{ matrix.python-version }}
82
+ uses: astral-sh/setup-uv@v7
83
+ with:
84
+ python-version: ${{ matrix.python-version }}
85
+ enable-cache: true
86
+
87
+ - name: Install dependencies
88
+ run: uv sync --no-dev --group testing --resolution ${{ matrix.resolution }}
89
+
90
+ - name: Set Windows resolution
91
+ if: runner.os == 'Windows'
92
+ run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
93
+ shell: powershell
94
+
95
+ - name: Install Windows OpenGL
96
+ if: runner.os == 'Windows'
97
+ run: |
98
+ git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git
99
+ powershell gl-ci-helpers/appveyor/install_opengl.ps1
100
+ if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}
101
+ shell: powershell
102
+
103
+ - name: Test
104
+ uses: aganders3/headless-gui@v2
105
+ with:
106
+ run: uv run pytest --color=yes --cov=napari_track_edit --cov-report=xml --cov-report=term-missing .
107
+
108
+ - name: Coverage
109
+ uses: codecov/codecov-action@v7
110
+ with:
111
+ token: ${{ secrets.CODECOV_TOKEN }}
112
+ slug: live-image-tracking-tools/napari-track-edit