sapiopycommons 2024.10.25a345__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,7 +1,9 @@
1
1
  from sapiopylib.rest.ELNService import ElnManager
2
2
  from sapiopylib.rest.User import SapioUser
3
+ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, AbstractReportTerm, RawReportTerm
3
4
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
4
5
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, ElnExperimentQueryCriteria
6
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnExperimentStatus, ElnBaseDataType
5
7
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
6
8
 
7
9
  from sapiopycommons.customreport.custom_report_builder import CustomReportBuilder
@@ -11,9 +13,131 @@ from sapiopycommons.datatype.pseudo_data_types import EnbEntryOptionsPseudoDef,
11
13
  from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil, FieldValue, \
12
14
  ExperimentEntryIdentifier, ExperimentIdentifier
13
15
  from sapiopycommons.general.custom_report_util import CustomReportUtil
16
+ from sapiopycommons.general.exceptions import SapioException
14
17
  from sapiopycommons.recordmodel.record_handler import RecordHandler
15
18
 
16
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]
139
+
140
+
17
141
  # FR-46908 - Provide a utility class that holds experiment related custom reports e.g. getting all the experiments
18
142
  # that given records were used in or getting all records of a datatype used in given experiments.
19
143
  class ExperimentReportUtil:
@@ -34,7 +158,7 @@ class ExperimentReportUtil:
34
158
  user: SapioUser = AliasUtil.to_sapio_user(context)
35
159
  data_type_name: str = AliasUtil.to_singular_data_type_name(records)
36
160
 
37
- record_ids = [record.record_id for record in records]
161
+ record_ids: list[int] = AliasUtil.to_record_ids(records)
38
162
  rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, record_ids=record_ids)
39
163
 
40
164
  id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
@@ -47,6 +171,45 @@ class ExperimentReportUtil:
47
171
 
48
172
  return {record: list(exps) for record, exps in record_to_exps.items()}
49
173
 
174
+ @staticmethod
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
+
50
213
  @staticmethod
51
214
  def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[ExperimentIdentifier],
52
215
  wrapper_type: type[WrappedType]) -> dict[int, list[WrappedType]]:
@@ -148,52 +311,299 @@ class ExperimentReportUtil:
148
311
  return options
149
312
 
150
313
  @staticmethod
151
- def get_experiments_by_name(context: UserIdentifier, name: str) -> list[ElnExperiment]:
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]:
152
361
  """
153
362
  Run a custom report that retrieves every experiment in the system with a given name.
154
363
 
155
364
  :param context: The current webhook context or a user object to send requests from.
156
365
  :param name: The name of the experiment to query for.
157
- :return: A list of every experiment in the system with a name that matches the input.
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.
158
369
  """
159
- return ExperimentReportUtil.get_experiments_by_names(context, [name])[name]
370
+ return ExperimentReportUtil.get_experiments_by_names(context, [name], criteria)[name]
160
371
 
161
372
  @staticmethod
162
- def get_experiments_by_names(context: UserIdentifier, names: list[str]) -> dict[str, list[ElnExperiment]]:
373
+ def get_experiments_by_names(context: UserIdentifier, names: list[str],
374
+ criteria: ExperimentReportCriteria = ExperimentReportCriteria()) -> dict[str, list[ElnExperiment]]:
163
375
  """
164
376
  Run a custom report that retrieves every experiment in the system with a name from a list of names.
165
377
 
166
378
  :param context: The current webhook context or a user object to send requests from.
167
379
  :param names: The names of the experiment to query for.
168
- :return: A dictionary mapping the experiment name to a list of every experiment in the system with that name.
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.
169
383
  """
170
- user = AliasUtil.to_sapio_user(context)
384
+ criteria.names = names
385
+ experiments: list[ElnExperiment] = ExperimentReportUtil.get_experiments_for_criteria(context, criteria)
171
386
 
172
- report_builder = CustomReportBuilder(NotebookExperimentPseudoDef.DATA_TYPE_NAME)
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)
173
477
  report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME)
174
- root = TermBuilder.is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
175
- NotebookExperimentPseudoDef.EXPERIMENT_NAME__FIELD_NAME,
176
- names)
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
+
177
572
  report_builder.set_root_term(root)
573
+ return report_builder.build_report_criteria()
178
574
 
179
- # Ensure that each entry appears in the dictionary, even if it has no experiments.
180
- ret_val: dict[str, list[ElnExperiment]] = {x: [] for x in names}
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.
181
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)
182
586
  exp_ids: list[int] = []
183
- for row in CustomReportUtil.run_custom_report(user, report_builder.build_report_criteria()):
587
+ for row in CustomReportUtil.run_custom_report(user, report):
184
588
  exp_ids.append(row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name])
185
589
  if not exp_ids:
186
- return ret_val
590
+ return []
187
591
 
188
592
  criteria = ElnExperimentQueryCriteria(notebook_experiment_id_white_list=exp_ids)
189
- experiments: list[ElnExperiment] = ElnManager(user).get_eln_experiment_by_criteria(criteria)
190
- for experiment in experiments:
191
- ret_val.get(experiment.notebook_experiment_name).append(experiment)
192
- return ret_val
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)
193
603
 
194
604
  @staticmethod
