sapiopycommons 2024.11.10a363__py3-none-any.whl → 2024.11.12a365__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 (45) hide show
  1. sapiopycommons/callbacks/callback_util.py +532 -83
  2. sapiopycommons/callbacks/field_builder.py +537 -0
  3. sapiopycommons/chem/IndigoMolecules.py +1 -0
  4. sapiopycommons/chem/Molecules.py +1 -0
  5. sapiopycommons/customreport/__init__.py +0 -0
  6. sapiopycommons/customreport/column_builder.py +60 -0
  7. sapiopycommons/customreport/custom_report_builder.py +130 -0
  8. sapiopycommons/customreport/term_builder.py +299 -0
  9. sapiopycommons/datatype/attachment_util.py +11 -10
  10. sapiopycommons/datatype/data_fields.py +61 -0
  11. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  12. sapiopycommons/eln/experiment_handler.py +272 -70
  13. sapiopycommons/eln/experiment_report_util.py +653 -0
  14. sapiopycommons/files/complex_data_loader.py +5 -4
  15. sapiopycommons/files/file_bridge.py +31 -24
  16. sapiopycommons/files/file_bridge_handler.py +340 -0
  17. sapiopycommons/files/file_data_handler.py +2 -5
  18. sapiopycommons/files/file_util.py +59 -9
  19. sapiopycommons/files/file_validator.py +92 -6
  20. sapiopycommons/files/file_writer.py +44 -15
  21. sapiopycommons/general/accession_service.py +375 -0
  22. sapiopycommons/general/aliases.py +207 -6
  23. sapiopycommons/general/audit_log.py +189 -0
  24. sapiopycommons/general/custom_report_util.py +212 -37
  25. sapiopycommons/general/exceptions.py +21 -8
  26. sapiopycommons/general/popup_util.py +21 -0
  27. sapiopycommons/general/sapio_links.py +50 -0
  28. sapiopycommons/general/time_util.py +8 -2
  29. sapiopycommons/multimodal/multimodal.py +146 -0
  30. sapiopycommons/multimodal/multimodal_data.py +486 -0
  31. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  32. sapiopycommons/processtracking/endpoints.py +22 -22
  33. sapiopycommons/recordmodel/record_handler.py +481 -97
  34. sapiopycommons/rules/eln_rule_handler.py +34 -25
  35. sapiopycommons/rules/on_save_rule_handler.py +34 -31
  36. sapiopycommons/sftpconnect/__init__.py +0 -0
  37. sapiopycommons/sftpconnect/sftp_builder.py +69 -0
  38. sapiopycommons/webhook/webhook_context.py +39 -0
  39. sapiopycommons/webhook/webhook_handlers.py +201 -42
  40. sapiopycommons/webhook/webservice_handlers.py +67 -0
  41. {sapiopycommons-2024.11.10a363.dist-info → sapiopycommons-2024.11.12a365.dist-info}/METADATA +4 -2
  42. sapiopycommons-2024.11.12a365.dist-info/RECORD +57 -0
  43. sapiopycommons-2024.11.10a363.dist-info/RECORD +0 -38
  44. {sapiopycommons-2024.11.10a363.dist-info → sapiopycommons-2024.11.12a365.dist-info}/WHEEL +0 -0
  45. {sapiopycommons-2024.11.10a363.dist-info → sapiopycommons-2024.11.12a365.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,189 @@
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]
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
+ root_term = TermBuilder.is_term(AuditLogPseudoDef.DATA_TYPE_NAME,
141
+ AuditLogPseudoDef.RECORD_ID__FIELD_NAME,
142
+ record_ids)
143
+
144
+ # If the user passed in any specific fields, then we should limit the query to those fields.
145
+ if fields:
146
+ fields: list[str] = AliasUtil.to_data_field_names(fields)
147
+ field_term = TermBuilder.is_term(AuditLogPseudoDef.DATA_TYPE_NAME,
148
+ AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME,
149
+ fields)
150
+ root_term = TermBuilder.and_terms(root_term, field_term)
151
+
152
+ return CustomReportCriteria(AuditLogUtil.report_columns(), root_term)
153
+
154
+ def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
155
+ fields: list[FieldIdentifier] | None = None) \
156
+ -> dict[RecordIdentifier, list[AuditLogEntry]]:
157
+ """
158
+ This method runs a custom report for changes made to the given data records using the audit log.
159
+ See "create_data_record_audit_log_report" for more details about the data record audit log report.
160
+
161
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
162
+ :param fields: The data field names to include changes for.
163
+ :return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
164
+ AuditLogEntry objects which match the record id value of those records.
165
+ """
166
+ fields: list[str] = AliasUtil.to_data_field_names(fields)
167
+ # First, we must build our report criteria for running the Custom Report.
168
+ criteria = AuditLogUtil.create_data_record_audit_log_report(records, fields)
169
+
170
+ # Then we must run the custom report using that criteria.
171
+ raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
172
+
173
+ # This section will prepare a map matching the original RecordIdentifier by record id.
174
+ # This is because the audit log entries will have record ids, but we want the keys in our result map
175
+ # to match the record identifiers that the user passed in, for convenience.
176
+ record_identifier_mapping: dict[int, RecordIdentifier] = dict()
177
+ for record in records:
178
+ record_id = AliasUtil.to_record_id(record)
179
+ record_identifier_mapping[record_id] = record
180
+
181
+ # Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
182
+ # and the value is a list of applicable audit log entries.
183
+ final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
184
+ for audit_entry_data in raw_report_data:
185
+ audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
186
+ identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
187
+ final_audit_data.setdefault(identifier, []).append(audit_entry)
188
+
189
+ return final_audit_data
@@ -1,19 +1,21 @@
1
1
  from collections.abc import Iterable
