sapiopycommons 2024.10.24a342__py3-none-any.whl → 2024.10.29a346__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 sapiopycommons might be problematic. Click here for more details.

@@ -1,15 +1,141 @@
1
+ from sapiopylib.rest.ELNService import ElnManager
1
2
  from sapiopylib.rest.User import SapioUser
3
+ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, AbstractReportTerm, RawReportTerm
2
4
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
5
+ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, ElnExperimentQueryCriteria
6
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnExperimentStatus, ElnBaseDataType
3
7
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
4
8
 
5
9
  from sapiopycommons.customreport.custom_report_builder import CustomReportBuilder
6
10
  from sapiopycommons.customreport.term_builder import TermBuilder
7
- from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil
11
+ from sapiopycommons.datatype.pseudo_data_types import EnbEntryOptionsPseudoDef, NotebookExperimentOptionPseudoDef, \
12
+ NotebookExperimentPseudoDef, ExperimentEntryRecordPseudoDef, EnbEntryPseudoDef
13
+ from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil, FieldValue, \
14
+ ExperimentEntryIdentifier, ExperimentIdentifier
8
15
  from sapiopycommons.general.custom_report_util import CustomReportUtil
16
+ from sapiopycommons.general.exceptions import SapioException
9
17
  from sapiopycommons.recordmodel.record_handler import RecordHandler
10
18
 
11
- _NOTEBOOK_ID = "EXPERIMENTID"
12
- _RECORD_ID = "RECORDID"
19
+
20
+ # FR-47234: Create an ExperimentReportCriteria class for finer experiment searching
21
+ # and create/update ExperimentReportUtil functions to use it.
22
+ class ExperimentReportCriteria:
23
+ """
24
+ Experiment report criteria is used to restrict the results of searches for experiments.
25
+ """
26
+ notebook_ids: list[int] | None
27
+ not_notebook_ids: list[int] | None
28
+ names: list[str] | None
29
+ not_names: list[str] | None
30
+ created_by: list[str] | None
31
+ not_created_by: list[str] | None
32
+ last_modified_by: list[str] | None
33
+ not_last_modified_by: list[str] | None
34
+ owners: list[str] | None
35
+ not_owners: list[str] | None
36
+ statuses: list[str] | None
37
+ not_statuses: list[str] | None
38
+ templates: str | None
39
+ not_templates: str | None
40
+ is_from_template: bool | None
41
+ is_from_protocol_template: bool | None
42
+ is_modifiable: bool | None
43
+ is_active: bool | None
44
+ created_before: int | None
45
+ created_after: int | None
46
+ due_before: int | None
47
+ due_after: int | None
48
+ last_modified_before: int | None
49
+ last_modified_after: int | None
50
+
51
+ def __init__(self, *,
52
+ notebook_ids: list[ExperimentIdentifier] | None = None,
53
+ not_notebook_ids: list[ExperimentIdentifier] | None = None,
54
+ names: list[str] | None = None,
55
+ not_names: list[str] | None = None,
56
+ created_by: list[str] | None = None,
57
+ not_created_by: list[str] | None = None,
58
+ last_modified_by: list[str] | None = None,
59
+ not_last_modified_by: list[str] | None = None,
60
+ owners: list[str] | None = None,
61
+ not_owners: list[str] | None = None,
62
+ statuses: list[ElnExperimentStatus | str] | None = None,
63
+ not_statuses: list[ElnExperimentStatus | str] | None = None,
64
+ templates: list[str] | None = None,
65
+ not_templates: list[str] | None = None,
66
+ is_from_template: bool | None = None,
67
+ is_from_protocol_template: bool | None = None,
68
+ is_modifiable: bool | None = None,
69
+ is_active: bool | None = None,
70
+ created_after: int | None = None,
71
+ created_before: int | None = None,
72
+ due_after: int | None = None,
73
+ due_before: int | None = None,
74
+ last_modified_after: int | None = None,
75
+ last_modified_before: int | None = None):
76
+ """
77
+ Restrict searches using the following criteria.
78
+
79
+ :param notebook_ids: The allowed notebook ID(s) of the experiment.
80
+ :param not_notebook_ids: The disallowed notebook ID(s) of the experiment.
81
+ :param names: The allowed name(s) of the experiment.
82
+ :param not_names: The disallowed name(s) of the experiment.
83
+ :param created_by: The allowed username(s) of the user who created the experiment.
84
+ :param not_created_by: The disallowed username(s) of the user who created the experiment.
85
+ :param last_modified_by: The allowed username(s) of the user who last modified the experiment.
86
+ :param not_last_modified_by: The disallowed username(s) of the user who last modified the experiment.
87
+ :param owners: The allowed username(s) of the user who owns the experiment.
88
+ :param not_owners: The disallowed username(s) of the user who owns the experiment.
89
+ :param statuses: The allowed status(es) of the experiment.
90
+ :param not_statuses: The disallowed status(es) of the experiment.
91
+ :param templates: The allowed template name(s) that the experiment was created from.
92
+ :param not_templates: The disallowed template name(s) that the experiment was created from.
93
+ :param is_from_template: Whether the experiment was created from a template.
94
+ :param is_from_protocol_template: Whether the experiment was created from a protocol template.
95
+ :param is_modifiable: Whether the experiment is modifiable.
96
+ :param is_active: Whether the experiment is from an active template.
97
+ :param created_after: A timestamp after which the experiment was created.
98
+ :param created_before: A timestamp before which the experiment was created.
99
+ :param due_after: A timestamp after which the experiment's approval is due.
100
+ :param due_before: A timestamp before which the experiment's approval is due.
101
+ :param last_modified_after: A timestamp after which the experiment last modified.
102
+ :param last_modified_before: A timestamp before which the experiment last modified.
103
+ """
104
+ self.notebook_ids = notebook_ids
105
+ self.not_notebook_ids = not_notebook_ids
106
+ self.names = names
107
+ self.not_names = not_names
108
+ self.created_by = created_by
109
+ self.not_created_by = not_created_by
110
+ self.last_modified_by = last_modified_by
111
+ self.not_last_modified_by = not_last_modified_by
112
+ self.owners = owners
113
+ self.not_owners = not_owners
114
+ self.statuses = statuses
115
+ self.not_statuses = not_statuses
116
+ self.templates = templates
117
+ self.not_templates = not_templates
118
+
119
+ self.is_from_template = is_from_template
120
+ self.is_from_protocol_template = is_from_protocol_template
121
+ self.is_modifiable = is_modifiable
122
+ self.is_active = is_active
123
+
124
+ self.created_before = created_before
125
+ self.created_after = created_after
126
+ self.due_before = due_before
127
+ self.due_after = due_after
128
+ self.last_modified_before = last_modified_before
129
+ self.last_modified_after = last_modified_after
130
+
131
+ if self.notebook_ids is not None:
132
+ self.notebook_ids = AliasUtil.to_notebook_ids(self.notebook_ids)
133
+ if self.not_notebook_ids is not None:
134
+ self.not_notebook_ids = AliasUtil.to_notebook_ids(self.not_notebook_ids)
135
+ if self.statuses is not None:
136
+ self.statuses = [x.description if isinstance(x, ElnExperimentStatus) else x for x in self.statuses]
137
+ if self.not_statuses is not None:
138
+ self.not_statuses = [x.description if isinstance(x, ElnExperimentStatus) else x for x in self.not_statuses]
13
139
 
