boris-behav-obs 9.2.1__tar.gz → 9.2.2__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 (120) hide show
  1. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/PKG-INFO +4 -7
  2. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/behavior_binary_table.py +4 -6
  3. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/core.py +2 -7
  4. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/export_events.py +1 -1
  5. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/observation_operations.py +16 -9
  6. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/player_dock_widget.py +19 -4
  7. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/plot_events.py +1 -1
  8. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/plugins.py +31 -4
  9. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/project_functions.py +6 -14
  10. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/time_budget_functions.py +1 -1
  11. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/time_budget_widget.py +1 -1
  12. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/utilities.py +0 -2
  13. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/version.py +2 -2
  14. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris_behav_obs.egg-info/PKG-INFO +4 -7
  15. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris_behav_obs.egg-info/SOURCES.txt +2 -1
  16. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris_behav_obs.egg-info/requires.txt +0 -1
  17. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/pyproject.toml +4 -11
  18. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_utilities.py +7 -0
  19. boris_behav_obs-9.2.2/tests/test_utilities2.py +124 -0
  20. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/LICENSE.TXT +0 -0
  21. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/MANIFEST.in +0 -0
  22. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/README.TXT +0 -0
  23. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/README.md +0 -0
  24. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/__init__.py +0 -0
  25. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/__main__.py +0 -0
  26. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/about.py +0 -0
  27. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/add_modifier.py +0 -0
  28. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/add_modifier_ui.py +0 -0
  29. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/advanced_event_filtering.py +0 -0
  30. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/analysis_plugins/__init__.py +0 -0
  31. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/analysis_plugins/number_of_occurences.py +0 -0
  32. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/analysis_plugins/number_of_occurences_by_independent_variable.py +0 -0
  33. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/analysis_plugins/time_budget.py +0 -0
  34. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/behav_coding_map_creator.py +0 -0
  35. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/behaviors_coding_map.py +0 -0
  36. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/boris_cli.py +0 -0
  37. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/cmd_arguments.py +0 -0
  38. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/coding_pad.py +0 -0
  39. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/config.py +0 -0
  40. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/config_file.py +0 -0
  41. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/connections.py +0 -0
  42. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/converters.py +0 -0
  43. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/converters_ui.py +0 -0
  44. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/cooccurence.py +0 -0
  45. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/core_qrc.py +0 -0
  46. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/core_ui.py +0 -0
  47. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/db_functions.py +0 -0
  48. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/dev.py +0 -0
  49. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/dialog.py +0 -0
  50. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/duration_widget.py +0 -0
  51. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/edit_event.py +0 -0
  52. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/edit_event_ui.py +0 -0
  53. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/event_operations.py +0 -0
  54. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/events_cursor.py +0 -0
  55. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/events_snapshots.py +0 -0
  56. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/exclusion_matrix.py +0 -0
  57. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/export_observation.py +0 -0
  58. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/external_processes.py +0 -0
  59. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/geometric_measurement.py +0 -0
  60. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/gui_utilities.py +0 -0
  61. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/image_overlay.py +0 -0
  62. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/import_observations.py +0 -0
  63. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/irr.py +0 -0
  64. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/latency.py +0 -0
  65. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/map_creator.py +0 -0
  66. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/measurement_widget.py +0 -0
  67. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/media_file.py +0 -0
  68. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/menu_options.py +0 -0
  69. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/modifiers_coding_map.py +0 -0
  70. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/mpv-1.0.3.py +0 -0
  71. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/mpv.py +0 -0
  72. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/mpv2.py +0 -0
  73. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/observation.py +0 -0
  74. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/observation_ui.py +0 -0
  75. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/observations_list.py +0 -0
  76. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/otx_parser.py +0 -0
  77. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/param_panel.py +0 -0
  78. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/param_panel_ui.py +0 -0
  79. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/plot_data_module.py +0 -0
  80. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/plot_events_rt.py +0 -0
  81. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/plot_spectrogram_rt.py +0 -0
  82. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/plot_waveform_rt.py +0 -0
  83. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/portion/__init__.py +0 -0
  84. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/portion/const.py +0 -0
  85. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/portion/dict.py +0 -0
  86. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/portion/func.py +0 -0
  87. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/portion/interval.py +0 -0
  88. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/portion/io.py +0 -0
  89. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/preferences.py +0 -0
  90. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/preferences_ui.py +0 -0
  91. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/project.py +0 -0
  92. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/project_import_export.py +0 -0
  93. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/project_ui.py +0 -0
  94. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/qrc_boris.py +0 -0
  95. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/qrc_boris5.py +0 -0
  96. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/select_modifiers.py +0 -0
  97. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/select_observations.py +0 -0
  98. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/select_subj_behav.py +0 -0
  99. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/state_events.py +0 -0
  100. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/subjects_pad.py +0 -0
  101. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/synthetic_time_budget.py +0 -0
  102. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/transitions.py +0 -0
  103. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/video_equalizer.py +0 -0
  104. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/video_equalizer_ui.py +0 -0
  105. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/video_operations.py +0 -0
  106. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/view_df.py +0 -0
  107. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/view_df_ui.py +0 -0
  108. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris/write_event.py +0 -0
  109. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris_behav_obs.egg-info/dependency_links.txt +0 -0
  110. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris_behav_obs.egg-info/entry_points.txt +0 -0
  111. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/boris_behav_obs.egg-info/top_level.txt +0 -0
  112. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/setup.cfg +0 -0
  113. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_db_functions.py +0 -0
  114. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_export_observation.py +0 -0
  115. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_irr.py +0 -0
  116. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_observation_gui.py +0 -0
  117. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_otx_parser.py +0 -0
  118. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_preferences_gui.py +0 -0
  119. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_project_functions.py +0 -0
  120. {boris_behav_obs-9.2.1 → boris_behav_obs-9.2.2}/tests/test_time_budget.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.2.1