2
- from typing import Any
3
2
 
4
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
5
4
  from sapiopylib.rest.User import SapioUser
6
- from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport
7
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
5
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport, CustomReportCriteria, RawReportTerm
6
+
7
+ from sapiopycommons.general.aliases import UserIdentifier, FieldValue, AliasUtil, FieldIdentifierKey
8
8
 
9
9
 
10
10
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
11
11
  class CustomReportUtil:
12
12
  @staticmethod
13
- def run_system_report(context: SapioWebhookContext | SapioUser,
13
+ def run_system_report(context: UserIdentifier,
14
14
  report_name: str,
15
- filters: dict[str, Iterable[Any]] | None = None,
16
- 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]]:
17
19
  """
18
20
  Run a system report and return the results of that report as a list of dictionaries for the values of each
19
21
  column in each row.
@@ -27,29 +29,97 @@ class CustomReportUtil:
27
29
  filter on. Only those headers that both the filters and the custom report share will take effect. That is,
28
30
  any filters that have a header name that isn't in the custom report will be ignored.
29
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
+ :param page_number: The page number to start the search from, If None, starts on the first page.
30
34
  :return: The results of the report listed row by row, mapping each cell to the header it is under. The header
31
35
  values in the dicts are the data field names of the columns.
36
+ If two columns in the search have the same data field name but differing data type names, then the
37
+ dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
38
+ had a Sample column with a data field name of Identifier and a Request column with the same data field name,
39
+ then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
32
40
  """
33
- results = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit)
41
+ results: tuple = CustomReportUtil.__exhaust_system_report(context, report_name, page_limit,
42
+ page_size, page_number)
34
43
  columns: list[ReportColumn] = results[0]
35
- rows: list[list[Any]] = results[1]
44
+ rows: list[list[FieldValue]] = results[1]
45
+ return CustomReportUtil.__process_results(rows, columns, filters)
36
46
 