14
140
 
15
141
  # FR-46908 - Provide a utility class that holds experiment related custom reports e.g. getting all the experiments
@@ -23,8 +149,8 @@ class ExperimentReportUtil:
23
149
  If a record wasn't used in any experiments then it will be mapped to an empty list.
24
150
 
25
151
  :param context: The current webhook context or a user object to send requests from.
26
- :param records: a list of records of the same data type.
27
- :return: a dictionary mapping each record to a list of ids of each experiment it was used in.
152
+ :param records: A list of records of the same data type.
153
+ :return: A dictionary mapping each record to a list of ids of each experiment it was used in.
28
154
  """
29
155
  if not records:
30
156
  return {}
@@ -32,30 +158,70 @@ class ExperimentReportUtil:
32
158
  user: SapioUser = AliasUtil.to_sapio_user(context)
33
159
  data_type_name: str = AliasUtil.to_singular_data_type_name(records)
34
160
 
35
- record_ids = [record.record_id for record in records]
161
+ record_ids: list[int] = AliasUtil.to_record_ids(records)
36
162
  rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, record_ids=record_ids)
37
163
 
38
164
  id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
39
165
  record_to_exps: dict[SapioRecord, set[int]] = {record: set() for record in records}
40
166
  for row in rows:
41
- record_id: int = row[_RECORD_ID]
42
- exp_id: int = row[_NOTEBOOK_ID]
167
+ record_id: int = row["RecordId"]
168
+ exp_id: int = row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
43
169
  record = id_to_record[record_id]
44
170
  record_to_exps[record].add(exp_id)
45
171
 
46
172
  return {record: list(exps) for record, exps in record_to_exps.items()}
47
173
 
48
174
  @staticmethod
49
- def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[int],
175
+ def map_eln_records_to_experiment_ids(context: UserIdentifier, records: list[SapioRecord]) \
176
+ -> dict[SapioRecord, int]:
177
+ """
178
+ Return a dictionary mapping each ELN record to the ID of the experiments that each were used in.
179
+
180
+ :param context: The current webhook context or a user object to send requests from.
181
+ :param records: A list of ELN data type records. They are permitted to be of mixed data type names.
182
+ :return: A dictionary mapping each record to the ID of the experiment it was used in.
183
+ """
184
+ if not records:
185
+ return {}
186
+
187
+ data_types: set[str] = AliasUtil.to_data_type_names(records, True, False)
188
+ for data_type in data_types:
189
+ if not ElnBaseDataType.is_eln_type(data_type):
190
+ raise SapioException(f"Data type {data_type} is not an ELN data type.")
191
+
192
+ dt_to_record: dict[str, list[SapioRecord]] = {}
193
+ for record in records:
194
+ dt_to_record.setdefault(AliasUtil.to_data_type_name(record, False), []).append(record)
195
+
196
+ report_builder = CustomReportBuilder(EnbEntryPseudoDef.DATA_TYPE_NAME)
197
+ report_builder.add_column(EnbEntryPseudoDef.DATA_TYPE_NAME__FIELD_NAME)
198
+ report_builder.add_column(EnbEntryPseudoDef.EXPERIMENT_ID__FIELD_NAME)
199
+ report_builder.set_root_term(TermBuilder.is_term(EnbEntryPseudoDef.DATA_TYPE_NAME,
200
+ EnbEntryPseudoDef.DATA_TYPE_NAME__FIELD_NAME,
201
+ data_types))
202
+ criteria = report_builder.build_report_criteria()
203
+
204
+ ret_val: dict[SapioRecord, int] = {}
205
+ rows: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, criteria)
206
+ for row in rows:
207
+ dt: str = row[EnbEntryPseudoDef.DATA_TYPE_NAME__FIELD_NAME.field_name]
208
+ exp_id: int = row[EnbEntryPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
209
+ for record in dt_to_record[dt]:
210
+ ret_val[record] = exp_id
211
+ return ret_val
212
+
213
+ @staticmethod
214
+ def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[ExperimentIdentifier],
50
215
  wrapper_type: type[WrappedType]) -> dict[int, list[WrappedType]]:
51
216
  """