3
+ Version: 9.2.2
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -686,13 +686,10 @@ Project-URL: Issues, https://github.com/olivierfriard/BORIS/issues
686
686
  Classifier: Topic :: Scientific/Engineering
687
687
  Classifier: Intended Audience :: Science/Research
688
688
  Classifier: Intended Audience :: Education
689
- Classifier: Programming Language :: Python :: 3.8
690
- Classifier: Programming Language :: Python :: 3.9
691
- Classifier: Programming Language :: Python :: 3.10
692
- Classifier: Programming Language :: Python :: 3.11
693
689
  Classifier: Programming Language :: Python :: 3.12
694
690
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
695
691
  Classifier: Operating System :: OS Independent
692
+ Classifier: Topic :: Scientific/Engineering
696
693
  Requires-Python: >=3.12
697
694
  Description-Content-Type: text/x-rst
698
695
  License-File: LICENSE.TXT
@@ -705,7 +702,7 @@ Requires-Dist: pyreadr
705
702
  Requires-Dist: pyside6==6.8.0.2
706
703
  Requires-Dist: hachoir>=3.3.0
707
704
  Provides-Extra: dev
708
- Requires-Dist: black; extra == "dev"
709
705
  Requires-Dist: ruff; extra == "dev"
710
706
  Requires-Dist: pytest; extra == "dev"
711
707
  Requires-Dist: pytest-cov; extra == "dev"
708
+ Dynamic: license-file
@@ -80,12 +80,12 @@ def create_behavior_binary_table(pj: dict, selected_observations: list, paramete
80
80
  end_time = dec(max_obs_length)
81
81
 
82
82
  if parameters_obs["time"] == cfg.TIME_OBS_INTERVAL:
83
- obs_interval = pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
83
+ obs_interval = pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
84
84
  offset = pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
85
85
  start_time = dec(obs_interval[0]) + offset
86
86
  # Use max observation length for end time if no interval is defined (=0)
87
87
  max_obs_length, _ = observation_operations.observation_length(pj, [obs_id])
88
- end_time = dec(obs_interval[1]) + offset if obs_interval[1] != 0 else dec(max_obs_length)
88
+ end_time = dec(obs_interval[1]) + offset if obs_interval[1] not in (0, None) else dec(max_obs_length)
89
89
 
90
90
  if obs_id not in results_df:
91
91
  results_df[obs_id] = {}
@@ -166,8 +166,8 @@ def behavior_binary_table(self):
166
166
  None,
167
167
  cfg.programName,
168
168
  (
169
- "Depending of the length of your observations "
170
- "the execution of this function may be very long.<br>"
169
+ "Depending on the length of yours observations "
170
+ "the execution of this function may take a long time.<br>"
171
171
  "The program interface may freeze, be patient. <br>"
172
172
  ),
173
173
  )