37
- ret: list[dict[str, Any]] = []
38
- for row in rows:
39
- row_data: dict[str, Any] = {}
40
- filter_row: bool = False
41
- for value, column in zip(row, columns):
42
- header: str = column.data_field_name
43
- if filters is not None and header in filters and value not in filters.get(header):
44
- filter_row = True
45
- break
46
- row_data.update({header: value})
47
- if filter_row is False:
48
- ret.append(row_data)
49
- return ret
47
+ @staticmethod
48
+ def run_custom_report(context: UserIdentifier,
49
+ report_criteria: CustomReportCriteria,
50
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
51
+ page_limit: int | None = None,
52
+ page_size: int | None = None,
53
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
54
+ """
55
+ Run a custom report and return the results of that report as a list of dictionaries for the values of each
56
+ column in each row.
57
+
58
+ Custom reports are constructed by the caller, specifying the report terms and the columns that will be in the
59
+ results. They are like advanced or predefined searches from the system, except they are constructed from
60
+ within the webhook instead of from within the system.
61
+
62
+ :param context: The current webhook context or a user object to send requests from.
63
+ :param report_criteria: The custom report criteria to run.
64
+ :param filters: If provided, filter the results of the report using the given mapping of headers to values to
65
+ filter on. Only those headers that both the filters and the custom report share will take effect. That is,
66
+ any filters that have a header name that isn't in the custom report will be ignored.
67
+ Note that this parameter is only provided for parity with the other run report functions. If you need to
68
+ filter the results of a search, it would likely be more beneficial to have just added a new term to the
69
+ input report criteria that corresponds to the filter.
70
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
71
+ :param page_size: The size of each page of results in the search. If None, uses the value from the given report
72
+ criteria. If not None, overwrites the value from the given report criteria.
73
+ :param page_number: The page number to start the search from, If None, uses the value from the given report
74
+ criteria. If not None, overwrites the value from the given report criteria.
75
+ :return: The results of the report listed row by row, mapping each cell to the header it is under. The header
76
+ values in the dicts are the data field names of the columns.
77
+ If two columns in the search have the same data field name but differing data type names, then the
78
+ dictionary key to the value in the column will be "DataTypeName.DataFieldName". For example, if you
79
+ had a Sample column with a data field name of Identifier and a Request column with the same data field name,
80
+ then the dictionary keys for these columns would be Sample.Identifier and Request.Identifier respectively.
81
+ """
82
+ results: tuple = CustomReportUtil.__exhaust_custom_report(context, report_criteria, page_limit,
83
+ page_size, page_number)
84
+ columns: list[ReportColumn] = results[0]
85
+ rows: list[list[FieldValue]] = results[1]
86
+ return CustomReportUtil.__process_results(rows, columns, filters)
50
87
 
51
88
  @staticmethod
52
- def get_system_report_criteria(context: SapioWebhookContext | SapioUser, report_name: str) -> CustomReport:
89
+ def run_quick_report(context: UserIdentifier,
90
+ report_term: RawReportTerm,
91
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
92
+ page_limit: int | None = None,
93
+ page_size: int | None = None,
94
+ page_number: int | None = None) -> list[dict[str, FieldValue]]:
95
+ """
96
+ Run a quick report and return the results of that report as a list of dictionaries for the values of each
97
+ column in each row.
98
+
99
+ Quick reports are helpful for cases where you need to query record field values in a more complex manner than
100
+ the data record manager allows, but still simpler than a full-blown custom report. The columns that are returned
101
+ in a quick search are every visible field from the data type that corresponds to the given report term. (Fields
102
+ which are not marked as visible in the data designer will be excluded.)
103
+
104
+ :param context: The current webhook context or a user object to send requests from.
105
+ :param report_term: The raw report term to use for the quick report.
106
+ :param filters: If provided, filter the results of the report using the given mapping of headers to values to
107
+ filter on. Only those headers that both the filters and the custom report share will take effect. That is,
108
+ any filters that have a header name that isn't in the custom report will be ignored.
109
+ :param page_limit: The maximum number of pages to query. If None, exhausts all possible pages.
110
+ :param page_size: The size of each page of results in the search. If None, the page size is set by the server.
111
+ :param page_number: The page number to start the search from, If None, starts on the first page.
112
+ :return: The results of the report listed row by row, mapping each cell to the header it is under. The header
113
+ values in the dicts are the data field names of the columns.
114
+ """
115
+ results: tuple = CustomReportUtil.__exhaust_quick_report(context, report_term, page_limit,
116
+ page_size, page_number)
117
+ columns: list[ReportColumn] = results[0]
118
+ rows: list[list[FieldValue]] = results[1]
119
+ return CustomReportUtil.__process_results(rows, columns, filters)
120
+
121
+ @staticmethod
122
+ def get_system_report_criteria(context: UserIdentifier, report_name: str) -> CustomReport:
53
123
  """
54
124
  Retrieve a custom report from the system given the name of the report. This works by querying the system report
55
125
  with a page number and size of 1 to minimize the amount of data transfer needed to retrieve the report's config.
@@ -64,27 +134,132 @@ class CustomReportUtil:
64
134
  :param report_name: The name of the system report to run.
65
135
  :return: The CustomReport object for the given system report name.
66
136
  """
67
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
137
+ user: SapioUser = AliasUtil.to_sapio_user(context)
68
138
  report_man = DataMgmtServer.get_custom_report_manager(user)
69
139
  return report_man.run_system_report_by_name(report_name, 1, 1)
70
140
 
71
141
  @staticmethod
72
- def __exhaust_system_report(context: SapioWebhookContext | SapioUser, report_name: str, page_limit: int | None = None) \
73
- -> tuple[list[ReportColumn], list[list[Any]]]:
74
- user: SapioUser = context if isinstance(context, SapioUser) else context.user
142
+ def __exhaust_system_report(context: UserIdentifier,
143
+ report_name: str,
144
+ page_limit: int | None,
145
+ page_size: int | None,
146
+ page_number: int | None) \
147
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
148
+ """
149
+ Given a system report, iterate over every page of the report and collect the results
150
+ until there are no remaining pages.
151
+ """
152
+ user: SapioUser = AliasUtil.to_sapio_user(context)
153
+ report_man = DataMgmtServer.get_custom_report_manager(user)
154
+
155
+ result = None
156
+ has_next_page: bool = True
157
+ rows: list[list[FieldValue]] = []
158
+ cur_page: int = 1
159
+ while has_next_page and (not page_limit or cur_page <= page_limit):
160
+ result = report_man.run_system_report_by_name(report_name, page_size, page_number)
161
+ page_size = result.page_size
162
+ page_number = result.page_number
163
+ has_next_page = result.has_next_page
164
+ rows.extend(result.result_table)
165
+ cur_page += 1
166
+ return result.column_list, rows
167
+
168
+ @staticmethod
169
+ def __exhaust_custom_report(context: UserIdentifier,
170
+ report: CustomReportCriteria,
171
+ page_limit: int | None,
172
+ page_size: int | None,
173
+ page_number: int | None) \
174
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
175
+ """
176
+ Given a custom report, iterate over every page of the report and collect the results
177
+ until there are no remaining pages.
178
+ """
179
+ user: SapioUser = AliasUtil.to_sapio_user(context)
75
180
  report_man = DataMgmtServer.get_custom_report_manager(user)
76
181
 
77
- report = None
78
- page_size: int | None = None
79
- page_number: int | None = None
182
+ result = None
183
+ if page_size is not None:
184
+ report.page_size = page_size
185
+ if page_number is not None:
186
+ report.page_number = page_number
80
187
  has_next_page: bool = True
81
- rows: list[list[Any]] = []
188
+ rows: list[list[FieldValue]] = []
82
189
  cur_page: int = 1
83
- while has_next_page and (not page_limit or cur_page < page_limit):
84
- report = report_man.run_system_report_by_name(report_name, page_size, page_number)
85
- page_size = report.page_size
86
- page_number = report.page_number
87
- has_next_page = report.has_next_page
88
- rows.extend(report.result_table)
190
+ while has_next_page and (not page_limit or cur_page <= page_limit):
191
+ result = report_man.run_custom_report(report)
192
+ report.page_size = result.page_size
193
+ report.page_number = result.page_number
194
+ has_next_page = result.has_next_page
195
+ rows.extend(result.result_table)
89
196
  cur_page += 1
90
- return report.column_list, rows
197
+ return result.column_list, rows
198
+
199
+ @staticmethod
200
+ def __exhaust_quick_report(context: UserIdentifier,
201
+ report_term: RawReportTerm,
202
+ page_limit: int | None,
203
+ page_size: int | None,
204
+ page_number: int | None) \
205
+ -> tuple[list[ReportColumn], list[list[FieldValue]]]:
206
+ """
207
+ Given a quick report, iterate over every page of the report and collect the results
208
+ until there are no remaining pages.
209
+ """
210
+ user: SapioUser = AliasUtil.to_sapio_user(context)
211
+ report_man = DataMgmtServer.get_custom_report_manager(user)
212
+
213
+ result = None
214
+ has_next_page: bool = True
215
+ rows: list[list[FieldValue]] = []
216
+ cur_page: int = 1
217
+ while has_next_page and (not page_limit or cur_page <= page_limit):
218
+ result = report_man.run_quick_report(report_term, page_size, page_number)
219
+ page_size = result.page_size
220
+ page_number = result.page_number
221
+ has_next_page = result.has_next_page
222
+ rows.extend(result.result_table)
223
+ cur_page += 1
224
+ return result.column_list, rows
225
+
226
+ @staticmethod
227
+ def __process_results(rows: list[list[FieldValue]], columns: list[ReportColumn],
228
+ filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None) -> list[dict[str, FieldValue]]:
229
+ """
230
+ Given the results of a report as a list of row values and the report's columns, combine these lists to
231
+ result in a singular list of dictionaries for each row in the results.
232
+
233
+ If any filter criteria has been provided, also use that to filter the row.
234
+ """
235
+ # It may be the case that two columns have the same data field name but differing data type names.
236
+ # If this occurs, then we need to be able to differentiate these columns in the resulting dictionary.
237
+ prepend_dt: set[str] = set()
238
+ encountered_names: list[str] = []
239
+ for column in columns:
240
+ field_name: str = column.data_field_name
241
+ if field_name in encountered_names:
242
+ prepend_dt.add(field_name)
243
+ else:
244
+ encountered_names.append(field_name)
245
+
246
+ if filters:
247
+ filters: dict[str, Iterable[FieldValue]] = AliasUtil.to_data_field_names_dict(filters)
248
+
249
+ ret: list[dict[str, FieldValue]] = []
250
+ for row in rows:
251
+ row_data: dict[str, FieldValue] = {}
252
+ filter_row: bool = False
253
+ for value, column in zip(row, columns):
254
+ header: str = column.data_field_name
255
+ # If two columns share the same data field name, prepend the data type name of the column to the
256
+ # data field name.
257
+ if header in prepend_dt:
258
+ header = column.data_type_name + "." + header
259
+ if filters is not None and header in filters and value not in filters.get(header):
260
+ filter_row = True
261
+ break
262
+ row_data.update({header: value})
263
+ if filter_row is False:
264
+ ret.append(row_data)
265
+ return ret
@@ -3,34 +3,47 @@ class SapioException(Exception):
3
3
  """
4
4
  A generic exception thrown by sapiopycommons methods. Typically caused by programmer error, but may also be from
5
5
  extremely edge case user errors. For expected user errors, use SapioUserErrorException.
6
+
7
+ CommonsWebhookHandler's default behavior for this and any other exception that doesn't extend SapioException is
8
+ to return a generic toaster message saying that an unexpected error has occurred.
6
9
  """
7
10
  pass
8
11
 
9
12
 
10
- # CommonsWebhookHandler catches this exception and returns "User Cancelled."
11
13
  class SapioUserCancelledException(SapioException):
12
14
  """
13
15
  An exception thrown when the user cancels a client callback.
16
+
17
+ CommonsWebhookHandler's default behavior is to simply end the webhook session with a true result without logging
18
+ the exception.
19
+ """
20
+ pass
21
+
22
+
23
+ class SapioDialogTimeoutException(SapioException):
24
+ """
25
+ An exception thrown when the user leaves a client callback open for too long.
26
+
27
+ CommonsWebhookHandler's default behavior is to display an OK popup notifying the user that the dialog has timed out.
14
28
  """
15
29
  pass
16
30
 
17
31
 
18
- # CommonsWebhookHandler catches this exception and returns the text to the user as display text in a webhook result.
19
32
  class SapioUserErrorException(SapioException):
20
33
  """
21
34
  An exception caused by user error (e.g. user provided a CSV when an XLSX was expected), which promises to return a
22
- user-friendly message explaining the error that should be displayed to the user. It is the responsibility of the
23
- programmer to catch any such exceptions and return the value in e.args[0] as text for the user to see (such as
24
- through the display text of a webhook result).
35
+ user-friendly message explaining the error that should be displayed to the user.
36
+
37
+ CommonsWebhookHandler's default behavior is to return the error message in a toaster popup.
25
38
  """
26
39
  pass
27
40
 
28
41
 
29
- # CommonsWebhookHandler catches this exception and returns the text in a display_error client callback.
30
42
  class SapioCriticalErrorException(SapioException):
31
43
  """
32
44
  A critical exception caused by user error, which promises to return a user-friendly message explaining the error
33
- that should be displayed to the user. It is the responsibility of the programmer to catch any such exceptions and
34
- return the value in e.args[0] as text for the user to see (such as through a dialog form client callback request).
45
+ that should be displayed to the user.
46
+
47
+ CommonsWebhookHandler's default behavior is to return the error message in a display_error callback.
35
48
  """
36
49
  pass
@@ -1,3 +1,5 @@
1
+ import warnings
2
+
1
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
2
4
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
3
5
  from sapiopylib.rest.pojo.datatype.FieldDefinition import VeloxStringFieldDefinition, AbstractVeloxFieldDefinition, \
@@ -51,6 +53,7 @@ class PopupUtil:
51
53
  :param request_context: Context that will be returned to the webhook server in the client callback result.
52
54
  :return: A SapioWebhookResult with the popup as its client callback request.
53
55
  """
56
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
54
57
  if display_name is None:
55
58
  display_name = data_type
56
59
  if plural_display_name is None:
@@ -97,6 +100,7 @@ class PopupUtil:
97
100
  :param request_context: Context that will be returned to the webhook server in the client callback result.
98
101
  :return: A SapioWebhookResult with the popup as its client callback request.
99
102
  """
103
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
100
104
  # Get the field definitions of the data type.
101
105
  data_type: str = record.data_type_name
102
106
  type_man = DataMgmtServer.get_data_type_manager(context.user)
@@ -155,6 +159,7 @@ class PopupUtil:
155
159
  :param request_context: Context that will be returned to the webhook server in the client callback result.
156
160
  :return: A SapioWebhookResult with the popup as its client callback request.
157
161
  """
162
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
158
163
  if max_length is None:
159
164
  max_length = len(default_value) if default_value else 100
160
165
  string_field = VeloxStringFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -191,6 +196,7 @@ class PopupUtil:
191
196
  :param request_context: Context that will be returned to the webhook server in the client callback result.
192
197
  :return: A SapioWebhookResult with the popup as its client callback request.
193
198
  """
199
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
194
200
  if default_value is None:
195
201
  default_value = max(0, min_value)
196
202
  integer_field = VeloxIntegerFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -229,6 +235,7 @@ class PopupUtil:
229
235
  :param request_context: Context that will be returned to the webhook server in the client callback result.
230
236
  :return: A SapioWebhookResult with the popup as its client callback request.
231
237
  """
238
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
232
239
  if default_value is None:
233
240
  default_value = min_value
234
241
  double_field = VeloxDoubleFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -260,6 +267,7 @@ class PopupUtil:
260
267
  :param request_context: Context that will be returned to the webhook server in the client callback result.
261
268
  :return: A SapioWebhookResult with the popup as its client callback request.
262
269
  """
270
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
263
271
  if display_name is None:
264
272
  display_name = data_type
265
273
  if plural_display_name is None:
@@ -295,6 +303,9 @@ class PopupUtil:
295
303
  :param request_context: Context that will be returned to the webhook server in the client callback result.
296
304
  :return: A SapioWebhookResult with the popup as its client callback request.
297
305
  """
306
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
307
+ if not records:
308
+ raise SapioException("No records provided.")
298
309
  data_types: set[str] = {x.data_type_name for x in records}
299
310
  if len(data_types) > 1:
300
311
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -347,6 +358,9 @@ class PopupUtil:
347
358
  :param request_context: Context that will be returned to the webhook server in the client callback result.
348
359
  :return: A SapioWebhookResult with the popup as its client callback request.
349
360
  """
361
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
362
+ if not records:
363
+ raise SapioException("No records provided.")
350
364
  data_types: set[str] = {x.data_type_name for x in records}
351
365
  if len(data_types) > 1:
352
366
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -391,6 +405,7 @@ class PopupUtil:
391
405
  :param request_context: Context that will be returned to the webhook server in the client callback result.
392
406
  :return: A SapioWebhookResult with the popup as its client callback request.
393
407
  """
408
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
394
409
  callback = ListDialogRequest(title, multi_select, options,
395
410
  callback_context_data=request_context)
396
411
  return SapioWebhookResult(True, client_callback_request=callback)
@@ -415,6 +430,7 @@ class PopupUtil:
415
430
  :param request_context: Context that will be returned to the webhook server in the client callback result.
416
431
  :return: A SapioWebhookResult with the popup as its client callback request.
417
432
  """
433
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
418
434
  callback = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
419
435
  callback_context_data=request_context)