52
- Return a dictionary mapping each experiment id to a list of records of the given type that were used in each experiment.
53
- If an experiment didn't use any records of the given type then it will be mapped to an empty list.
217
+ Return a dictionary mapping each experiment id to a list of records of the given type that were used in each
218
+ experiment. If an experiment didn't use any records of the given type then it will be mapped to an empty list.
54
219
 
55
220
  :param context: The current webhook context or a user object to send requests from.
56
- :param exp_ids: a list of experiment ids. These are specifically the Notebook Experiment ids which can be found in the title of the experiment.
221
+ :param exp_ids: A list of experiment identifiers.
57
222
  :param wrapper_type: The record model wrapper to use, corresponds to which data type we will query for.
58
- :return: a dictionary mapping each experiment id to a list of records of the given type that were used in that experiment.
223
+ :return: A dictionary mapping each experiment id to a list of records of the given type that were used in that
224
+ experiment.
59
225
  """
60
226
  if not exp_ids:
61
227
  return {}
@@ -64,23 +230,380 @@ class ExperimentReportUtil:
64
230
  record_handler = RecordHandler(user)
65
231
  data_type_name: str = wrapper_type.get_wrapper_data_type_name()
66
232
 
233
+ exp_ids: list[int] = AliasUtil.to_notebook_ids(exp_ids)
67
234
  rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, exp_ids=exp_ids)
68
- record_ids: set[int] = {row[_RECORD_ID] for row in rows}
235
+ record_ids: set[int] = {row["RecordId"] for row in rows}
69
236
  records = record_handler.query_models_by_id(wrapper_type, record_ids)
70
237
 
71
238
  id_to_record: dict[int, WrappedType] = RecordHandler.map_by_id(records)
72
239
  exp_to_records: dict[int, set[SapioRecord]] = {exp: set() for exp in exp_ids}
73
240
  for row in rows:
74
- record_id: int = row[_RECORD_ID]
75
- exp_id: int = row[_NOTEBOOK_ID]
241
+ record_id: int = row["RecordId"]
242
+ exp_id: int = row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
76
243
  record = id_to_record[record_id]
77
244
  exp_to_records[exp_id].add(record)
78
245
 
79
246
  return {exp: list(records) for exp, records in exp_to_records.items()}
80
247
 
248
+ @staticmethod
249
+ def get_experiment_options(context: UserIdentifier, experiments: list[ExperimentIdentifier]) \
250
+ -> dict[int, dict[str, str]]:
251
+ """
252
+ Run a custom report to retrieve the experiment options for all the provided experiments. Effectively a batched
253
+ version of the get_notebook_experiment_options function of ElnManager.
254
+
255
+ :param context: The current webhook context or a user object to send requests from.
256
+ :param experiments: The experiment identifiers to retrieve the experiment options for.
257
+ :return: A dictionary mapping the notebook experiment ID to the options for that experiment.
258
+ """
259
+ exp_ids: list[int] = AliasUtil.to_notebook_ids(experiments)
260
+
261
+ report_builder = CustomReportBuilder(NotebookExperimentOptionPseudoDef.DATA_TYPE_NAME)
262
+ root = TermBuilder.is_term(NotebookExperimentOptionPseudoDef.DATA_TYPE_NAME,
263
+ NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME,
264
+ exp_ids)
265
+ report_builder.set_root_term(root)
266
+ report_builder.add_column(NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME)
267
+ report_builder.add_column(NotebookExperimentOptionPseudoDef.OPTION_KEY__FIELD_NAME)
268
+ report_builder.add_column(NotebookExperimentOptionPseudoDef.OPTION_VALUE__FIELD_NAME)
269
+ report = report_builder.build_report_criteria()
270
+
271
+ # Ensure that each experiment appears in the dictionary, even if it has no experiment options.
272
+ options: dict[int, dict[str, str]] = {x: {} for x in exp_ids}
273
+ results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
274
+ for row in results:
275
+ exp_id: int = row[NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
276
+ key: str = row[NotebookExperimentOptionPseudoDef.OPTION_KEY__FIELD_NAME.field_name]
277
+ value: str = row[NotebookExperimentOptionPseudoDef.OPTION_VALUE__FIELD_NAME.field_name]
278
+ options[exp_id][key] = value
279
+ return options
280
+
281
+ @staticmethod
282
+ def get_experiment_entry_options(context: UserIdentifier, entries: list[ExperimentEntryIdentifier]) \
283
+ -> dict[int, dict[str, str]]:
284
+ """
285
+ Run a custom report to retrieve the entry options for all the provided entries. Effectively a batched
286
+ version of the get_experiment_entry_options function of ElnManager.
287
+
288
+ :param context: The current webhook context or a user object to send requests from.
289
+ :param entries: The experiment entry identifiers to retrieve the entry options for.
290
+ :return: A dictionary mapping the entry ID to the options for that entry.
291
+ """
292
+ entries: list[int] = AliasUtil.to_entry_ids(entries)
293
+ report_builder = CustomReportBuilder(EnbEntryOptionsPseudoDef.DATA_TYPE_NAME)
294
+ root = TermBuilder.is_term(EnbEntryOptionsPseudoDef.DATA_TYPE_NAME,
295
+ EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME,
296
+ entries)
297
+ report_builder.set_root_term(root)
298
+ report_builder.add_column(EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME)
299
+ report_builder.add_column(EnbEntryOptionsPseudoDef.ENTRY_OPTION_KEY__FIELD_NAME)
300
+ report_builder.add_column(EnbEntryOptionsPseudoDef.ENTRY_OPTION_VALUE__FIELD_NAME)
301
+ report = report_builder.build_report_criteria()
302
+
303
+ # Ensure that each entry appears in the dictionary, even if it has no entry options.
304
+ options: dict[int, dict[str, str]] = {x: {} for x in entries}
305
+ results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
306
+ for row in results:
307
+ entry_id: int = row[EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME.field_name]
308
+ key: str = row[EnbEntryOptionsPseudoDef.ENTRY_OPTION_KEY__FIELD_NAME.field_name]
309
+ value: str = row[EnbEntryOptionsPseudoDef.ENTRY_OPTION_VALUE__FIELD_NAME.field_name]
310
+ options[entry_id][key] = value
311
+ return options
312
+
313
+ @staticmethod
314
+ def get_template_names_for_experiments(context: UserIdentifier, experiments: list[ExperimentIdentifier]) \
315
+ -> dict[int, str]:
316
+ """
317
+ Run a custom report to retrieve the template names for all the provided experiments.
318
+
319
+ :param context: The current webhook context or a user object to send requests from.
320
+ :param experiments: The experiment identifiers to retrieve the template names for.
321
+ :return: A dictionary mapping the notebook experiment ID to the template name that the experiment was created from.
322
+ """
323
+ exp_ids: list[int] = AliasUtil.to_notebook_ids(experiments)
324
+
325
+ report_builder = CustomReportBuilder(NotebookExperimentPseudoDef.DATA_TYPE_NAME)
326
+ root = TermBuilder.is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
327
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
328
+ exp_ids)
329
+ report_builder.add_join(TermBuilder.compare_is_term("ELNExperiment",
330
+ "RecordId",
331
+ NotebookExperimentPseudoDef.DATA_TYPE_NAME,
332
+ NotebookExperimentPseudoDef.EXPERIMENT_RECORD_ID__FIELD_NAME))
333
+ report_builder.set_root_term(root)
334
+ report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME)
335
+ report_builder.add_column("TemplateExperimentName", FieldType.STRING, data_type="ELNExperiment")
336
+ report = report_builder.build_report_criteria()
337
+
338
+ ret_val: dict[int, str] = {}
339
+ results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
340
+ for row in results:
341
+ exp_id: int = row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
342
+ name: str = row["TemplateExperimentName"]
343
+ ret_val[exp_id] = name
344
+ return ret_val
345
+
346
+ @staticmethod
347
+ def get_experiments_for_criteria(context: UserIdentifier, criteria: ExperimentReportCriteria) -> list[ElnExperiment]:
348
+ """
349
+ Run a custom report that retrieves every experiment in the system for the given search criteria.
350
+
351
+ :param context: The current webhook context or a user object to send requests from.
352
+ :param criteria: The search criteria to query for experiments with.
353
+ :return: A list of every experiment in the system that matches the search criteria.
354
+ """
355
+ report: CustomReportCriteria = ExperimentReportUtil.build_experiment_id_report(criteria)
356
+ return ExperimentReportUtil.get_experiments_for_report(context, report)
357
+
358
+ @staticmethod
359
+ def get_experiments_by_name(context: UserIdentifier, name: str,
360
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria()) -> list[ElnExperiment]:
361
+ """
362
+ Run a custom report that retrieves every experiment in the system with a given name.
363
+
364
+ :param context: The current webhook context or a user object to send requests from.
365
+ :param name: The name of the experiment to query for.
366
+ :param criteria: Additional search criteria to filter the results.
367
+ :return: A list of every experiment in the system with a name that matches the input
368
+ that matches the given criteria.
369
+ """
370
+ return ExperimentReportUtil.get_experiments_by_names(context, [name], criteria)[name]
371
+
372
+ @staticmethod
373
+ def get_experiments_by_names(context: UserIdentifier, names: list[str],
374
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria()) -> dict[str, list[ElnExperiment]]:
375
+ """
376
+ Run a custom report that retrieves every experiment in the system with a name from a list of names.
377
+
378
+ :param context: The current webhook context or a user object to send requests from.
379
+ :param names: The names of the experiment to query for.
380
+ :param criteria: Additional search criteria to filter the results.
381
+ :return: A dictionary mapping the experiment name to a list of every experiment in the system with that name
382
+ that matches the given criteria.
383
+ """
384
+ criteria.names = names
385
+ experiments: list[ElnExperiment] = ExperimentReportUtil.get_experiments_for_criteria(context, criteria)
386
+
387
+ # Ensure that each name appears in the dictionary, even if it has no experiments.
388
+ ret_val: dict[str, list[ElnExperiment]] = {x: [] for x in names}
389
+ for experiment in experiments:
390
+ ret_val.get(experiment.notebook_experiment_name).append(experiment)
391
+ return ret_val
392
+
393
+ @staticmethod
394
+ def get_experiments_by_owner(context: UserIdentifier, owner: str,
395
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria()) -> list[ElnExperiment]:
396
+ """
397
+ Run a custom report that retrieves every experiment in the system with a given owner.
398
+
399
+ :param context: The current webhook context or a user object to send requests from.
400
+ :param owner: The username of the owner of the experiments to query for.
401
+ :param criteria: Additional search criteria to filter the results.
402
+ :return: A list of every experiment in the system with the owner that matches the input
403
+ that matches the given criteria.
404
+ """
405
+ return ExperimentReportUtil.get_experiments_by_owners(context, [owner], criteria)[owner]
406
+
407
+ @staticmethod
408
+ def get_experiments_by_owners(context: UserIdentifier, owners: list[str],
409
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria) \
410
+ -> dict[str, list[ElnExperiment]]:
411
+ """
412
+ Run a custom report that retrieves every experiment in the system with a given list of owners.
413
+
414
+ :param context: The current webhook context or a user object to send requests from.
415
+ :param owners: The usernames of the owner of the experiments to query for.
416
+ :param criteria: Additional search criteria to filter the results.
417
+ :return: A dictionary mapping the owner username to a list of every experiment in the system from that owner
418
+ that matches the given criteria.
419
+ """
420
+ criteria.owners = owners
421
+ experiments: list[ElnExperiment] = ExperimentReportUtil.get_experiments_for_criteria(context, criteria)
422
+
423
+ # Ensure that each name appears in the dictionary, even if it has no experiments.
424
+ ret_val: dict[str, list[ElnExperiment]] = {x: [] for x in owners}
425
+ for experiment in experiments:
426
+ ret_val.get(experiment.owner).append(experiment)
427
+ return ret_val
428
+
429
+ @staticmethod
430
+ def get_experiments_by_creator(context: UserIdentifier, created_by: str, *,
431
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria()) \
432
+ -> list[ElnExperiment]:
433
+ """
434
+ Run a custom report that retrieves every experiment in the system with a given creator.
435
+
436
+ :param context: The current webhook context or a user object to send requests from.
437
+ :param created_by: The username of the creator of the experiments to query for.
438
+ :param criteria: Additional search criteria to filter the results.
439
+ :return: A list of every experiment in the system with the creator that matches the input
440
+ that matches the given criteria.
441
+ """
442
+ return ExperimentReportUtil.get_experiments_by_creators(context, [created_by], criteria=criteria)[created_by]
443
+
444
+ @staticmethod
445
+ def get_experiments_by_creators(context: UserIdentifier, created_by: list[str], *,
446
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria()) \
447
+ -> dict[str, list[ElnExperiment]]:
448
+ """
449
+ Run a custom report that retrieves every experiment in the system with a given list of creators.
450
+
451
+ :param context: The current webhook context or a user object to send requests from.
452
+ :param created_by: The usernames of the creator of the experiments to query for.
453
+ :param criteria: Additional search criteria to filter the results.
454
+ :return: A dictionary mapping the owner username to a list of every experiment in the system from that creator
455
+ that matches the given criteria.
456
+ """
457
+ criteria.created_by = created_by
458
+ experiments: list[ElnExperiment] = ExperimentReportUtil.get_experiments_for_criteria(context, criteria)
459
+
460
+ # Ensure that each name appears in the dictionary, even if it has no experiments.
461
+ ret_val: dict[str, list[ElnExperiment]] = {x: [] for x in created_by}
462
+ for experiment in experiments:
463
+ ret_val.get(experiment.created_by).append(experiment)
464
+ return ret_val
465
+
466
+ @staticmethod
467
+ def build_experiment_id_report(criteria: ExperimentReportCriteria = ExperimentReportCriteria()) \
468
+ -> CustomReportCriteria:
469
+ """
470
+ Construct a custom report using the provided ExperimentReportCriteria.
471
+
472
+ :param criteria: The criteria to construct a custom report from.
473
+ :return: A custom report that can be used to search for experiment IDs that match the given criteria.
474
+ """
475
+ dt: str = NotebookExperimentPseudoDef.DATA_TYPE_NAME
476
+ report_builder = CustomReportBuilder(dt)
477
+ report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME)
478
+
479
+ root: AbstractReportTerm | None = None
480
+ if criteria.notebook_ids is not None:
481
+ root = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME, criteria.notebook_ids)
482
+ if criteria.not_notebook_ids is not None:
483
+ term = TermBuilder.not_term(dt, NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME, criteria.not_notebook_ids)
484
+ if root is not None:
485
+ root = TermBuilder.and_terms(root, term)
486
+ if root is None:
487
+ root = ExperimentReportUtil.all_notebook_experiments_term()
488
+
489
+ if criteria.names is not None:
490
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.EXPERIMENT_NAME__FIELD_NAME, criteria.names)
491
+ root = TermBuilder.and_terms(root, term)
492
+ if criteria.not_names is not None:
493
+ term = TermBuilder.not_term(dt, NotebookExperimentPseudoDef.EXPERIMENT_NAME__FIELD_NAME, criteria.not_names)
494
+ root = TermBuilder.and_terms(root, term)
495
+
496
+ if criteria.created_by is not None:
497
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.CREATED_BY__FIELD_NAME, criteria.created_by)
498
+ root = TermBuilder.and_terms(root, term)
499
+ if criteria.not_created_by is not None:
500
+ term = TermBuilder.not_term(dt, NotebookExperimentPseudoDef.CREATED_BY__FIELD_NAME, criteria.not_created_by)
501
+ root = TermBuilder.and_terms(root, term)
502
+
503
+ if criteria.last_modified_by is not None:
504
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.LAST_MODIFIED_BY__FIELD_NAME, criteria.last_modified_by)
505
+ root = TermBuilder.and_terms(root, term)
506
+ if criteria.not_last_modified_by is not None:
507
+ term = TermBuilder.not_term(dt, NotebookExperimentPseudoDef.LAST_MODIFIED_BY__FIELD_NAME, criteria.not_last_modified_by)
508
+ root = TermBuilder.and_terms(root, term)
509
+
510
+ if criteria.owners is not None:
511
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.EXPERIMENT_OWNER__FIELD_NAME, criteria.owners)
512
+ root = TermBuilder.and_terms(root, term)
513
+ if criteria.not_owners is not None:
514
+ term = TermBuilder.not_term(dt, NotebookExperimentPseudoDef.EXPERIMENT_OWNER__FIELD_NAME, criteria.not_owners)
515
+ root = TermBuilder.and_terms(root, term)
516
+
517
+ if criteria.statuses is not None:
518
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.STATUS__FIELD_NAME, criteria.statuses)
519
+ root = TermBuilder.and_terms(root, term)
520
+ if criteria.not_statuses is not None:
521
+ term = TermBuilder.not_term(dt, NotebookExperimentPseudoDef.STATUS__FIELD_NAME, criteria.not_statuses)
522
+ root = TermBuilder.and_terms(root, term)
523
+
524
+ # For the template name term, we need to join on the experiment record.
525
+ if criteria.templates is not None or criteria.not_templates is not None:
526
+ join = TermBuilder.compare_is_term("ELNExperiment",
527
+ "RecordId",
528
+ NotebookExperimentPseudoDef.DATA_TYPE_NAME,
529
+ NotebookExperimentPseudoDef.EXPERIMENT_RECORD_ID__FIELD_NAME)
530
+ report_builder.add_join(join)
531
+ if criteria.templates is not None:
532
+ term = TermBuilder.is_term("ELNExperiment", "TemplateExperimentName", criteria.templates)
533
+ root = TermBuilder.and_terms(root, term)
534
+ if criteria.not_templates is not None:
535
+ term = TermBuilder.not_term("ELNExperiment", "TemplateExperimentName", criteria.not_templates)
536
+ root = TermBuilder.and_terms(root, term)
537
+
538
+ if criteria.is_from_template is not None:
539
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.IS_TEMPLATE__FIELD_NAME, criteria.is_from_template)
540
+ root = TermBuilder.and_terms(root, term)
541
+ if criteria.is_from_protocol_template is not None:
542
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.IS_PROTOCOL_TEMPLATE__FIELD_NAME, criteria.is_from_protocol_template)
543
+ root = TermBuilder.and_terms(root, term)
544
+ if criteria.is_modifiable is not None:
545
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.IS_MODIFIABLE__FIELD_NAME, criteria.is_modifiable)
546
+ root = TermBuilder.and_terms(root, term)
547
+ if criteria.is_active is not None:
548
+ term = TermBuilder.is_term(dt, NotebookExperimentPseudoDef.IS_ACTIVE__FIELD_NAME, criteria.is_active)
549
+ root = TermBuilder.and_terms(root, term)
550
+
551
+ if criteria.created_after is not None:
552
+ term = TermBuilder.gte_term(dt, NotebookExperimentPseudoDef.DATE_CREATED__FIELD_NAME, criteria.created_after)
553
+ root = TermBuilder.and_terms(root, term)
554
+ if criteria.created_before is not None:
555
+ term = TermBuilder.lte_term(dt, NotebookExperimentPseudoDef.DATE_CREATED__FIELD_NAME, criteria.created_before)
556
+ root = TermBuilder.and_terms(root, term)
557
+
558
+ if criteria.last_modified_after is not None:
559
+ term = TermBuilder.gte_term(dt, NotebookExperimentPseudoDef.LAST_MODIFIED_DATE__FIELD_NAME, criteria.last_modified_after)
560
+ root = TermBuilder.and_terms(root, term)
561
+ if criteria.last_modified_before is not None:
562
+ term = TermBuilder.lte_term(dt, NotebookExperimentPseudoDef.LAST_MODIFIED_DATE__FIELD_NAME, criteria.last_modified_before)
563
+ root = TermBuilder.and_terms(root, term)
564
+
565
+ if criteria.due_after is not None:
566
+ term = TermBuilder.gte_term(dt, NotebookExperimentPseudoDef.APPROVAL_DUE_DATE__FIELD_NAME, criteria.due_after)
567
+ root = TermBuilder.and_terms(root, term)
568
+ if criteria.due_before is not None:
569
+ term = TermBuilder.lte_term(dt, NotebookExperimentPseudoDef.APPROVAL_DUE_DATE__FIELD_NAME, criteria.due_before)
570
+ root = TermBuilder.and_terms(root, term)
571
+
572
+ report_builder.set_root_term(root)
573
+ return report_builder.build_report_criteria()
574
+
575
+ @staticmethod
576
+ def get_experiments_for_report(context: UserIdentifier, report: CustomReportCriteria) -> list[ElnExperiment]:
577
+ """
578
+ Retrieve the ELN experiment objects for experiments whose notebook IDs appear in a custom report.
579
+
580
+ :param context: The current webhook context or a user object to send requests from.
581
+ :param report: A custom report that searches for ELN experiments, containing an "Experiment ID" column to
582
+ query for the experiments of.
583
+ :return: The ELN experiments that match the provided report.
584
+ """
585
+ user = AliasUtil.to_sapio_user(context)
586
+ exp_ids: list[int] = []
587
+ for row in CustomReportUtil.run_custom_report(user, report):
588
+ exp_ids.append(row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name])
589
+ if not exp_ids:
590
+ return []
591
+
592
+ criteria = ElnExperimentQueryCriteria(notebook_experiment_id_white_list=exp_ids)
593
+ return ElnManager(user).get_eln_experiment_by_criteria(criteria)
594
+
595
+ @staticmethod
596
+ def all_notebook_experiments_term() -> RawReportTerm:
597
+ """
598
+ :return: A report term searching for all notebook experiments.
599
+ """
600
+ return TermBuilder.gte_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
601
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
602
+ 0)
603
+
81
604
  @staticmethod
82
605
  def __get_record_experiment_relation_rows(user: SapioUser, data_type_name: str, record_ids: list[int] | None = None,
83
- exp_ids: list[int] | None = None) -> list[dict[str, int]]:
606
+ exp_ids: list[int] | None = None) -> list[dict[str, FieldValue]]:
84
607
  """