@@ -216,8 +216,6 @@ def behavior_binary_table(self):
216
216
  selected_observations,
217
217
  start_coding=start_coding,
218
218
  end_coding=end_coding,
219
- # start_interval=start_interval,
220
- # end_interval=end_interval,
221
219
  start_interval=start_interval,
222
220
  end_interval=end_interval,
223
221
  maxTime=max_media_duration_all_obs,
@@ -3639,10 +3639,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3639
3639
  self.liveTimer.stop()
3640
3640
  self.liveObservationStarted = False
3641
3641
  # set current time to end of observation interval
3642
- current_time = dec(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
3642
+ current_time = dec(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
3643
3643
  self.pb_live_obs.setText("Live observation finished")
3644
3644
 
3645
-
3646
3645
  self.lb_current_media_time.setText(util.convertTime(self.timeFormat, current_time))
3647
3646
 
3648
3647
  # extract State events
@@ -3672,7 +3671,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
3672
3671
  self.liveTimer.stop()
3673
3672
  self.pb_live_obs.setText("Live observation stopped (scan sampling)")
3674
3673
 
3675
-
3676
3674
  def start_live_observation(self):
3677
3675
  """
3678
3676
  activate the live observation mode
@@ -5754,10 +5752,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
5754
5752
  if self.playerType == cfg.MEDIA:
5755
5753
  self.pause_video()
5756
5754
 
5757
- if cfg.OBSERVATION_TIME_INTERVAL in self.pj[cfg.OBSERVATIONS][self.observationId]:
5758
- self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.OBSERVATION_TIME_INTERVAL][0]))
5759
- else:
5760
- self.seek_mediaplayer(0)
5755
+ self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]))
5761
5756
 
5762
5757
  self.update_visualizations()
5763
5758
 
@@ -788,7 +788,7 @@ def export_events_as_textgrid(self) -> None:
788
788
 
789
789
  if parameters["time"] == cfg.TIME_OBS_INTERVAL:
790
790
  max_media_duration, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], [obs_id])
791
- obs_interval = self.pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
791
+ obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
792
792
  offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
793
793
  min_time = float(obs_interval[0]) + offset
794
794
  # Use max media duration for max time if no interval is defined (=0)
@@ -359,14 +359,20 @@ def time_intervals_range(observations: dict, observations_list: list) -> Tuple[O
359
359
  decimal.Decimal: time of latest end interval
360
360
 
361
361
  """
362
- start_interval_list = []
363
- end_interval_list = []
362
+ start_interval_list: list = []
363
+ end_interval_list: list = []
364
364
  for obs_id in observations_list:
365
365
  observation = observations[obs_id]
366
366
  offset = observation[cfg.TIME_OFFSET]
367
- if dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset and dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset:
368
- start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
369
- end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
367
+ # check if observation interval is defined
368
+ if (
369
+ not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0]
370
+ and not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1]
371
+ ):
372
+ return None, None
373
+
374
+ start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
375
+ end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
370
376
 
371
377
  if not start_interval_list:
372
378
  earliest_start_interval = None
@@ -872,8 +878,8 @@ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
872
878
  observationWindow.cb_observation_time_interval.setText(
873
879
  (
874
880
  "Limit observation to a time interval: "
875
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][0]} - "
876
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][1]}"
881
+ f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
882
+ f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
877
883
  )
878
884
  )
879
885
 
@@ -1950,8 +1956,9 @@ def initialize_new_media_observation(self) -> bool:
1950
1956
  )
1951
1957
 
1952
1958
  # position media