195
605
  def __get_record_experiment_relation_rows(user: SapioUser, data_type_name: str, record_ids: list[int] | None = None,
196
- exp_ids: list[int] | None = None) -> list[dict[str, int]]:
606
+ exp_ids: list[int] | None = None) -> list[dict[str, FieldValue]]:
197
607
  """
198
608
  Return a list of dicts mapping \"RECORDID\" to the record id and \"EXPERIMENTID\" to the experiment id.
199
609
  At least one of record_ids and exp_ids should be provided.
@@ -212,17 +622,15 @@ class ExperimentReportUtil:
212
622
  exp_ids)
213
623
  else:
214
624
  # Get all experiments
215
- exp_term = TermBuilder.gte_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
216
- NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
217
- 0)
625
+ exp_term = ExperimentReportUtil.all_notebook_experiments_term()
218
626
 
219
627
  root_term = TermBuilder.and_terms(records_term, exp_term)
220
628
 
221
629
  # Join records on the experiment entry records that correspond to them.
222
- records_entry_join = TermBuilder.compare_is_term(ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME,
223
- ExperimentEntryRecordPseudoDef.RECORD_ID__FIELD_NAME,
224
- data_type_name,
225
- "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)
226
634
  # Join entry records on the experiment entries they are in.
227
635
  experiment_entry_enb_entry_join = TermBuilder.compare_is_term(EnbEntryPseudoDef.DATA_TYPE_NAME,
228
636
  EnbEntryPseudoDef.ENTRY_ID__FIELD_NAME,
@@ -239,7 +647,7 @@ class ExperimentReportUtil:
239
647
  report_builder.add_column("RecordId", FieldType.LONG)
240
648
  report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
241
649
  data_type=NotebookExperimentPseudoDef.DATA_TYPE_NAME)
242
- report_builder.add_join(records_entry_join)
243
- report_builder.add_join(experiment_entry_enb_entry_join)
244
- report_builder.add_join(enb_entry_experiment_join)
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)
245
653
  return CustomReportUtil.run_custom_report(user, report_builder.build_report_criteria())
@@ -6,6 +6,7 @@ from sapiopylib.rest.pojo.DataRecord import DataRecord
6
6
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
7
7
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
8
8
  from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
9
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
9
10
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
10
11
  from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol, ElnEntryStep
11
12
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
@@ -89,44 +90,50 @@ class AliasUtil:
89
90
  return record if isinstance(record, int) else record.record_id
90
91
 
91
92
  @staticmethod
92
- def to_data_type_name(value: DataTypeIdentifier) -> str:
93
+ def to_data_type_name(value: DataTypeIdentifier, convert_eln_dts: bool = True) -> str:
93
94
  """
94
95
  Convert a given value to a data type name.
95
96
 
96
97
  :param value: A value which is a string, record, or record model type.
98
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
97
99
  :return: A string of the data type name of the input value.
98
100
  """
99
- if isinstance(value, str):
100
- return value
101
101
  if isinstance(value, SapioRecord):
102
- return value.data_type_name
103
- return value.get_wrapper_data_type_name()
102
+ value = value.data_type_name
103
+ elif not isinstance(value, str):
104
+ value = value.get_wrapper_data_type_name()
105
+ if convert_eln_dts and ElnBaseDataType.is_eln_type(value):
106
+ return ElnBaseDataType.get_base_type(value).data_type_name
107
+ return value
104
108
 
105
109
  @staticmethod
106
- def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False) -> list[str] | set[str]:
110
+ def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False,
111
+ convert_eln_dts: bool = True) -> list[str] | set[str]:
107
112
  """
108
113
  Convert a given iterable of values to a list or set of data type names.
109
114
 
110
115
  :param values: An iterable of values which are strings, records, or record model types.
111
116
  :param return_set: If true, return a set instead of a list.
117
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
112
118
  :return: A list or set of strings of the data type name of the input value.
113
119
  """
114
- values = [AliasUtil.to_data_type_name(x) for x in values]
120
+ values = [AliasUtil.to_data_type_name(x, convert_eln_dts) for x in values]
115
121
  return set(values) if return_set else values
116
122
 
117
123
  @staticmethod
118
- def to_singular_data_type_name(values: Iterable[DataTypeIdentifier]) -> str:
124
+ def to_singular_data_type_name(values: Iterable[DataTypeIdentifier], convert_eln_dts: bool = True) -> str:
119
125
  """
120
126
  Convert a given iterable of values to a singular data type name that they share. Throws an exception if more
121
127
  than one data type name exists in the provided list of identifiers.
122
128
 
123
129
  :param values: An iterable of values which are strings, records, or record model types.
130
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
124
131
  :return: The single data type name that the input vales share. Returns an empty string if an empty iterable
125
132
  was provided.
126
133
  """
127
134
  if not values:
128
135
  return ""
129
- data_types: set[str] = AliasUtil.to_data_type_names(values, True)
136
+ data_types: set[str] = AliasUtil.to_data_type_names(values, True, convert_eln_dts)
130
137
  if len(data_types) > 1:
131
138
  raise SapioException(f"Provided values contain multiple data types: {data_types}. "
132
139
  f"Only expecting a single data type.")
@@ -38,9 +38,6 @@ class PyMolecule:
38
38
  normError: str | None
39
39
  desaltError: str | None
40
40
  desaltedList: list[str] | None
41
- registrationHash: str | None
42
- hasOrGroup: bool
43
- CXSMILESHash: str | None
44
41
 
45
42
 
46
43
  @dataclass
@@ -103,9 +100,9 @@ class PyMoleculeLoaderResult:
103
100
  compoundList: the compounds successfully loaded.
104
101
  errorList: an error record is added here for each one we failed to load in Sapio.
105
102
  """
106
- compoundByStr: dict[str, PyCompound] | None
107
- compoundList: list[PyCompound] | None
108
- errorList: list[ChemLoadingError] | None
103
+ compoundByStr: dict[str, PyCompound]
104
+ compoundList: list[PyCompound]
105
+ errorList: list[ChemLoadingError]
109
106
 
110
107
 
111
108
  @dataclass