sapiopycommons 2024.3.18a156__py3-none-any.whl → 2025.1.17a402__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.

Files changed (52) hide show
  1. sapiopycommons/callbacks/__init__.py +0 -0
  2. sapiopycommons/callbacks/callback_util.py +2041 -0
  3. sapiopycommons/callbacks/field_builder.py +545 -0
  4. sapiopycommons/chem/IndigoMolecules.py +52 -5
  5. sapiopycommons/chem/Molecules.py +114 -30
  6. sapiopycommons/customreport/__init__.py +0 -0
  7. sapiopycommons/customreport/column_builder.py +60 -0
  8. sapiopycommons/customreport/custom_report_builder.py +137 -0
  9. sapiopycommons/customreport/term_builder.py +315 -0
  10. sapiopycommons/datatype/attachment_util.py +17 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +390 -90
  14. sapiopycommons/eln/experiment_report_util.py +649 -0
  15. sapiopycommons/eln/plate_designer.py +152 -0
  16. sapiopycommons/files/complex_data_loader.py +31 -0
  17. sapiopycommons/files/file_bridge.py +153 -25
  18. sapiopycommons/files/file_bridge_handler.py +555 -0
  19. sapiopycommons/files/file_data_handler.py +633 -0
  20. sapiopycommons/files/file_util.py +270 -158
  21. sapiopycommons/files/file_validator.py +569 -0
  22. sapiopycommons/files/file_writer.py +377 -0
  23. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  24. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  25. sapiopycommons/general/accession_service.py +375 -0
  26. sapiopycommons/general/aliases.py +259 -18
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +252 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +85 -18
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +97 -7
  35. sapiopycommons/multimodal/multimodal.py +146 -0
  36. sapiopycommons/multimodal/multimodal_data.py +490 -0
  37. sapiopycommons/processtracking/__init__.py +0 -0
  38. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  39. sapiopycommons/processtracking/endpoints.py +192 -0
  40. sapiopycommons/recordmodel/record_handler.py +653 -149
  41. sapiopycommons/rules/eln_rule_handler.py +89 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +89 -12
  43. sapiopycommons/sftpconnect/__init__.py +0 -0
  44. sapiopycommons/sftpconnect/sftp_builder.py +70 -0
  45. sapiopycommons/webhook/webhook_context.py +39 -0
  46. sapiopycommons/webhook/webhook_handlers.py +617 -69
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
  49. sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
  50. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,185 @@