1953
- if cfg.OBSERVATION_TIME_INTERVAL in self.pj[cfg.OBSERVATIONS][self.observationId]:
1954
- self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.OBSERVATION_TIME_INTERVAL][0]), player=i)
1959
+ self.seek_mediaplayer(
1960
+ int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]), player=i
1961
+ )
1955
1962
 
1956
1963
  # restore video zoom level
1957
1964
  if cfg.ZOOM_LEVEL in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
@@ -23,7 +23,18 @@ This file is part of BORIS.
23
23
  import sys
24
24
  import logging
25
25
  import functools
26
- from PySide6.QtWidgets import QLabel, QDockWidget, QWidget, QHBoxLayout, QVBoxLayout, QSlider, QSizePolicy, QStackedWidget, QToolButton
26
+ from PySide6.QtWidgets import (
27
+ QApplication,
28
+ QLabel,
29
+ QDockWidget,
30
+ QWidget,
31
+ QHBoxLayout,
32
+ QVBoxLayout,
33
+ QSlider,
34
+ QSizePolicy,
35
+ QStackedWidget,
36
+ QToolButton,
37
+ )
27
38
  from PySide6.QtCore import Signal, QEvent, Qt
28
39
  from PySide6.QtGui import QIcon, QAction
29
40
 
@@ -141,7 +152,10 @@ class DW_player(QDockWidget):
141
152
  self.mute_button.setFocusPolicy(Qt.NoFocus)
142
153
  self.mute_button.setAutoRaise(True)
143
154
  self.mute_action = QAction()
144
- self.mute_action.setIcon(QIcon(":/volume_xmark"))
155
+
156
+ theme_mode = "dark" if QApplication.instance().palette().window().color().value() < 128 else "light"
157
+
158
+ self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
145
159
  self.mute_action.triggered.connect(self.mute_action_triggered)
146
160
  self.mute_button.setDefaultAction(self.mute_action)
147
161
 
@@ -180,10 +194,11 @@ class DW_player(QDockWidget):
180
194
  """
181
195
  emit signal when mute action is triggered
182
196
  """
197
+ theme_mode = "dark" if QApplication.instance().palette().window().color().value() < 128 else "light"
183
198
  if self.player.mute:
184
- self.mute_action.setIcon(QIcon(":/volume_xmark"))
199
+ self.mute_action.setIcon(QIcon(f":/volume_xmark_{theme_mode}"))
185
200
  else:
186
- self.mute_action.setIcon(QIcon(":/volume_off"))
201
+ self.mute_action.setIcon(QIcon(f":/volume_off_{theme_mode}"))
187
202
  self.mute_action_triggered_signal.emit(self.id_)
188
203
 
189
204
  def keyPressEvent(self, event):
@@ -441,7 +441,7 @@ def create_events_plot(
441
441
  max_time = float(obs_length)
442
442
 
443
443
  if interval == cfg.TIME_OBS_INTERVAL:
444
- obs_interval = self.pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
444
+ obs_interval = self.pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
445
445
  offset = float(self.pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET])
446
446
  min_time = float(obs_interval[0]) + offset
447
447
  # Use max media duration for max time if no interval is defined (=0)
@@ -65,24 +65,44 @@ def load_plugins(self):
65
65
  """
66
66
  load selected plugins in analysis menu
67
67
  """
68
+
69
+ def msg():
70
+ QMessageBox.warning(
71
+ self,
72
+ cfg.programName,
73
+ f"A plugin with the same name is already loaded ({self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name]}).\n\nThe plugin from {file_} is not loaded.",
74
+ QMessageBox.Ok | QMessageBox.Default,
75
+ QMessageBox.NoButton,
76
+ )
77
+
68
78
  self.menu_plugins.clear()
69
79
  self.config_param[cfg.ANALYSIS_PLUGINS] = {}
70
80
 
71
81
  # load BORIS plugins
72
- for file_ in (Path(__file__).parent / "analysis_plugins").glob("*.py"):
82
+ for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
73
83
  if file_.name == "__init__.py":
74
84
  continue
75
85
  plugin_name = get_plugin_name(file_)
76
86
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
87
+ # check if plugin with same name already loaded
88
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
89
+ msg()
90
+ continue
91
+
77
92
  self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
