boris-behav-obs 9.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1039 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This file is part of BORIS.
7
+
8
+ BORIS is free software; you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation; either version 3 of the License, or
11
+ any later version.
12
+
13
+ BORIS is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program; if not see <http://www.gnu.org/licenses/>.
20
+
21
+ """
22
+
23
+ import logging
24
+ import os
25
+ import pathlib as pl
26
+ from decimal import Decimal as dec
27
+ from io import StringIO
28
+ import pandas as pd
29
+ import time
30
+
31
+ try:
32
+ import pyreadr
33
+
34
+ flag_pyreadr_loaded = True
35
+ except ModuleNotFoundError:
36
+ flag_pyreadr_loaded = False
37
+
38
+
39
+ import tablib
40
+ from PySide6.QtCore import Qt
41
+ from PySide6.QtWidgets import (
42
+ QFileDialog,
43
+ QHBoxLayout,
44
+ QInputDialog,
45
+ QLabel,
46
+ QListWidget,
47
+ QMessageBox,
48
+ QPushButton,
49
+ QSizePolicy,
50
+ QSpacerItem,
51
+ QTableWidget,
52
+ QTableWidgetItem,
53
+ QVBoxLayout,
54
+ QWidget,
55
+ QApplication,
56
+ )
57
+
58
+ from . import config as cfg
59
+ from . import (
60
+ db_functions,
61
+ dialog,
62
+ gui_utilities,
63
+ observation_operations,
64
+ project_functions,
65
+ select_observations,
66
+ select_subj_behav,
67
+ time_budget_functions,
68
+ )
69
+ from . import utilities as util
70
+
71
+
72
+ class timeBudgetResults(QWidget):
73
+ """
74
+ class for displaying time budget results in new window
75
+ a function for exporting data in TSV, CSV, XLS and ODS formats is implemented
76
+
77
+ Args:
78
+ pj (dict): BORIS project
79
+ """
80
+
81
+ def __init__(self, pj, config_param):
82
+ super().__init__()
83
+
84
+ self.pj = pj
85
+ self.config_param = config_param
86
+
87
+ hbox = QVBoxLayout(self)
88
+
89
+ self.label = QLabel("")
90
+ hbox.addWidget(self.label)
91
+
92
+ self.lw = QListWidget()
93
+ # self.lw.setEnabled(False)
94
+ self.lw.setMaximumHeight(100)
95
+ hbox.addWidget(self.lw)
96
+
97
+ self.lbTotalObservedTime = QLabel("")
98
+ hbox.addWidget(self.lbTotalObservedTime)
99
+
100
+ # behaviors excluded from total time
101
+ self.excluded_behaviors_list = QLabel("")
102
+ hbox.addWidget(self.excluded_behaviors_list)
103
+
104
+ self.twTB = QTableWidget()
105
+ hbox.addWidget(self.twTB)
106
+
107
+ hbox2 = QHBoxLayout()
108
+
109
+ self.pbSave = QPushButton("Save results", clicked=self.pbSave_clicked)
110
+ hbox2.addWidget(self.pbSave)
111
+
112
+ spacerItem = QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
113
+ hbox2.addItem(spacerItem)
114
+
115
+ self.pbClose = QPushButton(cfg.CLOSE, clicked=self.close_clicked)
116
+ hbox2.addWidget(self.pbClose)
117
+
118
+ hbox.addLayout(hbox2)
119
+
120
+ self.setWindowTitle("Time budget")
121
+
122
+ def close_clicked(self):
123
+ """
124
+ save geometry of widget and close it
125
+ """
126
+ gui_utilities.save_geometry(self, "time budget")
127
+ self.close()
128
+
129
+ def pbSave_clicked(self):
130
+ """
131
+ save time budget analysis results in TSV, CSV, ODS, XLS format
132
+ """
133
+
134
+ def complete(lst: list, max_: int) -> list:
135
+ """
136
+ complete list with empty string until len = max
137
+
138
+ Args:
139
+ lst (list): list to complete
140
+ max_ (int): length of the returned list
141
+
142
+ Returns:
143
+ list: completed list
144
+ """
145
+
146
+ lst.extend([""] * (max_ - len(lst)))
147
+ return lst
148
+
149
+ logging.debug("save time budget results to file")
150
+
151
+ file_formats = (cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.XLS, cfg.HTML, cfg.TEXT_FILE, cfg.PANDAS_DF, cfg.RDS)
152
+
153
+ file_name, filter_ = QFileDialog().getSaveFileName(self, "Save Time budget analysis", "", ";;".join(file_formats))
154
+
155
+ if not file_name:
156
+ return
157
+
158
+ # add correct file extension if not present
159
+ if pl.Path(file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
160
+ if cfg.FILE_NAME_SUFFIX[filter_] != "cli":
161
+ file_name = str(pl.Path(file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
162
+ else:
163
+ file_name = str(pl.Path(file_name))
164
+ # check if file with new extension already exists
165
+ if pl.Path(file_name).is_file():
166
+ if (
167
+ dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
168
+ == cfg.CANCEL
169
+ ):
170
+ return
171
+
172
+ rows: list = []
173
+
174
+ header: list = ["Observation id", "Observation date", "Description"]
175
+ # indep var labels
176
+ header.extend([self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in self.pj[cfg.INDEPENDENT_VARIABLES]])
177
+ header.extend(["Time budget start", "Time budget stop", "Time budget duration"])
178
+
179
+ for idx in range(self.twTB.columnCount()):
180
+ header.append(self.twTB.horizontalHeaderItem(idx).text())
181
+ rows.append(header)
182
+
183
+ col1: list = []
184
+ # add obs id
185
+ if self.lw.count() == 1:
186
+ col1.append(self.lw.item(0).text())
187
+ else:
188
+ col1.append("NA, observations grouped")
189
+
190
+ # add obs date
191
+ if self.lw.count() == 1:
192
+ col1.append(self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get("date", "").replace("T", " "))
193
+ else:
194
+ # TODO: check if date is the same for all selected obs
195
+ col1.append("NA, observations grouped")
196
+
197
+ # description
198
+ if self.lw.count() == 1:
199
+ col1.append(util.eol2space(self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get(cfg.DESCRIPTION, "")))
200
+ else:
201
+ col1.append("NA, observations grouped")
202
+
203
+ # indep var values
204
+ for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
205
+ if self.lw.count() == 1:
206
+ # var has value in obs?
207
+ if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] in self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()].get(
208
+ cfg.INDEPENDENT_VARIABLES, []
209
+ ):
210
+ col1.append(
211
+ self.pj[cfg.OBSERVATIONS][self.lw.item(0).text()][cfg.INDEPENDENT_VARIABLES][
212
+ self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]
213
+ ]
214
+ )
215
+ else:
216
+ col1.append("")
217
+ else:
218
+ # TODO: check if var value is the same for all selected obs
219
+ col1.append("NA, observations grouped")
220
+
221
+ if self.time_interval == cfg.TIME_ARBITRARY_INTERVAL:
222
+ col1.extend([f"{self.min_time:0.3f}", f"{self.max_time:0.3f}", f"{self.max_time - self.min_time:0.3f}"])
223
+
224
+ if self.time_interval == cfg.TIME_FULL_OBS:
225
+ col1.extend(["Full observation", "Full observation", "Full observation"])
226
+
227
+ if self.time_interval == cfg.TIME_EVENTS:
228
+ col1.extend(["Limited to coded events", "Limited to coded events", "Limited to coded events"])
229
+
230
+ for row_idx in range(self.twTB.rowCount()):
231
+ values = []
232
+ for col_idx in range(self.twTB.columnCount()):
233
+ values.append(util.intfloatstr(self.twTB.item(row_idx, col_idx).text()))
234
+ rows.append(col1 + values)
235
+
236
+ """
237
+ else:
238
+ # observations list
239
+ obs_header = ["Observation id", "Observation date", "Description"]
240
+
241
+ # indep var
242
+ obs_header.extend(
243
+ [self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"] for idx in self.pj[cfg.INDEPENDENT_VARIABLES]]
244
+ )
245
+
246
+ obs_header.extend(["Time budget start", "Time budget stop", "Time budget duration"])
247
+
248
+ obs_rows = []
249
+ obs_rows.append(obs_header)
250
+ for idx in range(self.lw.count()):
251
+ row = []
252
+ # obs id
253
+ row.append(self.lw.item(idx).text())
254
+ row.append(self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get("date", ""))
255
+ row.append(util.eol2space(self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get(cfg.DESCRIPTION, "")))
256
+
257
+ for idx2 in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
258
+ # var has value in obs?
259
+ if self.pj[cfg.INDEPENDENT_VARIABLES][idx2]["label"] in self.pj[cfg.OBSERVATIONS][
260
+ self.lw.item(idx).text()
261
+ ].get(cfg.INDEPENDENT_VARIABLES, []):
262
+ row.append(
263
+ self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()][cfg.INDEPENDENT_VARIABLES][
264
+ self.pj[cfg.INDEPENDENT_VARIABLES][idx2]["label"]
265
+ ]
266
+ )
267
+ else:
268
+ row.append("")
269
+ # start stop duration
270
+ row.extend([f"{self.min_time:0.3f}", f"{self.max_time:0.3f}", f"{self.max_time - self.min_time:0.3f}"])
271
+
272
+ obs_rows.append(row)
273
+
274
+ # write file with observations information
275
+ data = tablib.Dataset()
276
+ data.title = "Time budget - Observations information"
277
+
278
+ for row in obs_rows:
279
+ data.append(complete(row, max([len(r) for r in obs_rows])))
280
+
281
+ with open(pl.Path(file_name).with_suffix(f".observations_info.{cfg.FILE_NAME_SUFFIX[filter_]}"), "wb") as f:
282
+ if filter_ in [cfg.TSV, cfg.CSV, cfg.HTML]:
283
+ f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[filter_])))
284
+ if filter_ in [cfg.ODS, cfg.XLSX, cfg.XLS]:
285
+ f.write(data.export(cfg.FILE_NAME_SUFFIX[filter_]))
286
+ """
287
+
288
+ """
289
+ rows.append(["Observations:"])
290
+ for idx in range(self.lw.count()):
291
+ rows.append([""])
292
+ rows.append(["Observation id", self.lw.item(idx).text()])
293
+ rows.append(["Observation date", self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get("date", "")])
294
+ rows.append(
295
+ [
296
+ "Description",
297
+ util.eol2space(self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()].get(cfg.DESCRIPTION, "")),
298
+ ]
299
+ )
300
+
301
+ if cfg.INDEPENDENT_VARIABLES in self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()]:
302
+ rows.append(["Independent variables:"])
303
+ for var in self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()][cfg.INDEPENDENT_VARIABLES]:
304
+ rows.append(
305
+ [var, self.pj[cfg.OBSERVATIONS][self.lw.item(idx).text()][cfg.INDEPENDENT_VARIABLES][var]]
306
+ )
307
+
308
+ if self.excluded_behaviors_list.text():
309
+ s1, s2 = self.excluded_behaviors_list.text().split(": ")
310
+ rows.extend([[""], [s1] + s2.split(", ")])
311
+
312
+ rows.extend([[""], [""], ["Time budget:"]])
313
+
314
+
315
+ # write header
316
+
317
+ header = [self.twTB.horizontalHeaderItem(col_idx).text() for col_idx in range(self.twTB.columnCount())]
318
+ rows.append(header)
319
+
320
+ for row in range(self.twTB.rowCount()):
321
+ values = []
322
+ for col_idx in range(self.twTB.columnCount()):
323
+ values.append(util.intfloatstr(self.twTB.item(row, col_idx).text()))
324
+ rows.append(values)
325
+ """
326
+
327
+ data = tablib.Dataset()
328
+ data.title = "Time budget"
329
+
330
+ for row in rows:
331
+ data.append(complete(row, max([len(r) for r in rows])))
332
+
333
+ if filter_ in (cfg.PANDAS_DF, cfg.RDS):
334
+ pass
335
+
336
+ # build pandas dataframe from the tsv export of tablib dataset
337
+ dtype = {
338
+ "Observation id": str,
339
+ "Observation date": str,
340
+ "Description": str,
341
+ "Time budget start": str,
342
+ "Time budget stop": str,
343
+ "Time budget duration": str,
344
+ "Subject": str,
345
+ "Behavior": str,
346
+ "Modifiers": str,
347
+ "Total number of occurences": float,
348
+ "Total duration (s)": float,
349
+ "Duration mean (s)": float,
350
+ "Duration std dev": float,
351
+ "inter-event intervals mean (s)": float,
352
+ "inter-event intervals std dev": float,
353
+ "% of total length ": float,
354
+ }
355
+
356
+ # indep var values
357
+ for idx in self.pj.get(cfg.INDEPENDENT_VARIABLES, []):
358
+ if self.pj[cfg.INDEPENDENT_VARIABLES][idx]["type"] == "numeric":
359
+ dtype[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = float
360
+ else:
361
+ dtype[self.pj[cfg.INDEPENDENT_VARIABLES][idx]["label"]] = str
362
+
363
+ df = pd.read_csv(
364
+ StringIO(data.export("tsv")),
365
+ sep="\t",
366
+ dtype=dtype,
367
+ parse_dates=[1],
368
+ )
369
+
370
+ if filter_ == cfg.PANDAS_DF:
371
+ df.to_pickle(file_name)
372
+
373
+ if flag_pyreadr_loaded and filter_ == cfg.RDS:
374
+ pyreadr.write_rds(file_name, df)
375
+
376
+ return
377
+
378
+ # write results
379
+ with open(file_name, "wb") as f:
380
+ if filter_ in (cfg.TSV, cfg.CSV, cfg.HTML, cfg.TEXT_FILE):
381
+ f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[filter_])))
382
+ if filter_ in (cfg.ODS, cfg.XLSX, cfg.XLS):
383
+ f.write(data.export(cfg.FILE_NAME_SUFFIX[filter_]))
384
+
385
+
386
+ def time_budget(self, mode: str, mode2: str = "list"):
387
+ """
388
+ time budget (by behavior or category)
389
+
390
+ Args:
391
+ mode (str): ["by_behavior", "by_category"]
392
+ mode2 (str): must be in ["list", "current"]
393
+ "current" time budget of current observation
394
+ """
395
+
396
+ if mode2 == "current":
397
+ if self.observationId:
398
+ selected_observations = [self.observationId]
399
+ else:
400
+ return
401
+
402
+ if mode2 == "list":
403
+ _, selected_observations = select_observations.select_observations2(self, mode=cfg.MULTIPLE, windows_title="")
404
+
405
+ if not selected_observations:
406
+ return
407
+
408
+ # check if coded behaviors are defined in ethogram
409
+ if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
410
+ return
411
+
412
+ # check if state events are paired
413
+ not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
414
+ if not_ok or not selected_observations:
415
+ return
416
+
417
+ flagGroup: bool = False
418
+ if len(selected_observations) > 1:
419
+ flagGroup = (
420
+ dialog.MessageDialog(cfg.programName, "Group the selected observations in a single time budget analysis?", [cfg.YES, cfg.NO])
421
+ == cfg.YES
422
+ )
423
+
424
+ max_media_duration_all_obs, total_media_duration_all_obs = observation_operations.media_duration(
425
+ self.pj[cfg.OBSERVATIONS], selected_observations
426
+ )
427
+
428
+ logging.debug(f"max_media_duration_all_obs: {max_media_duration_all_obs}, total_media_duration_all_obs={total_media_duration_all_obs}")
429
+
430
+ start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
431
+
432
+ start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
433
+
434
+ parameters: dict = select_subj_behav.choose_obs_subj_behav_category(
435
+ self,
436
+ selected_observations,
437
+ start_coding=start_coding,
438
+ end_coding=end_coding,
439
+ # start_interval=start_interval,
440
+ # end_interval=end_interval,
441
+ start_interval=None,
442
+ end_interval=None,
443
+ maxTime=max_media_duration_all_obs,
444
+ by_category=(mode == "by_category"),
445
+ n_observations=len(selected_observations),
446
+ show_exclude_non_coded_modifiers=True,
447
+ )
448
+ if parameters == {}:
449
+ return
450
+
451
+ if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
452
+ QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
453
+ return
454
+
455
+ logging.debug(f"{parameters=}")
456
+
457
+ # ask for excluding behaviors durations from total time
458
+ if start_coding is not None and not start_coding.is_nan():
459
+ cancel_pressed, parameters[cfg.EXCLUDED_BEHAVIORS] = self.filter_behaviors(
460
+ title="Select behaviors to exclude from the total time",
461
+ text=("The duration of the selected behaviors will be subtracted from the total time"),
462
+ table="",
463
+ behavior_type=cfg.STATE_EVENT_TYPES,
464
+ )
465
+ if cancel_pressed:
466
+ return
467
+ else:
468
+ parameters[cfg.EXCLUDED_BEHAVIORS] = []
469
+
470
+ self.statusbar.showMessage(f"Generating time budget for {len(selected_observations)} observation(s)")
471
+ QApplication.processEvents()
472
+
473
+ # check if time_budget window must be used
474
+ if flagGroup or len(selected_observations) == 1:
475
+ t0 = time.time()
476
+
477
+ cursor = db_functions.load_events_in_db(
478
+ self.pj,
479
+ parameters[cfg.SELECTED_SUBJECTS],
480
+ selected_observations,
481
+ parameters[cfg.SELECTED_BEHAVIORS],
482
+ time_interval=cfg.TIME_FULL_OBS,
483
+ )
484
+
485
+ total_observation_time = 0
486
+ for obsId in selected_observations:
487
+ obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
488
+
489
+ if obs_length == dec(-1): # media length not available
490
+ parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
491
+
492
+ if obs_length == dec(-2): # images obs without time
493
+ parameters[cfg.TIME_INTERVAL] = cfg.TIME_EVENTS
494
+
495
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS: # media file duration
496
+ min_time = float(0)
497
+ # check if the last event is recorded after media file length
498
+ try:
499
+ if float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0]) > float(obs_length):
500
+ max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0])
501
+ else:
502
+ max_time = float(obs_length)
503
+ except Exception:
504
+ max_time = float(obs_length)
505
+
506
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_OBS_INTERVAL:
507
+ obs_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
508
+ offset = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
509
+ min_time = float(obs_interval[0]) + offset
510
+ # Use max media duration for max time if no interval is defined (=0)
511
+ max_time = float(obs_interval[1]) + offset if obs_interval[1] != 0 else float(obs_length)
512
+
513
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS: # events duration
514
+ try:
515
+ min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0]) # first event
516
+ except Exception:
517
+ min_time = float(0)
518
+ try:
519
+ max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0]) # last event
520
+ except Exception:
521
+ # TODO: set to 0 if no events ?
522
+ max_time = float(obs_length)
523
+
524
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
525
+ min_time = float(parameters[cfg.START_TIME])
526
+ max_time = float(parameters[cfg.END_TIME])
527
+
528
+ # check intervals
529
+ for subj in parameters[cfg.SELECTED_SUBJECTS]:
530
+ for behav in parameters[cfg.SELECTED_BEHAVIORS]:
531
+ # if cfg.POINT in self.eventType(behav).upper():
532
+ if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
533
+ continue
534
+
535
+ # extract modifiers
536
+
537
+ cursor.execute(
538
+ "SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
539
+ (obsId, subj, behav),
540
+ )
541
+ distinct_modifiers = list(cursor.fetchall())
542
+
543
+ # logging.debug("distinct_modifiers: {}".format(distinct_modifiers))
544
+
545
+ for modifier in distinct_modifiers:
546
+ # logging.debug("modifier #{}#".format(modifier[0]))
547
+
548
+ # insert events at boundaries of time interval
549
+ if (
550
+ len(
551
+ cursor.execute(
552
+ (
553
+ "SELECT * FROM events "
554
+ "WHERE observation = ? AND subject = ? AND code = ? AND modifiers = ? "
555
+ "AND occurence < ?"
556
+ ),
557
+ (obsId, subj, behav, modifier[0], min_time),
558
+ ).fetchall()
559
+ )
560
+ % 2
561
+ ):
562
+ cursor.execute(
563
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
564
+ (obsId, subj, behav, "STATE", modifier[0], min_time),
565
+ )
566
+
567
+ if (
568
+ len(
569
+ cursor.execute(
570
+ (
571
+ "SELECT * FROM events WHERE observation = ? AND subject = ? AND code = ? "
572
+ "AND modifiers = ? AND occurence > ?"
573
+ ),
574
+ (obsId, subj, behav, modifier[0], max_time),
575
+ ).fetchall()
576
+ )
577
+ % 2
578
+ ):
579
+ cursor.execute(
580
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
581
+ (obsId, subj, behav, "STATE", modifier[0], max_time),
582
+ )
583
+ try:
584
+ cursor.execute("COMMIT")
585
+ except Exception:
586
+ pass
587
+
588
+ total_observation_time += max_time - min_time
589
+
590
+ # delete all events out of time interval from db
591
+ cursor.execute(
592
+ "DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
593
+ (obsId, min_time, max_time),
594
+ )
595
+ try:
596
+ cursor.execute("COMMIT")
597
+ except Exception:
598
+ pass
599
+
600
+ out, categories = time_budget_functions.time_budget_analysis(
601
+ self.pj[cfg.ETHOGRAM], cursor, selected_observations, parameters, by_category=(mode == "by_category")
602
+ )
603
+
604
+ # check excluded behaviors
605
+ excl_behaviors_total_time = {}
606
+ for element in out:
607
+ if element["subject"] not in excl_behaviors_total_time:
608
+ excl_behaviors_total_time[element["subject"]] = 0
609
+ if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
610
+ excl_behaviors_total_time[element["subject"]] += element["duration"] if not isinstance(element["duration"], str) else 0
611
+
612
+ # widget for results visualization
613
+ self.tb = timeBudgetResults(self.pj, self.config_param)
614
+
615
+ # add min and max time
616
+ self.tb.time_interval = parameters[cfg.TIME_INTERVAL]
617
+ self.tb.min_time = min_time
618
+ self.tb.max_time = max_time
619
+
620
+ # observations list
621
+ self.tb.label.setText("Selected observations")
622
+ for obs_id in selected_observations:
623
+ # self.tb.lw.addItem(f"{obs_id} {self.pj[OBSERVATIONS][obs_id]['date']} {self.pj[OBSERVATIONS][obs_id]['description']}")
624
+ self.tb.lw.addItem(obs_id)
625
+
626
+ # media length
627
+ if len(selected_observations) > 1:
628
+ if total_observation_time:
629
+ if self.timeFormat == cfg.HHMMSS:
630
+ self.tb.lbTotalObservedTime.setText(f"Total observation length: {util.seconds2time(total_observation_time)}")
631
+ if self.timeFormat == cfg.S:
632
+ self.tb.lbTotalObservedTime.setText(f"Total observation length: {float(total_observation_time):0.3f}")
633
+ else:
634
+ self.tb.lbTotalObservedTime.setText("Total observation length: not available")
635
+ else:
636
+ if self.timeFormat == cfg.HHMMSS:
637
+ self.tb.lbTotalObservedTime.setText(f"Analysis from {util.seconds2time(min_time)} to {util.seconds2time(max_time)}")
638
+ if self.timeFormat == cfg.S:
639
+ self.tb.lbTotalObservedTime.setText(f"Analysis from {float(min_time):0.3f} to {float(max_time):0.3f} s")
640
+
641
+ # behaviors excluded from total time
642
+ if parameters[cfg.EXCLUDED_BEHAVIORS]:
643
+ self.tb.excluded_behaviors_list.setText(
644
+ "Behaviors excluded from total time: " + (", ".join(parameters[cfg.EXCLUDED_BEHAVIORS]))
645
+ )
646
+ else:
647
+ self.tb.excluded_behaviors_list.setVisible(False)
648
+
649
+ self.statusbar.showMessage(f"Time budget generated in {round(time.time() - t0, 3)} s", 5000)
650
+ logging.debug("Time budget generated")
651
+
652
+ if mode == "by_behavior":
653
+ tb_fields = [
654
+ "Subject",
655
+ "Behavior",
656
+ "Modifiers",
657
+ "Total number of occurences",
658
+ "Total duration (s)",
659
+ "Duration mean (s)",
660
+ "Duration std dev",
661
+ "inter-event intervals mean (s)",
662
+ "inter-event intervals std dev",
663
+ "% of total length",
664
+ ]
665
+ fields = [
666
+ "subject",
667
+ "behavior",
668
+ "modifiers",
669
+ "number",
670
+ "duration",
671
+ "duration_mean",
672
+ "duration_stdev",
673
+ "inter_duration_mean",
674
+ "inter_duration_stdev",
675
+ ]
676
+
677
+ self.tb.twTB.setColumnCount(len(tb_fields))
678
+ self.tb.twTB.setHorizontalHeaderLabels(tb_fields)
679
+
680
+ for row in out:
681
+ self.tb.twTB.setRowCount(self.tb.twTB.rowCount() + 1)
682
+ column = 0
683
+ for field in fields:
684
+ if isinstance(row[field], float):
685
+ item = QTableWidgetItem(f"{row[field]:.3f}")
686
+ else:
687
+ item = QTableWidgetItem(str(row[field]).replace(" ()", ""))
688
+ # no modif allowed
689
+ item.setFlags(Qt.ItemIsEnabled)
690
+ self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
691
+ column += 1
692
+
693
+ # % of total time
694
+ if row["duration"] in (0, cfg.NA):
695
+ item = QTableWidgetItem(str(row["duration"]))
696
+ elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
697
+ tot_time = float(total_observation_time)
698
+ # substract time of excluded behaviors from the total for the subject
699
+ if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
700
+ tot_time -= excl_behaviors_total_time[row["subject"]]
701
+ item = QTableWidgetItem(f"{row['duration'] / tot_time * 100:.1f}" if tot_time > 0 else cfg.NA)
702
+
703
+ else:
704
+ item = QTableWidgetItem("-")
705
+
706
+ item.setFlags(Qt.ItemIsEnabled)
707
+ self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
708
+
709
+ if mode == "by_category":
710
+ tb_fields = ["Subject", "Category", "Total number", "Total duration (s)"]
711
+ fields = ["number", "duration"]
712
+
713
+ self.tb.twTB.setColumnCount(len(tb_fields))
714
+ self.tb.twTB.setHorizontalHeaderLabels(tb_fields)
715
+
716
+ for subject in categories:
717
+ for category in categories[subject]:
718
+ self.tb.twTB.setRowCount(self.tb.twTB.rowCount() + 1)
719
+
720
+ column = 0
721
+ item = QTableWidgetItem(subject)
722
+ item.setFlags(Qt.ItemIsEnabled)
723
+ self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
724
+
725
+ column = 1
726
+ if category == "":
727
+ item = QTableWidgetItem("No category")
728
+ else:
729
+ item = QTableWidgetItem(category)
730
+ item.setFlags(Qt.ItemIsEnabled)
731
+ self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
732
+
733
+ for field in fields:
734
+ column += 1
735
+
736
+ if field == "duration":
737
+ try:
738
+ item = QTableWidgetItem(f"{categories[subject][category][field]:0.3f}")
739
+ except Exception:
740
+ item = QTableWidgetItem(categories[subject][category][field])
741
+ else:
742
+ item = QTableWidgetItem(str(categories[subject][category][field]))
743
+ item.setFlags(Qt.ItemIsEnabled)
744
+ item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
745
+ self.tb.twTB.setItem(self.tb.twTB.rowCount() - 1, column, item)
746
+
747
+ self.tb.twTB.resizeColumnsToContents()
748
+
749
+ gui_utilities.restore_geometry(self.tb, "time budget", (800, 600))
750
+
751
+ self.tb.show()
752
+
753
+ if not flagGroup and len(selected_observations) > 1:
754
+ output_format, ok = QInputDialog.getItem(
755
+ self,
756
+ "Time budget analysis format",
757
+ "Available formats",
758
+ (
759
+ cfg.TSV,
760
+ cfg.CSV,
761
+ cfg.ODS,
762
+ cfg.ODS_WB,
763
+ cfg.XLSX,
764
+ cfg.XLSX_WB,
765
+ cfg.HTML,
766
+ cfg.XLS,
767
+ ),
768
+ 0,
769
+ False,
770
+ )
771
+ if not ok:
772
+ return
773
+
774
+ extension = cfg.FILE_NAME_SUFFIX[output_format]
775
+
776
+ if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
777
+ workbook = tablib.Databook()
778
+
779
+ wb_file_name, filter_ = QFileDialog(self).getSaveFileName(self, "Save Time budget analysis", "", output_format)
780
+ if not wb_file_name:
781
+ return
782
+
783
+ if pl.Path(wb_file_name).suffix != f".{cfg.FILE_NAME_SUFFIX[filter_]}":
784
+ wb_file_name = str(pl.Path(wb_file_name)) + "." + cfg.FILE_NAME_SUFFIX[filter_]
785
+ # check if file with new extension already exists
786
+ if pl.Path(wb_file_name).is_file():
787
+ if (
788
+ dialog.MessageDialog(cfg.programName, f"The file {wb_file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
789
+ == cfg.CANCEL
790
+ ):
791
+ return
792
+
793
+ else: # not workbook
794
+ exportDir = QFileDialog(self).getExistingDirectory(
795
+ self,
796
+ "Choose a directory to save the time budget analysis",
797
+ os.path.expanduser("~"),
798
+ options=QFileDialog.ShowDirsOnly,
799
+ )
800
+ if not exportDir:
801
+ return
802
+
803
+ if mode == "by_behavior":
804
+ tb_fields = [
805
+ "Subject",
806
+ "Behavior",
807
+ "Modifiers",
808
+ "Total number of occurences",
809
+ "Total duration (s)",
810
+ "Duration mean (s)",
811
+ "Duration std dev",
812
+ "inter-event intervals mean (s)",
813
+ "inter-event intervals std dev",
814
+ "% of total length",
815
+ ]
816
+ fields = [
817
+ "subject",
818
+ "behavior",
819
+ "modifiers",
820
+ "number",
821
+ "duration",
822
+ "duration_mean",
823
+ "duration_stdev",
824
+ "inter_duration_mean",
825
+ "inter_duration_stdev",
826
+ ]
827
+
828
+ if mode == "by_category":
829
+ tb_fields = ["Subject", "Category", "Total number of occurences", "Total duration (s)"]
830
+ fields = ["subject", "category", "number", "duration"]
831
+
832
+ mem_command = ""
833
+ for obsId in selected_observations:
834
+ cursor = db_functions.load_events_in_db(self.pj, parameters[cfg.SELECTED_SUBJECTS], [obsId], parameters[cfg.SELECTED_BEHAVIORS])
835
+
836
+ obs_length = observation_operations.observation_total_length(self.pj[cfg.OBSERVATIONS][obsId])
837
+
838
+ if obs_length == -1:
839
+ obs_length = 0
840
+
841
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_FULL_OBS:
842
+ min_time = float(0)
843
+ # check if the last event is recorded after media file length
844
+ try:
845
+ if float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0]) > float(obs_length):
846
+ max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0])
847
+ else:
848
+ max_time = float(obs_length)
849
+ except Exception:
850
+ max_time = float(obs_length)
851
+
852
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_EVENTS:
853
+ try:
854
+ min_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][0][0])
855
+ except Exception:
856
+ min_time = float(0)
857
+ try:
858
+ max_time = float(self.pj[cfg.OBSERVATIONS][obsId][cfg.EVENTS][-1][0])
859
+ except Exception:
860
+ max_time = float(obs_length)
861
+
862
+ if parameters[cfg.TIME_INTERVAL] == cfg.TIME_ARBITRARY_INTERVAL:
863
+ min_time = float(parameters[cfg.START_TIME])
864
+ max_time = float(parameters[cfg.END_TIME])
865
+
866
+ # check intervals
867
+ for subj in parameters[cfg.SELECTED_SUBJECTS]:
868
+ for behav in parameters[cfg.SELECTED_BEHAVIORS]:
869
+ if project_functions.event_type(behav, self.pj[cfg.ETHOGRAM]) in cfg.POINT_EVENT_TYPES:
870
+ continue
871
+
872
+ cursor.execute(
873
+ "SELECT distinct modifiers FROM events WHERE observation = ? AND subject = ? AND code = ?",
874
+ (obsId, subj, behav),
875
+ )
876
+ distinct_modifiers = list(cursor.fetchall())
877
+
878
+ for modifier in distinct_modifiers:
879
+ if (
880
+ len(
881
+ cursor.execute(
882
+ (
883
+ "SELECT * FROM events "
884
+ "WHERE observation = ? AND subject = ? "
885
+ "AND code = ? AND modifiers = ? AND occurence < ?"
886
+ ),
887
+ (obsId, subj, behav, modifier[0], min_time),
888
+ ).fetchall()
889
+ )
890
+ % 2
891
+ ):
892
+ cursor.execute(
893
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
894
+ (obsId, subj, behav, "STATE", modifier[0], min_time),
895
+ )
896
+ if (
897
+ len(
898
+ cursor.execute(
899
+ (
900
+ "SELECT * FROM events WHERE observation = ? AND subject = ? AND code = ?"
901
+ " AND modifiers = ? AND occurence > ?"
902
+ ),
903
+ (obsId, subj, behav, modifier[0], max_time),
904
+ ).fetchall()
905
+ )
906
+ % 2
907
+ ):
908
+ cursor.execute(
909
+ ("INSERT INTO events (observation, subject, code, type, modifiers, occurence) VALUES (?,?,?,?,?,?)"),
910
+ (obsId, subj, behav, cfg.STATE, modifier[0], max_time),
911
+ )
912
+ try:
913
+ cursor.execute("COMMIT")
914
+ except Exception:
915
+ pass
916
+
917
+ cursor.execute(
918
+ "DELETE FROM events WHERE observation = ? AND (occurence < ? OR occurence > ?)",
919
+ (obsId, min_time, max_time),
920
+ )
921
+
922
+ out, categories = time_budget_functions.time_budget_analysis(
923
+ self.pj[cfg.ETHOGRAM], cursor, [obsId], parameters, by_category=(mode == "by_category")
924
+ )
925
+
926
+ # check excluded behaviors
927
+ excl_behaviors_total_time = {}
928
+ for element in out:
929
+ if element["subject"] not in excl_behaviors_total_time:
930
+ excl_behaviors_total_time[element["subject"]] = 0
931
+ if element["behavior"] in parameters[cfg.EXCLUDED_BEHAVIORS]:
932
+ excl_behaviors_total_time[element["subject"]] += element["duration"] if element["duration"] != "NA" else 0
933
+
934
+ rows: list = []
935
+ col1: list = []
936
+ # observation id
937
+ col1.append(obsId)
938
+ col1.append(self.pj[cfg.OBSERVATIONS][obsId].get("date", "").replace("T", ""))
939
+ col1.append(util.eol2space(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DESCRIPTION, "")))
940
+ header = ["Observation id", "Observation date", "Description"]
941
+
942
+ indep_var_label: list = []
943
+ indep_var_values: list = []
944
+ for _, v in self.pj.get(cfg.INDEPENDENT_VARIABLES, {}).items():
945
+ indep_var_label.append(v["label"])
946
+ indep_var_values.append(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.INDEPENDENT_VARIABLES, {}).get(v["label"], ""))
947
+
948
+ header.extend(indep_var_label)
949
+ col1.extend(indep_var_values)
950
+
951
+ # interval analysis
952
+ if dec(min_time).is_nan(): # check if observation has timestamp
953
+ col1.extend([cfg.NA, cfg.NA, cfg.NA])
954
+ else:
955
+ col1.extend([f"{min_time:0.3f}", f"{max_time:0.3f}", f"{max_time - min_time:0.3f}"])
956
+ header.extend(["Time budget start", "Time budget stop", "Time budget duration"])
957
+
958
+ if mode == "by_behavior":
959
+ # header
960
+ rows.append(header + tb_fields)
961
+
962
+ for row in out:
963
+ values = []
964
+ for field in fields:
965
+ values.append(str(row[field]).replace(" ()", ""))
966
+ # % of total time
967
+ if row["duration"] in (0, cfg.NA):
968
+ values.append(row["duration"])
969
+ elif row["duration"] not in ("-", cfg.UNPAIRED) and not start_coding.is_nan():
970
+ tot_time = float(max_time - min_time)
971
+ # substract duration of excluded behaviors from total time for each subject
972
+ if row["subject"] in excl_behaviors_total_time and row["behavior"] not in parameters[cfg.EXCLUDED_BEHAVIORS]:
973
+ tot_time -= excl_behaviors_total_time[row["subject"]]
974
+ # % of tot time
975
+ values.append(round(row["duration"] / tot_time * 100, 1) if tot_time > 0 else cfg.NA)
976
+ else:
977
+ values.append("-")
978
+
979
+ rows.append(col1 + values)
980
+
981
+ if mode == "by_category":
982
+ rows.append(header + tb_fields)
983
+
984
+ for subject in categories:
985
+ for category in categories[subject]:
986
+ values = []
987
+ values.append(subject)
988
+ if category == "":
989
+ values.append("No category")
990
+ else:
991
+ values.append(category)
992
+
993
+ values.append(categories[subject][category]["number"])
994
+ try:
995
+ values.append(f"{categories[subject][category]['duration']:0.3f}")
996
+ except Exception:
997
+ values.append(categories[subject][category]["duration"])
998
+
999
+ rows.append(col1 + values)
1000
+
1001
+ data = tablib.Dataset()
1002
+ data.title = obsId
1003
+ for row in rows:
1004
+ data.append(util.complete(row, max([len(r) for r in rows])))
1005
+
1006
+ # check worksheet/workbook title for forbidden char (excel)
1007
+ data.title = util.safe_xl_worksheet_title(data.title, extension)
1008
+
1009
+ if output_format in (cfg.ODS_WB, cfg.XLSX_WB):
1010
+ workbook.add_sheet(data)
1011
+
1012
+ else:
1013
+ file_name = f"{pl.Path(exportDir) / pl.Path(util.safeFileName(obsId))}.{extension}"
1014
+ if mem_command != cfg.OVERWRITE_ALL and pl.Path(file_name).is_file():
1015
+ if mem_command == "Skip all":
1016
+ continue
1017
+ mem_command = dialog.MessageDialog(
1018
+ cfg.programName,
1019
+ f"The file {file_name} already exists.",
1020
+ [cfg.OVERWRITE, cfg.OVERWRITE_ALL, cfg.SKIP, cfg.SKIP_ALL, cfg.CANCEL],
1021
+ )
1022
+ if mem_command == cfg.CANCEL:
1023
+ return
1024
+ if mem_command in (cfg.SKIP, cfg.SKIP_ALL):
1025
+ continue
1026
+
1027
+ with open(file_name, "wb") as f:
1028
+ if output_format in (cfg.TSV, cfg.CSV, cfg.HTML):
1029
+ f.write(str.encode(data.export(cfg.FILE_NAME_SUFFIX[output_format])))
1030
+
1031
+ if output_format in (cfg.ODS, cfg.XLSX, cfg.XLS):
1032
+ f.write(data.export(cfg.FILE_NAME_SUFFIX[output_format]))
1033
+
1034
+ if output_format == cfg.XLSX_WB:
1035
+ with open(wb_file_name, "wb") as f:
1036
+ f.write(workbook.xlsx)
1037
+ if output_format == cfg.ODS_WB:
1038
+ with open(wb_file_name, "wb") as f:
1039
+ f.write(workbook.ods)