1
+ from enum import Enum
2
+
3
+ from sapiopylib.rest.User import SapioUser
4
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria
5
+
6
+ from sapiopycommons.customreport.column_builder import ColumnBuilder
7
+ from sapiopycommons.customreport.term_builder import TermBuilder
8
+ from sapiopycommons.datatype.pseudo_data_types import AuditLogPseudoDef
9
+ from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
10
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
11
+
12
+
13
+ class EventType(Enum):
14
+ """An enum to represent the possible event type values with the event type column in the audit log table."""
15
+ ADD = 0
16
+ DELETE = 1
17
+ MODIFY = 2
18
+ INFO = 3
19
+ ERROR = 4
20
+ WARNING = 5
21
+ IMPORT = 6
22
+ GENERATE = 7
23
+ EXPORT = 8
24
+ ADDREF = 9
25
+ REMOVEREF = 10
26
+ ESIGNATURE = 11
27
+ ROLEASSIGNMENT = 12
28
+
29
+
30
+ class AuditLogEntry:
31
+ __event_type: EventType
32
+ __date: int
33
+ __data_type_name: str
34
+ __record_id: int
35
+ __description: str
36
+ __users_login_name: str
37
+ __comment: str
38
+ __data_record_name: str
39
+ __data_field_name: str
40
+ __original_value: str
41
+ __new_value: str
42
+
43
+ @property
44
+ def event_type(self) -> EventType:
45
+ return self.__event_type
46
+
47
+ @property
48
+ def date(self) -> int:
49
+ return self.__date
50
+
51
+ @property
52
+ def data_type_name(self) -> str:
53
+ return self.__data_type_name
54
+
55
+ @property
56
+ def record_id(self) -> int:
57
+ return self.__record_id
58
+
59
+ @property
60
+ def description(self) -> str:
61
+ return self.__description
62
+
63
+ @property
64
+ def users_login_name(self) -> str:
65
+ return self.__users_login_name
66
+
67
+ @property
68
+ def comment(self) -> str:
69
+ return self.__comment
70
+
71
+ @property
72
+ def data_record_name(self) -> str:
73
+ return self.__data_record_name
74
+
75
+ @property
76
+ def data_field_name(self) -> str:
77
+ return self.__data_field_name
78
+
79
+ @property
80
+ def original_value(self) -> str:
81
+ return self.__original_value
82
+
83
+ @property
84
+ def new_value(self) -> str:
85
+ return self.__new_value
86
+
87
+ def __init__(self, report_row: dict[str, FieldValue]):
88
+ self.__event_type = EventType((report_row[AuditLogPseudoDef.EVENT_TYPE__FIELD_NAME.field_name]))
89
+ self.__date = report_row[AuditLogPseudoDef.TIME_STAMP__FIELD_NAME.field_name]
90
+ self.__data_type_name = report_row[AuditLogPseudoDef.DATA_TYPE_NAME__FIELD_NAME.field_name]
91
+ self.__record_id = report_row[AuditLogPseudoDef.RECORD_ID__FIELD_NAME.field_name]
92
+ self.__description = report_row[AuditLogPseudoDef.DESCRIPTION__FIELD_NAME.field_name]
93
+ self.__users_login_name = report_row[AuditLogPseudoDef.USER_NAME__FIELD_NAME.field_name]
94
+ self.__comment = report_row[AuditLogPseudoDef.USER_COMMENT__FIELD_NAME.field_name]
95
+ self.__data_record_name = report_row[AuditLogPseudoDef.RECORD_NAME__FIELD_NAME.field_name]
96
+ self.__data_field_name = report_row[AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME.field_name]
97
+ self.__original_value = report_row[AuditLogPseudoDef.ORIGINAL_VALUE__FIELD_NAME.field_name]
98
+ self.__new_value = report_row[AuditLogPseudoDef.NEW_VALUE__FIELD_NAME.field_name]
99
+
100
+
101
+ class AuditLogUtil:
102
+ user: SapioUser
103
+
104
+ def __init__(self, context: UserIdentifier):
105
+ self.user = AliasUtil.to_sapio_user(context)
106
+
107
+ @staticmethod
108
+ def report_columns() -> list[ReportColumn]:
109
+ return [
110
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.EVENT_TYPE__FIELD_NAME),
111
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.TIME_STAMP__FIELD_NAME),
112
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DATA_TYPE_NAME__FIELD_NAME),
113
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.RECORD_ID__FIELD_NAME),
114
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DESCRIPTION__FIELD_NAME),
115
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.USER_NAME__FIELD_NAME),
116
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.USER_COMMENT__FIELD_NAME),
117
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.RECORD_NAME__FIELD_NAME),
118
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME),
119
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.ORIGINAL_VALUE__FIELD_NAME),
120
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.NEW_VALUE__FIELD_NAME)
121
+ ]
122
+
123
+ @staticmethod
124
+ def create_data_record_audit_log_report(records: list[RecordIdentifier],
125
+ fields: list[FieldIdentifier] | None = None) -> CustomReportCriteria:
126
+ """
127
+ This method creates a CustomReportCriteria object for running an audit log query based on data records.
128
+
129
+ Creates a CustomReportCriteria object with a query term based on the record ids/records passed into the method.
130
+ Optionally, the fields parameter can be populated to limit the search to particular fields. If the fields
131
+ parameter is not populated, the search will include results for all field changes.
132
+
133
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
134
+ :param fields: The data field names to include changes for.
135
+ :return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
136
+ """
137
+ # Build the raw report term querying for any entry with a matching record ID value to the record ID's
138
+ # passed in.
139
+ record_ids = AliasUtil.to_record_ids(records)
140
+ tb = TermBuilder(AuditLogPseudoDef.DATA_TYPE_NAME)
141
+ root_term = tb.is_term(AuditLogPseudoDef.RECORD_ID__FIELD_NAME, record_ids)
142
+
143
+ # If the user passed in any specific fields, then we should limit the query to those fields.
144
+ if fields is not None and fields:
145
+ fields: list[str] = AliasUtil.to_data_field_names(fields)
146
+ field_term = tb.is_term(AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME, fields)
147
+ root_term = TermBuilder.and_terms(root_term, field_term)
148
+
149
+ return CustomReportCriteria(AuditLogUtil.report_columns(), root_term)
150
+
151
+ def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
152
+ fields: list[FieldIdentifier] | None = None) \
153
+ -> dict[RecordIdentifier, list[AuditLogEntry]]:
154
+ """
155
+ This method runs a custom report for changes made to the given data records using the audit log.
156
+ See "create_data_record_audit_log_report" for more details about the data record audit log report.
157
+
158
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
159
+ :param fields: The data field names to include changes for.
160
+ :return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
161
+ AuditLogEntry objects which match the record id value of those records.
162
+ """
163
+ # First, we must build our report criteria for running the Custom Report.
164
+ criteria = AuditLogUtil.create_data_record_audit_log_report(records, fields)
165
+
166
+ # Then we must run the custom report using that criteria.
167
+ raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
168
+
169
+ # This section will prepare a map matching the original RecordIdentifier by record id.
170
+ # This is because the audit log entries will have record ids, but we want the keys in our result map
171
+ # to match the record identifiers that the user passed in, for convenience.
172
+ record_identifier_mapping: dict[int, RecordIdentifier] = dict()
173
+ for record in records:
174
+ record_id = AliasUtil.to_record_id(record)
175
+ record_identifier_mapping[record_id] = record
176
+
177
+ # Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
178
+ # and the value is a list of applicable audit log entries.
179
+ final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
180
+ for audit_entry_data in raw_report_data:
181
+ audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
182
+ identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
183
+ final_audit_data.setdefault(identifier, []).append(audit_entry)
184
+
185
+ return final_audit_data
@@ -1,40 +1,281 @@
1
1
  from collections.abc import Iterable