78
93
 
79
94
  # load personal plugins
80
95
  if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
81
- for file_ in Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py"):
96
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
82
97
  if file_.name == "__init__.py":
83
98
  continue
84
99
  plugin_name = get_plugin_name(file_)
85
100
  if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
101
+ # check if plugin with same name already loaded
102
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
103
+ msg()
104
+ continue
105
+
86
106
  self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
87
107
 
88
108
  logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
@@ -108,7 +128,7 @@ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters:
108
128
  # filter selected behaviors
109
129
  df = df[df["Behavior"].isin(parameters["selected behaviors"])]
110
130
 
111
- if parameters["time"] == "interval of observation":
131
+ if parameters["time"] == cfg.TIME_OBS_INTERVAL:
112
132
  # filter each observation with observation interval start/stop
113
133
 
114
134
  # keep events between observation interval start time and observation interval stop/end
@@ -142,6 +162,7 @@ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters:
142
162
  | ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
143
163
  ]
144
164
 
165
+ # cut state events to interval
145
166
  df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
146
167
  df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
147
168
 
@@ -152,7 +173,7 @@ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters:
152
173
  print("filtered")
153
174
  print("=" * 50)
154
175
 
155
- print(f"{df=}")
176
+ # print(f"{df=}")
156
177
 
157
178
  return df
158
179
 
@@ -205,8 +226,12 @@ def run_plugin(self, plugin_name):
205
226
  if not selected_observations:
206
227
  return
207
228
 
229
+ logging.info("preparing dtaaframe for plugin")
230
+
208
231
  df = project_functions.project2dataframe(self.pj, selected_observations)
209
232
 
233
+ logging.info("done")
234
+
210
235
  """
211
236
  logging.debug("dataframe info")
212
237
  logging.debug(f"{df.info()}")
@@ -214,7 +239,9 @@ def run_plugin(self, plugin_name):
214
239
  """
215
240
 
216
241
  # filter the dataframe with parameters
242
+ logging.info("filtering dataframe for plugin")
217
243
  filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
244
+ logging.info("done")
218
245
 
219
246
  plugin_results = plugin_module.run(filtered_df)
220
247
  # test if plugin_tests is a tuple: if not transform to tuple
@@ -1899,20 +1899,12 @@ def project2dataframe(pj: dict, observations_list: list = []) -> pd.DataFrame:
1899
1899
  if idx_event in stop_event_idx:
1900
1900
  continue
1901
1901
  data["Observation id"].append(obs_id)
1902
- data["Observation date"].append(pj["observations"][obs_id]["date"])
1903
- data["Description"].append(" ".join(pj["observations"][obs_id]["description"].splitlines()))
1904
- data["Observation type"].append(pj["observations"][obs_id]["type"])
1905
-
1906
- data["Observation interval start"].append(
1907
- pj["observations"][obs_id]["observation time interval"][0]
1908
- if pj["observations"][obs_id]["observation time interval"][0]
1909
- else None
1910
- )
1911
- data["Observation interval stop"].append(
1912
- pj["observations"][obs_id]["observation time interval"][1]
1913
- if pj["observations"][obs_id]["observation time interval"][1]
1914
- else None
1915
- )
1902
+ data["Observation date"].append(pj[cfg.OBSERVATIONS][obs_id]["date"])
1903
+ data["Description"].append(" ".join(pj[cfg.OBSERVATIONS][obs_id]["description"].splitlines()))
1904
+ data["Observation type"].append(pj[cfg.OBSERVATIONS][obs_id]["type"])
1905
+
1906
+ data["Observation interval start"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0])
1907
+ data["Observation interval stop"].append(pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1])
1916
1908
 
1917
1909
  # data["Source"].append("")
1918
1910
  # data["Time offset (s)"].append(pj["observations"][obs_id]["time offset"])
