sapiopycommons 2024.8.28a313__py3-none-any.whl → 2024.8.28a314__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.

@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+ from weakref import WeakValueDictionary
6
+
7
+ from sapiopylib.rest.User import SapioUser
8
+
9
+ _STR_JAVA_TYPE = "java.lang.String"
10
+ _INT_JAVA_TYPE = "java.lang.Integer"
11
+ _BOOL_JAVA_TYPE = "java.lang.Boolean"
12
+
13
+
14
+ class AbstractAccessionServiceOperator(ABC):
15
+ """
16
+ Abstract class to define an accession service operator.
17
+ The default one in sapiopycommon only includes the out of box operators.
18
+ More can be added via java plugins for global operators.
19
+ """
20
+
21
+ @property
22
+ @abstractmethod
23
+ def op_class_name(self) -> str:
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def op_param_value_list(self) -> list[Any] | None:
29
+ pass
30
+
31
+ @property
32
+ @abstractmethod
33
+ def op_param_class_name_list(self) -> list[str] | None:
34
+ pass
35
+
36
+ @property
37
+ @abstractmethod
38
+ def default_accessor_name(self) -> str:
39
+ pass
40
+
41
+
42
+ class AccessionWithPrefixSuffix(AbstractAccessionServiceOperator):
43
+ """
44
+ Local operator for accessioning prefix and suffix format.
45
+ """
46
+ _prefix: str | None
47
+ _suffix: str | None
48
+ _num_of_digits: int | None
49
+ _start_num: int
50
+ _strict_mode: bool
51
+
52
+ @property
53
+ def prefix(self):
54
+ return self._prefix
55
+
56
+ @property
57
+ def suffix(self):
58
+ return self._suffix
59
+
60
+ @property
61
+ def num_of_digits(self):
62
+ return self._num_of_digits
63
+
64
+ @property
65
+ def start_num(self):
66
+ return self._start_num
67
+
68
+ @property
69
+ def strict_mode(self):
70
+ return self._strict_mode
71
+
72
+ def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
73
+ start_num: int = 1, strict_mode: bool = False):
74
+ if prefix is None:
75
+ prefix = ""
76
+ if suffix is None:
77
+ suffix = ""
78
+ self._prefix = prefix
79
+ self._suffix = suffix
80
+ self._num_of_digits = num_of_digits
81
+ self._start_num = start_num
82
+ self._strict_mode = strict_mode
83
+
84
+ @property
85
+ def op_param_value_list(self):
86
+ return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
87
+
88
+ @property
89
+ def op_param_class_name_list(self):
90
+ return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
91
+
92
+ @property
93
+ def op_class_name(self):
94
+ return "com.velox.accessionservice.operators.AccessionWithPrefixSuffix"
95
+
96
+ @property
97
+ def default_accessor_name(self):
98
+ return "PREFIX_AND_SUFFIX" + "(" + self.prefix + "," + self.suffix + ")";
99
+
100
+
101
+ class AccessionGlobalPrefixSuffix(AbstractAccessionServiceOperator):
102
+ """
103
+ Global operator for accessioning prefix and suffix format.
104
+ """
105
+ _prefix: str | None
106
+ _suffix: str | None
107
+ _num_of_digits: int | None
108
+ _start_num: int
109
+ _strict_mode: bool
110
+
111
+ @property
112
+ def prefix(self):
113
+ return self._prefix
114
+
115
+ @property
116
+ def suffix(self):
117
+ return self._suffix
118
+
119
+ @property
120
+ def num_of_digits(self):
121
+ return self._num_of_digits
122
+
123
+ @property
124
+ def start_num(self):
125
+ return self._start_num
126
+
127
+ @property
128
+ def strict_mode(self):
129
+ return self._strict_mode
130
+
131
+ def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
132
+ start_num: int = 1, strict_mode: bool = False):
133
+ if prefix is None:
134
+ prefix = ""
135
+ if suffix is None:
136
+ suffix = ""
137
+ self._prefix = prefix
138
+ self._suffix = suffix
139
+ self._num_of_digits = num_of_digits
140
+ self._start_num = start_num
141
+ self._strict_mode = strict_mode
142
+
143
+ @property
144
+ def op_param_value_list(self):
145
+ return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
146
+
147
+ @property
148
+ def op_param_class_name_list(self):
149
+ return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
150
+
151
+ @property
152
+ def op_class_name(self):
153
+ return "com.velox.accessionservice.operators.sapio.AccessionGlobalPrefixSuffix"
154
+
155
+ @property
156
+ def default_accessor_name(self):
157
+ return "PREFIX_AND_SUFFIX" + "(" + self._prefix + "," + self._suffix + ")"
158
+
159
+
160
+ class AccessionNextBarcode(AbstractAccessionServiceOperator):
161
+ """
162
+ From Java description:
163
+ This will start accessioning at the getNextBarcode() when there's no system preference to be backward compatible.
164
+ However, once it completes setting the first ID, it will start increment by its own preference and disregards getNextBarcode().
165
+
166
+ Recommend using AccessionServiceBasicManager to accession next barcode.
167
+ To avoid ambiguity in preference cache.
168
+
169
+ This should not be used unless we are using something legacy such as plate mapping template record creation
170
+ (Note: not 3D plating, I'm talking about the older aliquoter).
171
+ """
172
+
173
+ @property
174
+ def op_param_value_list(self):
175
+ return []
176
+
177
+ @property
178
+ def op_param_class_name_list(self):
179
+ return []
180
+
181
+ @property
182
+ def op_class_name(self):
183
+ return "com.velox.accessionservice.operators.sapio.AccessionNextBarcode"
184
+
185
+ @property
186
+ def default_accessor_name(self):
187
+ return "Barcode"
188
+
189
+
190
+ class AccessionRequestId(AbstractAccessionServiceOperator):
191
+ """
192
+ This class implements the accessioning operator for com.velox.sapioutils.shared.managers.DataRecordUtilManager.getNextRequestId()
193
+ and getNextRequestId(int numberOfCharacters).
194
+
195
+ Operation: For 4 characters start with A001, increment by 1 until A999. Then We use B001.
196
+ After Z999 we start with AA01 until we get to AA99, etc.
197
+
198
+ Exception: Skips I and O to prevent confusions with 1 and 0 when incrementing letters.
199
+
200
+ Properties:
201
+ numberOfCharacters: Number of characters maximum in the request ID.
202
+ accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compability patch for converting these to the new preference format.
203
+ """
204
+ _num_of_characters: int
205
+ _accessor_name: str
206
+
207
+ @property
208
+ def num_of_characters(self):
209
+ return self._num_of_characters
210
+
211
+ @property
212
+ def accessor_name(self):
213
+ return self._accessor_name
214
+
215
+ def __init__(self, num_of_characters: int = 4, accessor_name: str = None):
216
+ self._num_of_characters = num_of_characters
217
+ if not accessor_name:
218
+ accessor_name = self.default_accessor_name
219
+ self._accessor_name = accessor_name
220
+
221
+ @property
222
+ def op_class_name(self):
223
+ return "com.velox.accessionservice.operators.sapio.AccessionRequestId"
224
+
225
+ @property
226
+ def op_param_value_list(self):
227
+ return [self._num_of_characters, self._accessor_name]
228
+
229
+ @property
230
+ def op_param_class_name_list(self):
231
+ return [_INT_JAVA_TYPE, _STR_JAVA_TYPE]
232
+
233
+ @property
234
+ def default_accessor_name(self):
235
+ return "SapioNextRequestIdMap"
236
+
237
+
238
+ class AccessionServiceDescriptor:
239
+ """
240
+ Describes a single accession service's accessioning request
241
+
242
+ Attributes:
243
+ opClassName: The accession service operator class name as in Java
244
+ opParamValueList: Ordered list of parameter values to construct the accession service operator.
245
+ opParamClassNameList: Ordered list of FQCN of java classes in order of parameter value list.
246
+ dataTypeName: The data type to accession. Should be blank if opClassName resolves to a global operator.
247
+ dataFieldName: The data field to accession. Should be blank if opClassName resolves to a global operator.
248
+ accessorName: The accessor cache name to be used for accessioning.
249
+ numIds: The number of IDs to accession.
250
+ """
251
+ op: AbstractAccessionServiceOperator
252
+ dataTypeName: str | None
253
+ dataFieldName: str | None
254
+ accessorName: str
255
+ numIds: int
256
+
257
+ def __init__(self, accessor_name: str, op: AbstractAccessionServiceOperator, num_ids: int,
258
+ data_type_name: str | None, data_field_name: str | None):
259
+ self.accessorName = accessor_name
260
+ self.op = op
261
+ self.dataTypeName = data_type_name
262
+ self.dataFieldName = data_field_name
263
+ self.numIds = num_ids
264
+
265
+ def to_json(self):
266
+ return {
267
+ "opClassName": self.op.op_class_name,
268
+ "opParamValueList": self.op.op_param_value_list,
269
+ "opParamClassNameList": self.op.op_param_class_name_list,
270
+ "accessorName": self.accessorName,
271
+ "numIds": self.numIds,
272
+ "dataTypeName": self.dataTypeName,
273
+ "dataFieldName": self.dataFieldName
274
+ }
275
+
276
+
277
+ class AccessionService:
278
+ """
279
+ Provides Sapio Foundations Accession Service functionalities.
280
+ """
281
+ _user: SapioUser
282
+
283
+ __instances: WeakValueDictionary[SapioUser, AccessionService] = WeakValueDictionary()
284
+ __initialized: bool
285
+
286
+ @property
287
+ def user(self) -> SapioUser:
288
+ return self._user
289
+
290
+ def __new__(cls, user: SapioUser):
291
+ """
292
+ Observes singleton pattern per record model manager object.
293
+
294
+ :param user: The user that will make the webservice request to the application.
295
+ """
296
+ obj = cls.__instances.get(user)
297
+ if not obj:
298
+ obj = object.__new__(cls)
299
+ obj.__initialized = False
300
+ cls.__instances[user] = obj
301
+ return obj
302
+
303
+ def __init__(self, user: SapioUser):
304
+ if self.__initialized:
305
+ return
306
+ self._user = user
307
+ self.__initialized = True
308
+
309
+ def accession_with_config(self, data_type_name: str, data_field_name: str, num_ids: int) -> list[str]:
310
+ """
311
+ Accession with Configuration Manager => Accession Service configuration (This is not visible to regular users in SaaS)
312
+ """
313
+ payload = {
314
+ "dataTypeName": data_type_name,
315
+ "dataFieldName": data_field_name,
316
+ "numIds": num_ids
317
+ }
318
+ response = self.user.plugin_post("accessionservice/accession_with_config", payload=payload)
319
+ self.user.raise_for_status(response)
320
+ return list(response.json())
321
+
322
+ def accession_in_batch(self, descriptor: AccessionServiceDescriptor) -> list[str]:
323
+ """
324
+ This is the most flexible way to make use of accession service: directly via a descriptor object.
325
+ """
326
+ payload = descriptor.to_json()
327
+ response = self.user.plugin_post("accessionservice/accession", payload=payload)
328
+ self.user.raise_for_status(response)
329
+ return list(response.json())
330
+
331
+ def accession_next_request_id_list(self, num_of_characters: int, num_ids: int) -> list[str]:
332
+ """
333
+ Accession Request ID by old LIMS format. This is usually deprecated today.
334
+ :param num_of_characters: Number of characters minimum in request ID.
335
+ :param num_ids: Number of request IDs to accession.
336
+ """
337
+ op = AccessionRequestId(num_of_characters)
338
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
339
+ return self.accession_in_batch(descriptor)
340
+
341
+ def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
342
+ suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
343
+ """
344
+ Get the batch affixed IDs that are maximal in cache and contiguious for a particular datatype.datafield under a given format.
345
+ :param data_type_name: The datatype name to look for max ID
346
+ :param data_field_name: The datafield name to look for max ID
347
+ :param num_ids: The number of IDs to accession.
348
+ :param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
349
+ :param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
350
+ :param num_digits: None if unlimited with no leading zeros.
351
+ :param start_num The number to begin accessioning if this is the first time.
352
+ :return:
353
+ """
354
+ op = AccessionWithPrefixSuffix(prefix, suffix, num_digits, start_num)
355
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, data_type_name, data_field_name)
356
+ return self.accession_in_batch(descriptor)
357
+
358
+ def get_global_affixed_id_in_batch(
359
+ self, num_ids: int, prefix: str | None, suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
360
+ """
361
+ Get the next numOfIds affixed IDs using system preference cache that's maximum across all datatype and datafields and maximal for the format.
362
+ This method allows users to customize a start number instead of always starting at 1.
363
+ :param num_ids: The number of IDs to accession.
364
+ :param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
365
+ :param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
366
+ :param num_digits: None if unlimited with no leading zeros.
367
+ :param start_num The number to begin accessioning if this is the first time.
368
+ """
369
+ op: AbstractAccessionServiceOperator
370
+ if not prefix and not suffix:
371
+ op = AccessionNextBarcode()
372
+ else:
373
+ op = AccessionGlobalPrefixSuffix(prefix, suffix, num_digits, start_num)
374
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
375
+ return self.accession_in_batch(descriptor)
@@ -3,7 +3,7 @@ from typing import Any
3
3
 
4
4
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
5
5
  from sapiopylib.rest.User import SapioUser
6
- from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport
6
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReport, CustomReportCriteria, RawReportTerm
7
7
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
8
8
 
9
9
 
@@ -13,7 +13,9 @@ class CustomReportUtil:
13
13
  def run_system_report(context: SapioWebhookContext | SapioUser,
14
14
  report_name: str,
15
15
  filters: dict[str, Iterable[Any]] | None = None,
16
- page_limit: int | None = None) -> list[dict[str, Any]]:
16
+ page_limit: int | None = None,
17
+ page_size: int | None = None,
18
+ page_number: int | None = None) -> list[dict[str, Any]]:
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,26 +29,94 @@ 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
44
  rows: list[list[Any]] = 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: SapioWebhookContext | SapioUser,