2
- from typing import Any
3
2
 
4
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
5
- from sapiopylib.rest.pojo.CustomReport import ReportColumn
6
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
4
+ from sapiopylib.rest.User import SapioUser
5
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport, CustomReportCriteria, RawReportTerm
6
+
7
+ from sapiopycommons.general.aliases import UserIdentifier, FieldValue, AliasUtil, FieldIdentifierKey
7
8
 
8
9
 
9
10
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
10
11
  class CustomReportUtil:
11
12
  @staticmethod
12
- def run_system_report(context: SapioWebhookContext,
13
+ def run_system_report(context: UserIdentifier,
13
14
  report_name: str,
14
- filters: dict[str, Iterable[Any]] | None = None,
15
- page_limit: int | None = None) -> list[dict[str, Any]]:
15
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
16
+ page_limit: int | None = None,
17
+ page_size: int | None = None,
18
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
16
19
  """
17
20
  Run a system report and return the results of that report as a list of dictionaries for the values of each
18
21
  column in each row.
19
- :param context: The current webhook context.
22
+
23
+ System reports are also known as predefined searches in the system and must be defined in the data designer for
24
+ a specific data type. That is, saved searches created by users cannot be run using this function.
25
+
26
+ :param context: The current webhook context or a user object to send requests from.
20
27
  :param report_name: The name of the system report to run.
21
28
  :param filters: If provided, filter the results of the report using the given mapping of headers to values to
22
29
  filter on. Only those headers that both the filters and the custom report share will take effect. That is,
23
30
  any filters that have a header name that isn't in the custom report will be ignored.
24
31
  :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
32
+ :param page_size: The size of each page of results in the search. If None, the page size is set by the server,
33
+ which may be unlimited.
34
+ :param page_number: The page number to start the search from, If None, starts on the first page. Note that the
35
+ number of the first page is 0.
36
+ :return: The results of the report listed row by row, mapping each cell to the header it is under. The header
37
+ values in the dicts are the data field names of the columns.
38
+ If two columns in the search have the same data field name but differing data type names, then the
39
+ dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
40
+ had a Sample column with a data field name of Identifier and a Request column with the same data field name,
41
+ then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
42
+ """
43
+ results: tuple = CustomReportUtil._exhaust_system_report(context, report_name, page_limit,
44
+ page_size, page_number)
45
+ columns: list[ReportColumn] = results[0]
46
+ rows: list[list[FieldValue]] = results[1]
47
+ return CustomReportUtil._process_results(rows, columns, filters)
48
+
49
+ @staticmethod
50
+ def run_custom_report(context: UserIdentifier,
51
+ report_criteria: CustomReportCriteria,
52
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
53
+ page_limit: int | None = None,
54
+ page_size: int | None = None,
55
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
56
+ """
57
+ Run a custom report and return the results of that report as a list of dictionaries for the values of each
58
+ column in each row.
59
+
60
+ Custom reports are constructed by the caller, specifying the report terms and the columns that will be in the
61
+ results. They are like advanced or predefined searches from the system, except they are constructed from
62
+ within the webhook instead of from within the system.
63
+
64
+ :param context: The current webhook context or a user object to send requests from.
65
+ :param report_criteria: The custom report criteria to run.
66
+ :param filters: If provided, filter the results of the report using the given mapping of headers to values to
67
+ filter on. Only those headers that both the filters and the custom report share will take effect. That is,
68
+ any filters that have a header name that isn't in the custom report will be ignored.
69
+ Note that this parameter is only provided for parity with the other run report functions. If you need to
70
+ filter the results of a search, it would likely be more beneficial to have just added a new term to the
71
+ input report criteria that corresponds to the filter.
72
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
73
+ :param page_size: The size of each page of results in the search. If None, uses the value from the given report
74
+ criteria. If not None, overwrites the value from the given report criteria.
75
+ :param page_number: The page number to start the search from, If None, uses the value from the given report
76
+ criteria. If not None, overwrites the value from the given report criteria. Note that the number of the
77
+ first page is 0.
25
78
  :return: The results of the report listed row by row, mapping each cell to the header it is under. The header
26
79
  values in the dicts are the data field names of the columns.
80
+ If two columns in the search have the same data field name but differing data type names, then the
81
+ dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
82
+ had a Sample column with a data field name of Identifier and a Request column with the same data field name,
83
+ then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
27
84
  """
28
- results = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit)
85
+ results: tuple = CustomReportUtil._exhaust_custom_report(context, report_criteria, page_limit,
86
+ page_size, page_number)
29
87
  columns: list[ReportColumn] = results[0]
30
- rows: list[list[Any]] = results[1]
88
+ rows: list[list[FieldValue]] = results[1]
89
+ return CustomReportUtil._process_results(rows, columns, filters)
90
+
91
+ @staticmethod
92
+ def run_quick_report(context: UserIdentifier,
93
+ report_term: RawReportTerm,
94
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
95
+ page_limit: int | None = None,
96
+ page_size: int | None = None,
97
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
98
+ """
99
+ Run a quick report and return the results of that report as a list of dictionaries for the values of each
100
+ column in each row.
101
+
102
+ Quick reports are helpful for cases where you need to query record field values in a more complex manner than
103
+ the data record manager allows, but still simpler than a full-blown custom report. The columns that are returned
104
+ in a quick search are every visible field from the data type that corresponds to the given report term. (Fields
105
+ which are not marked as visible in the data designer will be excluded.)
106
+
107
+ :param context: The current webhook context or a user object to send requests from.
108
+ :param report_term: The raw report term to use for the quick report.
109
+ :param filters: If provided, filter the results of the report using the given mapping of headers to values to
110
+ filter on. Only those headers that both the filters and the custom report share will take effect. That is,
111
+ any filters that have a header name that isn't in the custom report will be ignored.
112
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
113
+ :param page_size: The size of each page of results in the search. If None, the page size is set by the server,
114
+ which may be unlimited.
115
+ :param page_number: The page number to start the search from, If None, starts on the first page. Note that the
116
+ number of the first page is 0.
117
+ :return: The results of the report listed row by row, mapping each cell to the header it is under. The header
118
+ values in the dicts are the data field names of the columns.
119
+ """
120
+ results: tuple = CustomReportUtil._exhaust_quick_report(context, report_term, page_limit,
121
+ page_size, page_number)
122
+ columns: list[ReportColumn] = results[0]
123
+ rows: list[list[FieldValue]] = results[1]
124
+ return CustomReportUtil._process_results(rows, columns, filters)
125
+
126
+ @staticmethod
127
+ def get_system_report_criteria(context: UserIdentifier, report_name: str) -> CustomReport:
128
+ """
129
+ Retrieve a custom report from the system given the name of the report. This works by querying the system report
130
+ with a page number and size of 1 to minimize the amount of data transfer needed to retrieve the report's config.
131
+
132
+ System reports are also known as predefined searches in the system and must be defined in the data designer for
133
+ a specific data type. That is, saved searches created by users cannot be run using this function.
134
+
135
+ Using this, you can add to the root term of the search to then run a new search, or provide it to client
136
+ callbacks or directives that take CustomReports.
137
+
138
+ :param context: The current webhook context or a user object to send requests from.
139
+ :param report_name: The name of the system report to run.
140
+ :return: The CustomReport object for the given system report name.
141
+ """
142
+ user: SapioUser = AliasUtil.to_sapio_user(context)
143
+ report_man = DataMgmtServer.get_custom_report_manager(user)
144
+ return report_man.run_system_report_by_name(report_name, 1, 0)
31
145
 
32
- ret: list[dict[str, Any]] = []
146
+ @staticmethod
147
+ def _exhaust_system_report(context: UserIdentifier,
148
+ report_name: str,
149
+ page_limit: int | None,
150
+ page_size: int | None,
151
+ page_number: int | None) \
152
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
153
+ """
154
+ Given a system report, iterate over every page of the report and collect the results
155
+ until there are no remaining pages.
156
+ """
157
+ user: SapioUser = AliasUtil.to_sapio_user(context)
158
+ report_man = DataMgmtServer.get_custom_report_manager(user)
159
+
160
+ # If a page size was provided but no page number was provided, then set the page number to 0,
161
+ # as both parameters are necessary in order to get paged results.
162
+ if page_size is not None and page_number is None:
163
+ page_number = 0
164
+
165
+ result = None
166
+ has_next_page: bool = True
167
+ rows: list[list[FieldValue]] = []
168
+ cur_page: int = 1
169
+ while has_next_page and (not page_limit or cur_page <= page_limit):
170
+ result = report_man.run_system_report_by_name(report_name, page_size, page_number)
171
+ page_size = result.page_size
172
+ page_number = result.page_number + 1
173
+ has_next_page = result.has_next_page
174
+ rows.extend(result.result_table)
175
+ cur_page += 1
176
+ return result.column_list, rows
177
+
178
+ @staticmethod
179
+ def _exhaust_custom_report(context: UserIdentifier,
180
+ report: CustomReportCriteria,
181
+ page_limit: int | None,
182
+ page_size: int | None,
183
+ page_number: int | None) \
184
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
185
+ """
186
+ Given a custom report, iterate over every page of the report and collect the results
187
+ until there are no remaining pages.
188
+ """
189
+ user: SapioUser = AliasUtil.to_sapio_user(context)
190
+ report_man = DataMgmtServer.get_custom_report_manager(user)
191
+
192
+ # If a page size was provided but no page number was provided, then set the page number to 0,
193
+ # as both parameters are necessary in order to get paged results.
194
+ if page_size is not None and page_number is None:
195
+ page_number = 0
196
+
197
+ result = None
198
+ if page_size is not None:
199
+ report.page_size = page_size
200
+ if page_number is not None:
201
+ report.page_number = page_number
202
+ has_next_page: bool = True
203
+ rows: list[list[FieldValue]] = []
204
+ cur_page: int = 1
205
+ while has_next_page and (not page_limit or cur_page <= page_limit):
206
+ result = report_man.run_custom_report(report)
207
+ report.page_size = result.page_size
208
+ report.page_number = result.page_number + 1
209
+ has_next_page = result.has_next_page
210
+ rows.extend(result.result_table)
211
+ cur_page += 1
212
+ return result.column_list, rows
213
+
214
+ @staticmethod
215
+ def _exhaust_quick_report(context: UserIdentifier,
216
+ report_term: RawReportTerm,
217
+ page_limit: int | None,
218
+ page_size: int | None,
219
+ page_number: int | None) \
220
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
221
+ """
222
+ Given a quick report, iterate over every page of the report and collect the results
223
+ until there are no remaining pages.
224
+ """
225
+ user: SapioUser = AliasUtil.to_sapio_user(context)
226
+ report_man = DataMgmtServer.get_custom_report_manager(user)
227
+
228
+ # If a page size was provided but no page number was provided, then set the page number to 0,
229
+ # as both parameters are necessary in order to get paged results.
230
+ if page_size is not None and page_number is None:
231
+ page_number = 0
232
+
233
+ result = None
234
+ has_next_page: bool = True
235
+ rows: list[list[FieldValue]] = []
236
+ cur_page: int = 1
237
+ while has_next_page and (not page_limit or cur_page <= page_limit):
238
+ result = report_man.run_quick_report(report_term, page_size, page_number)
239
+ page_size = result.page_size
240
+ page_number = result.page_number + 1
241
+ has_next_page = result.has_next_page
242
+ rows.extend(result.result_table)
243
+ cur_page += 1
244
+ return result.column_list, rows
245
+
246
+ @staticmethod
247
+ def _process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
248
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
249
+ """
250
+ Given the results of a report as a list of row values and the report's columns, combine these lists to
251
+ result in a singular list of dictionaries for each row in the results.
252
+
253
+ If any filter criteria has been provided, also use that to filter the row.
254
+ """
255
+ # It may be the case that two columns have the same data field name but differing data type names.
256
+ # If this occurs, then we need to be able to differentiate these columns in the resulting dictionary.
257
+ prepend_dt: set[str] = set()
258
+ encountered_names: list[str] = []
259
+ for column in columns:
260
+ field_name: str = column.data_field_name
261
+ if field_name in encountered_names:
262
+ prepend_dt.add(field_name)
263
+ else:
264
+ encountered_names.append(field_name)
265
+
266
+ if filters:
267
+ filters: dict[str, Iterable[FieldValue]] = AliasUtil.to_data_field_names_dict(filters)
268
+
269
+ ret: list[dict[str, FieldValue]] = []
33
270
  for row in rows:
34
- row_data: dict[str, Any] = {}
271
+ row_data: dict[str, FieldValue] = {}
35
272
  filter_row: bool = False
36
273
  for value, column in zip(row, columns):
37
274
  header: str = column.data_field_name
275
+ # If two columns share the same data field name, prepend the data type name of the column to the
276
+ # data field name.
277
+ if header in prepend_dt:
278
+ header = column.data_type_name + "." + header
38
279
  if filters is not None and header in filters and value not in filters.get(header):
39
280
  filter_row = True
40
281
  break
@@ -42,23 +283,3 @@ class CustomReportUtil:
42
283
  if filter_row is False:
43
284
  ret.append(row_data)
44
285
  return ret
45
-
46
- @staticmethod
47
- def __exhaust_system_report(context: SapioWebhookContext, report_name: str, page_limit: int | None = None) \
48
- -> tuple[list[ReportColumn], list[list[Any]]]:
49
- report_man = DataMgmtServer.get_custom_report_manager(context.user)
50
-
51
- report = None
52
- page_size: int | None = None
53
- page_number: int | None = None
54
- has_next_page: bool = True
55
- rows: list[list[Any]] = []
56
- cur_page: int = 1
57
- while has_next_page and (not page_limit or cur_page < page_limit):
58
- report = report_man.run_system_report_by_name(report_name, page_size, page_number)
59
- page_size = report.page_size
60
- page_number = report.page_number
61
- has_next_page = report.has_next_page
62
- rows.extend(report.result_table)
63
- cur_page += 1
64
- return report.column_list, rows
@@ -0,0 +1,86 @@
1
+ from typing import Iterable, cast
2
+
3
+ from sapiopylib.rest.User import SapioUser
4
+ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, CustomReport
5
+ from sapiopylib.rest.pojo.webhook.WebhookDirective import HomePageDirective, FormDirective, TableDirective, \
6
+ CustomReportDirective, ElnExperimentDirective, ExperimentEntryDirective
7
+
8
+ from sapiopycommons.general.aliases import SapioRecord, AliasUtil, ExperimentIdentifier, ExperimentEntryIdentifier, \
9
+ UserIdentifier
10
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
11
+
12
+
13
+ # FR-47392: Create a DirectiveUtil class to simplify the creation of directives.
14
+ class DirectiveUtil:
15
+ """
16
+ DirectiveUtil is a class for creating webhook directives. The utility functions reduce the provided variables
17
+ down to the exact type that the directives require, removing the need for the caller to handle the conversion.
18
+ """
19
+ user: SapioUser
20
+
21
+ def __init__(self, context: UserIdentifier):
22
+ """
23
+ :param context: The current webhook context or a user object to send requests from.
24
+ """
25
+ self.user = AliasUtil.to_sapio_user(context)
26
+
27
+ @staticmethod
28
+ def homepage() -> HomePageDirective:
29
+ """
30
+ :return: A directive that sends the user back to their home page.
31
+ """
32
+ return HomePageDirective()
33
+
34
+ @staticmethod
35
+ def record_form(record: SapioRecord) -> FormDirective:
36
+ """
37
+ :param record: A record in the system.
38
+ :return: A directive that sends the user to a specific data record form.
39
+ """
40
+ return FormDirective(AliasUtil.to_data_record(record))
41
+
42
+ @staticmethod
43
+ def record_table(records: Iterable[SapioRecord]) -> TableDirective:
44
+ """
45
+ :param records: A list of records in the system.
46
+ :return: A directive that sends the user to a table of data records.
47
+ """
48
+ return TableDirective(AliasUtil.to_data_records(records))
49
+
50
+ @staticmethod
51
+ def record_adaptive(records: Iterable[SapioRecord]) -> TableDirective | FormDirective:
52
+ """
53
+ :param records: A list of records in the system.
54
+ :return: A directive that sends the user to a table of data records if there are multiple records,
55
+ or a directive that sends the user to a specific data record form if there is only one record.
56
+ """
57
+ records: list[SapioRecord] = list(records)
58
+ if len(records) == 1:
59
+ return DirectiveUtil.record_form(records[0])
60
+ return DirectiveUtil.record_table(records)
61
+
62
+ def custom_report(self, report: CustomReport | CustomReportCriteria | str) -> CustomReportDirective:
63
+ """
64
+ :param report: A custom report, the criteria for a custom report, or the name of a system report.
65
+ :return: A directive that sends the user to the results of the provided custom report.
66
+ """
67
+ if isinstance(report, str):
68
+ report: CustomReport = CustomReportUtil.get_system_report_criteria(self.user, report)
69
+ return CustomReportDirective(cast(CustomReport, report))
70
+
71
+ @staticmethod
72
+ def eln_experiment(experiment: ExperimentIdentifier) -> ElnExperimentDirective:
73
+ """
74
+ :param experiment: An identifier for an experiment.
75
+ :return: A directive that sends the user to the ELN experiment.
76
+ """
77
+ return ElnExperimentDirective(AliasUtil.to_notebook_id(experiment))
78
+
79
+ @staticmethod
80
+ def eln_entry(experiment: ExperimentIdentifier, entry: ExperimentEntryIdentifier) -> ExperimentEntryDirective:
81
+ """
82
+ :param experiment: An identifier for an experiment.
83
+ :param entry: An identifier for an entry in the experiment.
84
+ :return: A directive that sends the user to the provided experiment entry within its ELN experiment.
85
+ """
86
+ return ExperimentEntryDirective(AliasUtil.to_notebook_id(experiment), AliasUtil.to_entry_id(entry))