85
608
  Return a list of dicts mapping \"RECORDID\" to the record id and \"EXPERIMENTID\" to the experiment id.
86
609
  At least one of record_ids and exp_ids should be provided.
@@ -88,31 +611,43 @@ class ExperimentReportUtil:
88
611
  assert (record_ids or exp_ids)
89
612
 
90
613
  if record_ids:
91
- records_term = TermBuilder.is_term(data_type_name, "RECORDID", record_ids)
614
+ records_term = TermBuilder.is_term(data_type_name, "RecordId", record_ids)
92
615
  else:
93
616
  # Get all records of the given type
94
617
  records_term = TermBuilder.all_records_term(data_type_name)
95
618
 
96
619
  if exp_ids:
97
- exp_term = TermBuilder.is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", exp_ids)
620
+ exp_term = TermBuilder.is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
621
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
622
+ exp_ids)
98
623
  else:
99
624
  # Get all experiments
100
- exp_term = TermBuilder.gte_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "0")
625
+ exp_term = ExperimentReportUtil.all_notebook_experiments_term()
101
626
 
102
627
  root_term = TermBuilder.and_terms(records_term, exp_term)
103
628
 
104
629
  # Join records on the experiment entry records that correspond to them.
105
- records_entry_join = TermBuilder.compare_is_term("EXPERIMENTENTRYRECORD", "RECORDID", data_type_name, "RECORDID")
630
+ records_entry_join = TermBuilder.compare_is_term(data_type_name,
631
+ "RecordId",
632
+ ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME,
633
+ ExperimentEntryRecordPseudoDef.RECORD_ID__FIELD_NAME)
106
634
  # Join entry records on the experiment entries they are in.
