boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__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.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +28 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +42 -49
- boris/config.py +141 -65
- boris/config_file.py +58 -67
- boris/connections.py +107 -61
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2373 -1786
- boris/core_qrc.py +15895 -10743
- boris/core_ui.py +943 -798
- boris/db_functions.py +17 -42
- boris/dev.py +109 -8
- boris/dialog.py +482 -236
- boris/duration_widget.py +9 -14
- boris/edit_event.py +61 -31
- boris/edit_event_ui.py +208 -97
- boris/event_operations.py +408 -293
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +184 -223
- boris/export_observation.py +74 -100
- boris/external_processes.py +123 -98
- boris/geometric_measurement.py +644 -290
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +4 -4
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +20 -57
- boris/latency.py +31 -24
- boris/measurement_widget.py +14 -18
- boris/media_file.py +17 -19
- boris/menu_options.py +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +533 -221
- boris/observation_operations.py +1025 -390
- boris/observation_ui.py +572 -362
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -68
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +25 -33
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +306 -83
- boris/preferences_ui.py +684 -227
- boris/project.py +448 -293
- boris/project_functions.py +671 -238
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -198
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +52 -35
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +627 -236
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +95 -29
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/converters.ui +0 -289
- boris/core.qrc +0 -36
- boris/core.ui +0 -1556
- boris/edit_event.ui +0 -233
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -814
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.12.dist-info/METADATA +0 -128
- boris_behav_obs-8.12.dist-info/RECORD +0 -108
- boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/plugins.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import logging
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from PySide6.QtGui import QAction
|
|
29
|
+
from PySide6.QtWidgets import QMessageBox
|
|
30
|
+
|
|
31
|
+
from . import config as cfg
|
|
32
|
+
from . import project_functions
|
|
33
|
+
from . import dialog
|
|
34
|
+
from . import view_df
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_plugins_to_menu(self):
|
|
38
|
+
"""
|
|
39
|
+
add plugins to the plugins menu
|
|
40
|
+
"""
|
|
41
|
+
for plugin_name in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
|
|
42
|
+
logging.debug(f"adding plugin '{plugin_name}' to menu")
|
|
43
|
+
# Create an action for each submenu option
|
|
44
|
+
action = QAction(self, triggered=lambda checked=False, name=plugin_name: run_plugin(self, name))
|
|
45
|
+
action.setText(plugin_name)
|
|
46
|
+
|
|
47
|
+
self.menu_plugins.addAction(action)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_plugin_name(plugin_path: str) -> str | None:
|
|
51
|
+
"""
|
|
52
|
+
get name of a Python plugin
|
|
53
|
+
"""
|
|
54
|
+
# search plugin name
|
|
55
|
+
plugin_name: str | None = None
|
|
56
|
+
with open(plugin_path, "r") as f_in:
|
|
57
|
+
for line in f_in:
|
|
58
|
+
if line.startswith("__plugin_name__"):
|
|
59
|
+
plugin_name = line.split("=")[1].strip().replace('"', "")
|
|
60
|
+
break
|
|
61
|
+
return plugin_name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_r_plugin_name(plugin_path: str) -> str | None:
|
|
65
|
+
"""
|
|
66
|
+
get name of a R plugin
|
|
67
|
+
"""
|
|
68
|
+
# search plugin name
|
|
69
|
+
plugin_name: str | None = None
|
|
70
|
+
with open(plugin_path, "r") as f_in:
|
|
71
|
+
for line in f_in:
|
|
72
|
+
if line.startswith("plugin_name"):
|
|
73
|
+
if "=" in line:
|
|
74
|
+
plugin_name = line.split("=")[1].strip().replace('"', "").replace("'", "")
|
|
75
|
+
break
|
|
76
|
+
elif "<-" in line:
|
|
77
|
+
plugin_name = line.split("<-")[1].strip().replace('"', "").replace("'", "")
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
plugin_name = None
|
|
81
|
+
break
|
|
82
|
+
return plugin_name
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_r_plugin_description(plugin_path: str) -> str | None:
|
|
86
|
+
"""
|
|
87
|
+
get description of a R plugin
|
|
88
|
+
"""
|
|
89
|
+
# search plugin name
|
|
90
|
+
plugin_description: str | None = None
|
|
91
|
+
with open(plugin_path, "r") as f_in:
|
|
92
|
+
for line in f_in:
|
|
93
|
+
if line.startswith("description"):
|
|
94
|
+
if "=" in line:
|
|
95
|
+
plugin_description = line.split("=")[1].strip().replace('"', "").replace("'", "")
|
|
96
|
+
break
|
|
97
|
+
elif "<-" in line:
|
|
98
|
+
plugin_description = line.split("<-")[1].strip().replace('"', "").replace("'", "")
|
|
99
|
+
break
|
|
100
|
+
else:
|
|
101
|
+
plugin_description = None
|
|
102
|
+
break
|
|
103
|
+
return plugin_description
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_plugins(self):
|
|
107
|
+
"""
|
|
108
|
+
load selected plugins in config_param
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
logging.debug("Loading plugins")
|
|
112
|
+
|
|
113
|
+
def msg():
|
|
114
|
+
QMessageBox.warning(
|
|
115
|
+
self,
|
|
116
|
+
cfg.programName,
|
|
117
|
+
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.",
|
|
118
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
119
|
+
QMessageBox.NoButton,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.menu_plugins.clear()
|
|
123
|
+
self.config_param[cfg.ANALYSIS_PLUGINS] = {}
|
|
124
|
+
|
|
125
|
+
# load BORIS plugins
|
|
126
|
+
for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
|
|
127
|
+
if file_.name.startswith("_"):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
logging.debug(f"Loading plugin: {Path(file_).stem}")
|
|
131
|
+
|
|
132
|
+
# test module
|
|
133
|
+
module_name = Path(file_).stem
|
|
134
|
+
spec = importlib.util.spec_from_file_location(module_name, file_)
|
|
135
|
+
plugin_module = importlib.util.module_from_spec(spec)
|
|
136
|
+
spec.loader.exec_module(plugin_module)
|
|
137
|
+
attributes_list = dir(plugin_module)
|
|
138
|
+
|
|
139
|
+
if "__plugin_name__" in attributes_list:
|
|
140
|
+
plugin_name = plugin_module.__plugin_name__
|
|
141
|
+
else:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if "run" not in attributes_list:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# plugin_name = get_plugin_name(file_)
|
|
148
|
+
if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
|
|
149
|
+
# check if plugin with same name already loaded
|
|
150
|
+
if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
|
|
151
|
+
msg()
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
|
|
155
|
+
|
|
156
|
+
# load personal plugins
|
|
157
|
+
if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
|
|
158
|
+
for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
|
|
159
|
+
if file_.name.startswith("_"):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
logging.debug(f"Loading personal plugin: {Path(file_).stem}")
|
|
163
|
+
|
|
164
|
+
# test module
|
|
165
|
+
module_name = Path(file_).stem
|
|
166
|
+
spec = importlib.util.spec_from_file_location(module_name, file_)
|
|
167
|
+
plugin_module = importlib.util.module_from_spec(spec)
|
|
168
|
+
spec.loader.exec_module(plugin_module)
|
|
169
|
+
attributes_list = dir(plugin_module)
|
|
170
|
+
|
|
171
|
+
if "__plugin_name__" in attributes_list:
|
|
172
|
+
plugin_name = plugin_module.__plugin_name__
|
|
173
|
+
else:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
if "run" not in attributes_list:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# plugin_name = get_plugin_name(file_)
|
|
180
|
+
if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
|
|
181
|
+
# check if plugin with same name already loaded
|
|
182
|
+
if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
|
|
183
|
+
msg()
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
|
|
187
|
+
|
|
188
|
+
# load personal R plugins
|
|
189
|
+
if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
|
|
190
|
+
for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.R")):
|
|
191
|
+
if file_.name.startswith("_"):
|
|
192
|
+
continue
|
|
193
|
+
plugin_name = get_r_plugin_name(file_)
|
|
194
|
+
if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
|
|
195
|
+
# check if plugin with same name already loaded
|
|
196
|
+
if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
|
|
197
|
+
msg()
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
|
|
201
|
+
|
|
202
|
+
logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters: dict = {}) -> pd.DataFrame:
|
|
206
|
+
"""
|
|
207
|
+
filter the dataframe following parameters
|
|
208
|
+
|
|
209
|
+
filter by selected observations.
|
|
210
|
+
filter by selected subjects.
|
|
211
|
+
filter by selected behaviors.
|
|
212
|
+
filter by time interval.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
# filter selected observations
|
|
216
|
+
df = df[df["Observation id"].isin(observations_list)]
|
|
217
|
+
|
|
218
|
+
if parameters:
|
|
219
|
+
# filter selected subjects
|
|
220
|
+
df = df[df["Subject"].isin(parameters["selected subjects"])]
|
|
221
|
+
|
|
222
|
+
# filter selected behaviors
|
|
223
|
+
df = df[df["Behavior"].isin(parameters["selected behaviors"])]
|
|
224
|
+
|
|
225
|
+
if parameters["time"] == cfg.TIME_OBS_INTERVAL:
|
|
226
|
+
# filter each observation with observation interval start/stop
|
|
227
|
+
|
|
228
|
+
# keep events between observation interval start time and observation interval stop/end
|
|
229
|
+
df_interval = df[
|
|
230
|
+
(
|
|
231
|
+
((df["Start (s)"] >= df["Observation interval start"]) & (df["Start (s)"] <= df["Observation interval stop"]))
|
|
232
|
+
| ((df["Stop (s)"] >= df["Observation interval start"]) & (df["Stop (s)"] <= df["Observation interval stop"]))
|
|
233
|
+
)
|
|
234
|
+
| ((df["Start (s)"] < df["Observation interval start"]) & (df["Stop (s)"] > df["Observation interval stop"]))
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
df_interval.loc[df["Start (s)"] < df["Observation interval start"], "Start (s)"] = df["Observation interval start"]
|
|
238
|
+
df_interval.loc[df["Stop (s)"] > df["Observation interval stop"], "Stop (s)"] = df["Observation interval stop"]
|
|
239
|
+
|
|
240
|
+
df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
|
|
241
|
+
|
|
242
|
+
df = df_interval
|
|
243
|
+
|
|
244
|
+
else:
|
|
245
|
+
# filter selected time interval
|
|
246
|
+
if parameters["start time"] is not None and parameters["end time"] is not None:
|
|
247
|
+
MIN_TIME = parameters["start time"]
|
|
248
|
+
MAX_TIME = parameters["end time"]
|
|
249
|
+
|
|
250
|
+
# keep events between start time and end_time
|
|
251
|
+
df_interval = df[
|
|
252
|
+
(
|
|
253
|
+
((df["Start (s)"] >= MIN_TIME) & (df["Start (s)"] <= MAX_TIME))
|
|
254
|
+
| ((df["Stop (s)"] >= MIN_TIME) & (df["Stop (s)"] <= MAX_TIME))
|
|
255
|
+
)
|
|
256
|
+
| ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
# cut state events to interval
|
|
260
|
+
df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
|
|
261
|
+
df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
|
|
262
|
+
|
|
263
|
+
df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
|
|
264
|
+
|
|
265
|
+
df = df_interval
|
|
266
|
+
|
|
267
|
+
print("filtered")
|
|
268
|
+
print("=" * 50)
|
|
269
|
+
|
|
270
|
+
# print(f"{df=}")
|
|
271
|
+
|
|
272
|
+
return df
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def run_plugin(self, plugin_name):
|
|
276
|
+
"""
|
|
277
|
+
run plugin
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
if not self.project:
|
|
281
|
+
QMessageBox.warning(
|
|
282
|
+
self,
|
|
283
|
+
cfg.programName,
|
|
284
|
+
"No observations found. Open a project first",
|
|
285
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
286
|
+
QMessageBox.NoButton,
|
|
287
|
+
)
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
|
|
291
|
+
|
|
292
|
+
if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
|
|
293
|
+
QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
plugin_path: str = self.config_param.get(cfg.ANALYSIS_PLUGINS, {}).get(plugin_name, "")
|
|
297
|
+
|
|
298
|
+
logging.debug(f"{plugin_path=}")
|
|
299
|
+
|
|
300
|
+
if not Path(plugin_path).is_file():
|
|
301
|
+
QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
logging.debug(f"run plugin from {plugin_path}")
|
|
305
|
+
|
|
306
|
+
# select observations to analyze
|
|
307
|
+
selected_observations, parameters = self.obs_param()
|
|
308
|
+
if not selected_observations:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
logging.info("preparing dataframe for plugin")
|
|
312
|
+
|
|
313
|
+
message, df = project_functions.project2dataframe(self.pj, selected_observations)
|
|
314
|
+
if message:
|
|
315
|
+
logging.critical(message)
|
|
316
|
+
QMessageBox.critical(self, cfg.programName, message)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
logging.info("done")
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
logging.debug("dataframe info")
|
|
323
|
+
logging.debug(f"{df.info()}")
|
|
324
|
+
logging.debug(f"{df.head()}")
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
# filter the dataframe with parameters
|
|
328
|
+
logging.info("filtering dataframe for plugin")
|
|
329
|
+
filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
|
|
330
|
+
logging.info("done")
|
|
331
|
+
|
|
332
|
+
if Path(plugin_path).suffix == ".py":
|
|
333
|
+
# load plugin as module
|
|
334
|
+
module_name = Path(plugin_path).stem
|
|
335
|
+
|
|
336
|
+
spec = importlib.util.spec_from_file_location(module_name, plugin_path)
|
|
337
|
+
plugin_module = importlib.util.module_from_spec(spec)
|
|
338
|
+
|
|
339
|
+
logging.debug(f"{plugin_module=}")
|
|
340
|
+
|
|
341
|
+
spec.loader.exec_module(plugin_module)
|
|
342
|
+
|
|
343
|
+
plugin_version = plugin_module.__version__
|
|
344
|
+
plugin_version_date = plugin_module.__version_date__
|
|
345
|
+
|
|
346
|
+
logging.info(
|
|
347
|
+
f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# run plugin
|
|
351
|
+
plugin_results = plugin_module.run(filtered_df)
|
|
352
|
+
|
|
353
|
+
if Path(plugin_path).suffix in (".R", ".r"):
|
|
354
|
+
try:
|
|
355
|
+
from rpy2 import robjects
|
|
356
|
+
from rpy2.robjects import pandas2ri
|
|
357
|
+
from rpy2.robjects.packages import SignatureTranslatedAnonymousPackage
|
|
358
|
+
from rpy2.robjects.conversion import localconverter
|
|
359
|
+
except Exception:
|
|
360
|
+
QMessageBox.critical(self, cfg.programName, "The rpy2 Python module is not installed. R plugins cannot be used")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
# Read code from file
|
|
364
|
+
try:
|
|
365
|
+
with open(plugin_path, "r") as f:
|
|
366
|
+
r_code = f.read()
|
|
367
|
+
except Exception:
|
|
368
|
+
QMessageBox.critical(self, cfg.programName, f"Error reading the plugin {plugin_path}.")
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
# read version
|
|
372
|
+
plugin_version = next(
|
|
373
|
+
(
|
|
374
|
+
x.split("<-")[1].replace('"', "").replace("'", "").strip()
|
|
375
|
+
for x in r_code.splitlines()
|
|
376
|
+
if x.replace(" ", "").startswith("version<-")
|
|
377
|
+
),
|
|
378
|
+
None,
|
|
379
|
+
)
|
|
380
|
+
# read version date
|
|
381
|
+
plugin_version_date = next(
|
|
382
|
+
(
|
|
383
|
+
x.split("<-")[1].replace('"', "").replace("'", "").strip()
|
|
384
|
+
for x in r_code.split("\n")
|
|
385
|
+
if x.replace(" ", "").startswith("version_date<")
|
|
386
|
+
),
|
|
387
|
+
None,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
r_plugin = SignatureTranslatedAnonymousPackage(r_code, "r_plugin")
|
|
391
|
+
|
|
392
|
+
with localconverter(robjects.default_converter + pandas2ri.converter):
|
|
393
|
+
r_df = robjects.conversion.py2rpy(filtered_df)
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
r_result = r_plugin.run(r_df)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
QMessageBox.critical(self, cfg.programName, f"Error in the plugin {plugin_path}: {e}.")
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
with localconverter(robjects.default_converter + pandas2ri.converter):
|
|
402
|
+
plugin_results = robjects.conversion.rpy2py(r_result)
|
|
403
|
+
|
|
404
|
+
# test if plugin_results is a tuple: if not transform it to tuple
|
|
405
|
+
if not isinstance(plugin_results, tuple):
|
|
406
|
+
plugin_results = tuple([plugin_results])
|
|
407
|
+
|
|
408
|
+
self.plugin_visu: list = []
|
|
409
|
+
for result in plugin_results:
|
|
410
|
+
if isinstance(result, str):
|
|
411
|
+
self.plugin_visu.append(dialog.Results_dialog())
|
|
412
|
+
self.plugin_visu[-1].setWindowTitle(plugin_name)
|
|
413
|
+
self.plugin_visu[-1].ptText.clear()
|
|
414
|
+
self.plugin_visu[-1].ptText.appendPlainText(result)
|
|
415
|
+
self.plugin_visu[-1].show()
|
|
416
|
+
elif isinstance(result, pd.DataFrame):
|
|
417
|
+
self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_version} ({plugin_version_date})", result))
|
|
418
|
+
self.plugin_visu[-1].show()
|
|
419
|
+
else:
|
|
420
|
+
# result is not str nor dataframe
|
|
421
|
+
QMessageBox.critical(
|
|
422
|
+
None,
|
|
423
|
+
cfg.programName,
|
|
424
|
+
(
|
|
425
|
+
f"Plugin returns an unknown object type: {type(result)}\n\n"
|
|
426
|
+
"Plugins must return str and/or Pandas Dataframes.\n"
|
|
427
|
+
"Check the plugin code."
|
|
428
|
+
),
|
|
429
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
430
|
+
QMessageBox.NoButton,
|
|
431
|
+
)
|
boris/portion/__init__.py
CHANGED
|
@@ -3,18 +3,28 @@ from .interval import Interval, open, closed, openclosed, closedopen, empty, sin
|
|
|
3
3
|
from .func import iterate
|
|
4
4
|
from .io import from_string, to_string, from_data, to_data
|
|
5
5
|
|
|
6
|
-
# disabled because BORIS does not need IntervalDict
|
|
6
|
+
# disabled because BORIS does not need IntervalDict
|
|
7
7
|
# so the sortedcontainers module is not required
|
|
8
|
-
#from .dict import IntervalDict
|
|
8
|
+
# from .dict import IntervalDict
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
"inf",
|
|
13
|
+
"CLOSED",
|
|
14
|
+
"OPEN",
|
|
15
|
+
"Interval",
|
|
16
|
+
"open",
|
|
17
|
+
"closed",
|
|
18
|
+
"openclosed",
|
|
19
|
+
"closedopen",
|
|
20
|
+
"singleton",
|
|
21
|
+
"empty",
|
|
22
|
+
"iterate",
|
|
23
|
+
"from_string",
|
|
24
|
+
"to_string",
|
|
25
|
+
"from_data",
|
|
26
|
+
"to_data",
|
|
27
|
+
"IntervalDict",
|
|
18
28
|
]
|
|
19
29
|
|
|
20
30
|
CLOSED = Bound.CLOSED
|
boris/portion/const.py
CHANGED
|
@@ -5,11 +5,12 @@ class Bound(enum.Enum):
|
|
|
5
5
|
"""
|
|
6
6
|
Bound types, either CLOSED for inclusive, or OPEN for exclusive.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
CLOSED = True
|
|
9
10
|
OPEN = False
|
|
10
11
|
|
|
11
12
|
def __bool__(self):
|
|
12
|
-
raise ValueError(
|
|
13
|
+
raise ValueError("The truth value of a bound is ambiguous.")
|
|
13
14
|
|
|
14
15
|
def __invert__(self):
|
|
15
16
|
return Bound.CLOSED if self is Bound.OPEN else Bound.OPEN
|
|
@@ -21,7 +22,7 @@ class Bound(enum.Enum):
|
|
|
21
22
|
return self.name
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
class _Singleton
|
|
25
|
+
class _Singleton:
|
|
25
26
|
__instance = None
|
|
26
27
|
|
|
27
28
|
def __new__(cls, *args, **kwargs):
|
|
@@ -35,21 +36,29 @@ class _PInf(_Singleton):
|
|
|
35
36
|
Represent positive infinity.
|
|
36
37
|
"""
|
|
37
38
|
|
|
38
|
-
def __neg__(self):
|
|
39
|
+
def __neg__(self):
|
|
40
|
+
return _NInf()
|
|
39
41
|
|
|
40
|
-
def __lt__(self, o):
|
|
42
|
+
def __lt__(self, o):
|
|
43
|
+
return False
|
|
41
44
|
|
|
42
|
-
def __le__(self, o):
|
|
45
|
+
def __le__(self, o):
|
|
46
|
+
return isinstance(o, _PInf)
|
|
43
47
|
|
|
44
|
-
def __gt__(self, o):
|
|
48
|
+
def __gt__(self, o):
|
|
49
|
+
return not isinstance(o, _PInf)
|
|
45
50
|
|
|
46
|
-
def __ge__(self, o):
|
|
51
|
+
def __ge__(self, o):
|
|
52
|
+
return True
|
|
47
53
|
|
|
48
|
-
def __eq__(self, o):
|
|
54
|
+
def __eq__(self, o):
|
|
55
|
+
return isinstance(o, _PInf)
|
|
49
56
|
|
|
50
|
-
def __repr__(self):
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
return "+inf"
|
|
51
59
|
|
|
52
|
-
def __hash__(self):
|
|
60
|
+
def __hash__(self):
|
|
61
|
+
return hash(float("+inf"))
|
|
53
62
|
|
|
54
63
|
|
|
55
64
|
class _NInf(_Singleton):
|
|
@@ -57,21 +66,29 @@ class _NInf(_Singleton):
|
|
|
57
66
|
Represent negative infinity.
|
|
58
67
|
"""
|
|
59
68
|
|
|
60
|
-
def __neg__(self):
|
|
69
|
+
def __neg__(self):
|
|
70
|
+
return _PInf()
|
|
61
71
|
|
|
62
|
-
def __lt__(self, o):
|
|
72
|
+
def __lt__(self, o):
|
|
73
|
+
return not isinstance(o, _NInf)
|
|
63
74
|
|
|
64
|
-
def __le__(self, o):
|
|
75
|
+
def __le__(self, o):
|
|
76
|
+
return True
|
|
65
77
|
|
|
66
|
-
def __gt__(self, o):
|
|
78
|
+
def __gt__(self, o):
|
|
79
|
+
return False
|
|
67
80
|
|
|
68
|
-
def __ge__(self, o):
|
|
81
|
+
def __ge__(self, o):
|
|
82
|
+
return isinstance(o, _NInf)
|
|
69
83
|
|
|
70
|
-
def __eq__(self, o):
|
|
84
|
+
def __eq__(self, o):
|
|
85
|
+
return isinstance(o, _NInf)
|
|
71
86
|
|
|
72
|
-
def __repr__(self):
|
|
87
|
+
def __repr__(self):
|
|
88
|
+
return "-inf"
|
|
73
89
|
|
|
74
|
-
def __hash__(self):
|
|
90
|
+
def __hash__(self):
|
|
91
|
+
return hash(float("-inf"))
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
# Positive infinity
|
boris/portion/dict.py
CHANGED
|
@@ -28,7 +28,7 @@ class IntervalDict(MutableMapping):
|
|
|
28
28
|
number of distinct values (not keys) that are stored.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
__slots__ = (
|
|
31
|
+
__slots__ = ("_storage",)
|
|
32
32
|
|
|
33
33
|
def __init__(self, mapping_or_iterable=None):
|
|
34
34
|
"""
|
|
@@ -352,10 +352,10 @@ class IntervalDict(MutableMapping):
|
|
|
352
352
|
return key in self.domain()
|
|
353
353
|
|
|
354
354
|
def __repr__(self):
|
|
355
|
-
return
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
355
|
+
return "{}{}{}".format(
|
|
356
|
+
"{",
|
|
357
|
+
", ".join("{!r}: {!r}".format(i, v) for i, v in self.items()),
|
|
358
|
+
"}",
|
|
359
359
|
)
|
|
360
360
|
|
|
361
361
|
def __eq__(self, other):
|
boris/portion/func.py
CHANGED
|
@@ -31,7 +31,7 @@ def iterate(interval, step, *, base=None, reverse=False):
|
|
|
31
31
|
:return: a lazy iterator.
|
|
32
32
|
"""
|
|
33
33
|
if base is None:
|
|
34
|
-
base =
|
|
34
|
+
base = lambda x: x
|
|
35
35
|
|
|
36
36
|
exclude = operator.lt if not reverse else operator.gt
|
|
37
37
|
include = operator.le if not reverse else operator.ge
|
|
@@ -39,7 +39,7 @@ def iterate(interval, step, *, base=None, reverse=False):
|
|
|
39
39
|
|
|
40
40
|
value = base(interval.lower if not reverse else interval.upper)
|
|
41
41
|
if (value == -inf and not reverse) or (value == inf and reverse):
|
|
42
|
-
raise ValueError(
|
|
42
|
+
raise ValueError("Cannot start iteration with infinity.")
|
|
43
43
|
|
|
44
44
|
for i in interval if not reverse else reversed(interval):
|
|
45
45
|
value = base(i.lower if not reverse else i.upper)
|