@@ -260,7 +260,7 @@ def synthetic_time_budget_bin(pj: dict, selected_observations: list, parameters_
260
260
  max_time = dec(end_time)
261
261
 
262
262
  if time_interval == cfg.TIME_OBS_INTERVAL:
263
- obs_interval = pj[cfg.OBSERVATIONS][obs_id][cfg.OBSERVATION_TIME_INTERVAL]
263
+ obs_interval = pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
264
264
  offset = pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
265
265
  min_time = dec(obs_interval[0]) + offset
266
266
  # Use max media duration for max time if no interval is defined (=0)
@@ -504,7 +504,7 @@ def time_budget(self, mode: str, mode2: str = "list"):
504
504
  max_time = float(obs_length)
505
505
 
506
506
  if parameters[cfg.TIME_INTERVAL] == cfg.TIME_OBS_INTERVAL:
507
- obs_interval = self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL]
507
+ obs_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
508
508
  offset = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
509
509
  min_time = float(obs_interval[0]) + offset
510
510
  # Use max media duration for max time if no interval is defined (=0)
@@ -1456,8 +1456,6 @@ def accurate_media_analysis(ffmpeg_bin: str, file_name: str) -> dict:
1456
1456
 
1457
1457
  ffprobe_results = ffprobe_media_analysis(ffmpeg_bin, file_name)
1458
1458
 
1459
- print(f"{ffprobe_results=}")
1460
-
1461
1459
  logging.debug(f"file_name: {file_name}")
1462
1460
  logging.debug(f"ffprobe_results: {ffprobe_results}")
1463
1461
 
@@ -20,5 +20,5 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- __version__ = "9.2.1"
24
- __version_date__ = "2025-03-17"
23
+ __version__ = "9.2.2"
24
+ __version_date__ = "2025-03-28"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: boris-behav-obs
3
- Version: 9.2.1
3
+ Version: 9.2.2
4
4
  Summary: BORIS - Behavioral Observation Research Interactive Software
5
5
  Author-email: Olivier Friard <olivier.friard@unito.it>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -686,13 +686,10 @@ Project-URL: Issues, https://github.com/olivierfriard/BORIS/issues
686
686
  Classifier: Topic :: Scientific/Engineering
687
687
  Classifier: Intended Audience :: Science/Research
688
688
  Classifier: Intended Audience :: Education
689
- Classifier: Programming Language :: Python :: 3.8
690
- Classifier: Programming Language :: Python :: 3.9
691
- Classifier: Programming Language :: Python :: 3.10
692
- Classifier: Programming Language :: Python :: 3.11
693
689
  Classifier: Programming Language :: Python :: 3.12
694
690
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
695
691
  Classifier: Operating System :: OS Independent
692
+ Classifier: Topic :: Scientific/Engineering
696
693
  Requires-Python: >=3.12
697
694
  Description-Content-Type: text/x-rst
698
695
  License-File: LICENSE.TXT
@@ -705,7 +702,7 @@ Requires-Dist: pyreadr
705
702
  Requires-Dist: pyside6==6.8.0.2
706
703
  Requires-Dist: hachoir>=3.3.0
707
704
  Provides-Extra: dev
708
- Requires-Dist: black; extra == "dev"
709
705
  Requires-Dist: ruff; extra == "dev"
710
706
  Requires-Dist: pytest; extra == "dev"
711
707
  Requires-Dist: pytest-cov; extra == "dev"
708
+ Dynamic: license-file
@@ -114,4 +114,5 @@ tests/test_otx_parser.py
114
114
  tests/test_preferences_gui.py
115
115
  tests/test_project_functions.py
116
116
  tests/test_time_budget.py
117
- tests/test_utilities.py
117
+ tests/test_utilities.py
118
+ tests/test_utilities2.py
@@ -8,7 +8,6 @@ pyside6==6.8.0.2
8
8
  hachoir>=3.3.0
9
9
 
10
10
  [dev]
11
- black
12
11
  ruff
13
12
  pytest
14
13
  pytest-cov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boris-behav-obs"
3
- version = "9.2.1"
3
+ version = "9.2.2"
4
4
  description = "BORIS - Behavioral Observation Research Interactive Software"
5
5
  authors = [{ name="Olivier Friard", email="olivier.friard@unito.it" }]
6
6
  readme = "README.rst"