49
+ report_criteria: CustomReportCriteria,
50
+ filters: dict[str, Iterable[Any]] | None = None,
51
+ page_limit: int | None = None,
52
+ page_size: int | None = None,
53
+ page_number: int | None = None) -> list[dict[str, Any]]:
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[Any]] = results[1]
86
+ return CustomReportUtil.__process_results(rows, columns, filters)
87
+
88
+ @staticmethod
89
+ def run_quick_report(context: SapioWebhookContext | SapioUser,
90
+ report_term: RawReportTerm,
91
+ filters: dict[str, Iterable[Any]] | None = None,
92
+ page_limit: int | None = None,
93
+ page_size: int | None = None,
94
+ page_number: int | None = None) -> list[dict[str, Any]]:
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[Any]] = results[1]
119
+ return CustomReportUtil.__process_results(rows, columns, filters)
50
120
 
51
121
  @staticmethod
52
122
  def get_system_report_criteria(context: SapioWebhookContext | SapioUser, report_name: str) -> CustomReport:
@@ -69,22 +139,124 @@ class CustomReportUtil:
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) \
142
+ def __exhaust_system_report(context: SapioWebhookContext | SapioUser,
143
+ report_name: str,
144
+ page_limit: int | None,
145
+ page_size: int | None,
146
+ page_number: int | None) \
73
147
  -> tuple[list[ReportColumn], list[list[Any]]]:
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
+ """
74
152
  user: SapioUser = context if isinstance(context, SapioUser) else context.user
75
153
  report_man = DataMgmtServer.get_custom_report_manager(user)
76
154
 
77
- report = None
78
- page_size: int | None = None
79
- page_number: int | None = None
155
+ result = None
80
156
  has_next_page: bool = True
81
157
  rows: list[list[Any]] = []
82
158
  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)
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)
89
165
  cur_page += 1
90
- return report.column_list, rows
166
+ return result.column_list, rows
167
+
168
+ @staticmethod
169
+ def __exhaust_custom_report(context: SapioWebhookContext | SapioUser,
170
+ report: CustomReportCriteria,
171
+ page_limit: int | None,
172
+ page_size: int | None,
173
+ page_number: int | None) \
174
+ -> tuple[list[ReportColumn], list[list[Any]]]:
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 = context if isinstance(context, SapioUser) else context.user
180
+ report_man = DataMgmtServer.get_custom_report_manager(user)
181
+
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
187
+ has_next_page: bool = True
188
+ rows: list[list[Any]] = []
189
+ cur_page: int = 1
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)
196
+ cur_page += 1
197
+ return result.column_list, rows
198
+
199
+ @staticmethod
200
+ def __exhaust_quick_report(context: SapioWebhookContext | SapioUser,
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[Any]]]:
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 = context if isinstance(context, SapioUser) else context.user
211
+ report_man = DataMgmtServer.get_custom_report_manager(user)
212
+
213
+ result = None
214
+ has_next_page: bool = True
215
+ rows: list[list[Any]] = []
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[Any]], columns: list[ReportColumn],
228
+ filters: dict[str, Iterable[Any]] | None) -> list[dict[str, Any]]:
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
+ ret: list[dict[str, Any]] = []
247
+ for row in rows:
248
+ row_data: dict[str, Any] = {}
249
+ filter_row: bool = False
250
+ for value, column in zip(row, columns):
251
+ header: str = column.data_field_name
252
+ # If two columns share the same data field name, prepend the data type name of the column to the
253
+ # data field name.
254
+ if header in prepend_dt:
255
+ header = column.data_type_name + "." + header
256
+ if filters is not None and header in filters and value not in filters.get(header):
257
+ filter_row = True
258
+ break
259
+ row_data.update({header: value})
260
+ if filter_row is False:
261
+ ret.append(row_data)
262
+ return ret