420
436
  return SapioWebhookResult(True, client_callback_request=callback)
@@ -437,6 +453,7 @@ class PopupUtil:
437
453
  :param request_context: Context that will be returned to the webhook server in the client callback result.
438
454
  :return: A SapioWebhookResult with the popup as its client callback request.
439
455
  """
456
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
440
457
  return PopupUtil.option_popup(title, msg, ["OK"], 0, user_can_cancel, request_context=request_context)
441
458
 
442
459
  @staticmethod
@@ -458,6 +475,7 @@ class PopupUtil:
458
475
  :param request_context: Context that will be returned to the webhook server in the client callback result.
459
476
  :return: A SapioWebhookResult with the popup as its client callback request.
460
477
  """
478
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
461
479
  return PopupUtil.option_popup(title, msg, ["Yes", "No"], 0 if default_yes else 1, user_can_cancel,
462
480
  request_context=request_context)
463
481
 
@@ -470,6 +488,7 @@ class PopupUtil:
470
488
 
471
489
  Deprecated for PopupUtil.text_field_popup.
472
490
  """
491
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
473
492
  return PopupUtil.string_field_popup(title, "", field_name, msg, len(msg), False, data_type,
474
493
  request_context=request_context, auto_size=True)
475
494
 
@@ -481,6 +500,7 @@ class PopupUtil:
481
500
 
482
501
  Deprecated for PopupUtil.option_popup.
483
502
  """
503
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
484
504
  return PopupUtil.option_popup(title, msg, options, 0, user_can_cancel, request_context=request_context)
485
505
 
486
506
  @staticmethod
@@ -490,4 +510,5 @@ class PopupUtil:
490
510
 
491
511
  Deprecated for PopupUtil.ok_popup.
492
512
  """
513
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
493
514
  return PopupUtil.ok_popup(title, msg, False, request_context=request_context)