@@ -10,13 +10,10 @@ classifiers=[
10
10
  "Topic :: Scientific/Engineering",
11
11
  "Intended Audience :: Science/Research",
12
12
  "Intended Audience :: Education",
13
- "Programming Language :: Python :: 3.8",
14
- "Programming Language :: Python :: 3.9",
15
- "Programming Language :: Python :: 3.10",
16
- "Programming Language :: Python :: 3.11",
17
13
  "Programming Language :: Python :: 3.12",
18
14
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
19
15
  "Operating System :: OS Independent",
16
+ "Topic :: Scientific/Engineering",
20
17
  ]
21
18
  dependencies = [
22
19
  "exifread>=3.0.0",
@@ -30,7 +27,7 @@ dependencies = [
30
27
  ]
31
28
 
32
29
  [project.optional-dependencies]
33
- dev = ["black", "ruff", "pytest", "pytest-cov"]
30
+ dev = ["ruff", "pytest", "pytest-cov"]
34
31
 
35
32
  [project.urls]
36
33
  Homepage = "http://www.boris.unito.it"
@@ -42,10 +39,6 @@ Issues = "https://github.com/olivierfriard/BORIS/issues"
42
39
  [project.scripts]
43
40
  boris-behav-obs = "boris:main"
44
41
 
45
- [build-system]
46
- requires = ["setuptools", "wheel"]
47
- build-backend = "setuptools.build_meta"
48
-
49
42
 
50
43
  [tool.distutils.bdist_wheel]
51
44
  universal = true
@@ -55,7 +48,7 @@ line-length = 140
55
48
  exclude = ["*_ui.py", "mpv*"]
56
49
 
57
50
  [tool.bumpver]
58
- current_version = "9.2.1"
51
+ current_version = "9.2.2"
59
52
  version_pattern = "MAJOR.MINOR.PATCH"
60
53
 
61
54
 
@@ -44,6 +44,8 @@ class Test_accurate_media_analysis(object):
44
44
  "resolution": "640x480",
45
45
  }
46
46
 
47
+
48
+ """
47
49
  def test_no_media(self):
48
50
  r = utilities.accurate_media_analysis("ffmpeg", "files/test.boris")
49
51
  assert "error" in r
@@ -358,10 +360,14 @@ class Test_get_current_states_by_subject(object):
358
360
 
359
361
 
360
362
  """
363
+
364
+
361
365
  class Test_get_ip_address(object):
362
366
  def test_1(self):
363
367
  print(utilities.get_ip_address())
364
368
  assert False