107
- experiment_entry_enb_entry_join = TermBuilder.compare_is_term("ENBENTRY", "ENTRYID", "EXPERIMENTENTRYRECORD", "ENTRYID")
635
+ experiment_entry_enb_entry_join = TermBuilder.compare_is_term(EnbEntryPseudoDef.DATA_TYPE_NAME,
636
+ EnbEntryPseudoDef.ENTRY_ID__FIELD_NAME,
637
+ ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME,
638
+ ExperimentEntryRecordPseudoDef.ENTRY_ID__FIELD_NAME)
108
639
  # Join entries on the experiments they are in.
109
- enb_entry_experiment_join = TermBuilder.compare_is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "ENBENTRY", "EXPERIMENTID")
640
+ enb_entry_experiment_join = TermBuilder.compare_is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
641
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
642
+ EnbEntryPseudoDef.DATA_TYPE_NAME,
643
+ EnbEntryPseudoDef.EXPERIMENT_ID__FIELD_NAME)
110
644
 
111
645
  report_builder = CustomReportBuilder(data_type_name)
112
646
  report_builder.set_root_term(root_term)
113
- report_builder.add_column("RECORDID", FieldType.LONG, data_type=data_type_name)
114
- report_builder.add_column("EXPERIMENTID", FieldType.LONG, data_type="NOTEBOOKEXPERIMENT")
115
- report_builder.add_join(records_entry_join)
116
- report_builder.add_join(experiment_entry_enb_entry_join)
117
- report_builder.add_join(enb_entry_experiment_join)
647
+ report_builder.add_column("RecordId", FieldType.LONG)
648
+ report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
649
+ data_type=NotebookExperimentPseudoDef.DATA_TYPE_NAME)
650
+ report_builder.add_join(records_entry_join, ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME)
651
+ report_builder.add_join(experiment_entry_enb_entry_join, EnbEntryPseudoDef.DATA_TYPE_NAME)
652
+ report_builder.add_join(enb_entry_experiment_join, NotebookExperimentPseudoDef.DATA_TYPE_NAME)
118
653
  return CustomReportUtil.run_custom_report(user, report_builder.build_report_criteria())