369
+
370
+
365
371
  """
366
372
 
367
373
 
@@ -587,3 +593,4 @@ class Test_versiontuple(object):
587
593
  def test_4(self):
588
594
  r = utilities.versiontuple("")
589
595
  assert r == ("00000000",)
596
+ """
@@ -0,0 +1,124 @@
1
+ import sys
2
+ import os
3
+ import pytest
4
+ import subprocess
5
+ from unittest.mock import patch, MagicMock
6
+ import logging
7
+
8
+ # Assuming the function is imported from a module named `media_analysis`
9
+ # from media_analysis import accurate_media_analysis
10
+
11
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
12
+
13
+
14
+ from boris import utilities
15
+ from boris import config
16
+
17
+
18
+ # Mocking the ffprobe_media_analysis function
19
+ def mock_ffprobe_media_analysis(ffmpeg_bin, file_name):
20
+ return {
21
+ "duration": 120,
22
+ "duration_ms": 120000,
23
+ "bitrate": 5000,
24
+ "frames_number": 3000,
25
+ "fps": 25,
26
+ "has_video": True,
27
+ "has_audio": True,
28
+ }
29
+
30
+
31
+ # Mocking the time2seconds function
32
+ def mock_time2seconds(time_str):
33
+ return 120
34
+
35
+
36
+ # Mocking the dec function
37
+ def mock_dec(value):
38
+ return float(value)
39
+
40
+
41
+ @patch("utilities.ffprobe_media_analysis", side_effect=mock_ffprobe_media_analysis)
42
+ @patch("utilities.time2seconds", side_effect=mock_time2seconds)
43
+ @patch("utilities.dec", side_effect=mock_dec)
44
+ def test_accurate_media_analysis_success(mock_dec, mock_time2seconds, mock_ffprobe_media_analysis):
45
+ ffmpeg_bin = "/path/to/ffmpeg"
46
+ file_name = "test_video.mp4"
47
+
48
+ result = utilities.accurate_media_analysis(ffmpeg_bin, file_name)
49
+
50
+ assert result["duration"] == 120
51
+ assert result["duration_ms"] == 120000
52
+ assert result["bitrate"] == 5000
53
+ assert result["frames_number"] == 3000
54
+ assert result["fps"] == 25
55
+ assert result["has_video"] is True
56
+ assert result["has_audio"] is True
57
+
58
+
59
+ """
60
+
61
+ @patch("utilities.ffprobe_media_analysis", return_value={"error": "ffprobe failed"})
62
+ @patch("utilities.time2seconds", side_effect=mock_time2seconds)
63
+ @patch("utilities.dec", side_effect=mock_dec)
64
+ @patch("subprocess.Popen")
65
+ def test_accurate_media_analysis_fallback(mock_popen, mock_dec, mock_time2seconds, mock_ffprobe_media_analysis):
66
+ ffmpeg_bin = "/path/to/ffmpeg"
67
+ file_name = "test_video.mp4"
68
+
69
+ # Mocking the subprocess.Popen behavior
70
+ mock_process = MagicMock()
71
+ mock_process.communicate.return_value = (
72
+ b"",
73
+ b"Duration: 00:02:00.00, bitrate: 5000 kb/s\nStream #0:0: Video: h264, 1920x1080, 25 fps\nStream #0:1: Audio: aac",
74
+ )
75
+ mock_popen.return_value = mock_process
76
+
77
+ result = utilities.accurate_media_analysis(ffmpeg_bin, file_name)
78
+
79
+ assert result["duration"] == 120
80
+ assert result["duration_ms"] == 120000
81
+ assert result["bitrate"] == 5000000
82
+ assert result["frames_number"] == 3000
83
+ assert result["fps"] == 25
84
+ assert result["has_video"] is True
85
+ assert result["has_audio"] is True
86
+
87
+
88
+ @patch("utilities.ffprobe_media_analysis", return_value={"error": "ffprobe failed"})
89
+ @patch("utilities.time2seconds", side_effect=mock_time2seconds)
90
+ @patch("utilities.dec", side_effect=mock_dec)
91
+ @patch("subprocess.Popen")
92
+ def test_accurate_media_analysis_file_not_found(mock_popen, mock_dec, mock_time2seconds, mock_ffprobe_media_analysis):
93
+ ffmpeg_bin = "/path/to/ffmpeg"
94
+ file_name = "nonexistent_video.mp4"
95
+
96
+ # Mocking the subprocess.Popen behavior
97
+ mock_process = MagicMock()
98
+ mock_process.communicate.return_value = (b"", b"No such file or directory")
99
+ mock_popen.return_value = mock_process
100
+
101
+ result = utilities.accurate_media_analysis(ffmpeg_bin, file_name)
102
+
103
+ assert result["error"] == "No such file or directory"
104
+
105
+
106
+ @patch("utilities.ffprobe_media_analysis", return_value={"error": "ffprobe failed"})
107
+ @patch("utilities.time2seconds", side_effect=mock_time2seconds)
108
+ @patch("utilities.dec", side_effect=mock_dec)
109
+ @patch("subprocess.Popen")
110
+ def test_accurate_media_analysis_invalid_media(mock_popen, mock_dec, mock_time2seconds, mock_ffprobe_media_analysis):
111
+ ffmpeg_bin = "/path/to/ffmpeg"
112
+ file_name = "invalid_media.mp4"
113
+
114
+ # Mocking the subprocess.Popen behavior
115
+ mock_process = MagicMock()
116
+ mock_process.communicate.return_value = (b"", b"Invalid data found when processing input")
117
+ mock_popen.return_value = mock_process
118
+
119
+ result = utilities.accurate_media_analysis(ffmpeg_bin, file_name)
120
+
121
+ assert result["error"] == "This file does not seem to be a media file"
122
+
123
+
124
+ """