sapiopycommons 2025.2.25a450__py3-none-any.whl → 2025.3.6a453__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.
- sapiopycommons/ai/tool_of_tools.py +61 -91
- sapiopycommons/callbacks/callback_util.py +3 -3
- sapiopycommons/customreport/auto_pagers.py +25 -17
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/general/aliases.py +12 -5
- sapiopycommons/general/html_formatter.py +456 -0
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/general/sapio_links.py +12 -4
- sapiopycommons/recordmodel/record_handler.py +151 -105
- {sapiopycommons-2025.2.25a450.dist-info → sapiopycommons-2025.3.6a453.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.2.25a450.dist-info → sapiopycommons-2025.3.6a453.dist-info}/RECORD +13 -12
- {sapiopycommons-2025.2.25a450.dist-info → sapiopycommons-2025.3.6a453.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.2.25a450.dist-info → sapiopycommons-2025.3.6a453.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import io
|
|
3
3
|
import math
|
|
4
|
-
import re
|
|
5
4
|
from typing import Final, Mapping, Any
|
|
6
5
|
|
|
7
6
|
import requests
|
|
@@ -26,7 +25,7 @@ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
|
|
|
26
25
|
from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
|
|
27
26
|
from sapiopylib.rest.pojo.eln.ExperimentEntryCriteria import ElnEntryCriteria, ElnFormEntryUpdateCriteria, \
|
|
28
27
|
ElnDashboardEntryUpdateCriteria
|
|
29
|
-
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnEntryType, ElnBaseDataType
|
|
28
|
+
from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnEntryType, ElnBaseDataType, ExperimentEntryStatus
|
|
30
29
|
from sapiopylib.rest.pojo.eln.eln_headings import ElnExperimentTabAddCriteria, ElnExperimentTab
|
|
31
30
|
from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
|
|
32
31
|
from sapiopylib.rest.utils.ProtocolUtils import ELNStepFactory
|
|
@@ -35,6 +34,7 @@ from sapiopylib.rest.utils.Protocols import ElnEntryStep, ElnExperimentProtocol
|
|
|
35
34
|
from sapiopycommons.callbacks.field_builder import FieldBuilder
|
|
36
35
|
from sapiopycommons.general.aliases import AliasUtil, SapioRecord
|
|
37
36
|
from sapiopycommons.general.exceptions import SapioException
|
|
37
|
+
from sapiopycommons.general.html_formatter import HtmlFormatter
|
|
38
38
|
from sapiopycommons.general.time_util import TimeUtil
|
|
39
39
|
from sapiopycommons.multimodal.multimodal import MultiModalManager
|
|
40
40
|
from sapiopycommons.multimodal.multimodal_data import ImageDataRequestPojo
|
|
@@ -102,88 +102,6 @@ def format_tot_headers(headers: Mapping[str, str]) -> dict[str, str]:
|
|
|
102
102
|
return {k.lower(): v for k, v in headers.items()}
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
class HtmlFormatter:
|
|
106
|
-
"""
|
|
107
|
-
A class for formatting text in HTML with tag classes supported by the client.
|
|
108
|
-
"""
|
|
109
|
-
TIMESTAMP_TEXT__CSS_CLASS_NAME: Final[str] = "timestamp-text"
|
|
110
|
-
HEADER_1_TEXT__CSS_CLASS_NAME: Final[str] = "header1-text"
|
|
111
|
-
HEADER_2_TEXT__CSS_CLASS_NAME: Final[str] = "header2-text"
|
|
112
|
-
HEADER_3_TEXT__CSS_CLASS_NAME: Final[str] = "header3-text"
|
|
113
|
-
BODY_TEXT__CSS_CLASS_NAME: Final[str] = "body-text"
|
|
114
|
-
CAPTION_TEXT__CSS_CLASS_NAME: Final[str] = "caption-text"
|
|
115
|
-
|
|
116
|
-
@staticmethod
|
|
117
|
-
def timestamp(text: str) -> str:
|
|
118
|
-
"""
|
|
119
|
-
Given a text string, return that same text string HTML formatted using the timestamp CSS class.
|
|
120
|
-
|
|
121
|
-
:param text: The text to format.
|
|
122
|
-
:return: The HTML formatted text.
|
|
123
|
-
"""
|
|
124
|
-
return f"<span class=\"{HtmlFormatter.TIMESTAMP_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
125
|
-
|
|
126
|
-
@staticmethod
|
|
127
|
-
def header_1(text: str) -> str:
|
|
128
|
-
"""
|
|
129
|
-
Given a text string, return that same text string HTML formatted using the header 1 CSS class.
|
|
130
|
-
|
|
131
|
-
:param text: The text to format.
|
|
132
|
-
:return: The HTML formatted text.
|
|
133
|
-
"""
|
|
134
|
-
return f"<span class=\"{HtmlFormatter.HEADER_1_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
135
|
-
|
|
136
|
-
@staticmethod
|
|
137
|
-
def header_2(text: str) -> str:
|
|
138
|
-
"""
|
|
139
|
-
Given a text string, return that same text string HTML formatted using the header 2 CSS class.
|
|
140
|
-
|
|
141
|
-
:param text: The text to format.
|
|
142
|
-
:return: The HTML formatted text.
|
|
143
|
-
"""
|
|
144
|
-
return f"<span class=\"{HtmlFormatter.HEADER_2_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
145
|
-
|
|
146
|
-
@staticmethod
|
|
147
|
-
def header_3(text: str) -> str:
|
|
148
|
-
"""
|
|
149
|
-
Given a text string, return that same text string HTML formatted using the header 3 CSS class.
|
|
150
|
-
|
|
151
|
-
:param text: The text to format.
|
|
152
|
-
:return: The HTML formatted text.
|
|
153
|
-
"""
|
|
154
|
-
return f"<span class=\"{HtmlFormatter.HEADER_3_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
155
|
-
|
|
156
|
-
@staticmethod
|
|
157
|
-
def body(text: str) -> str:
|
|
158
|
-
"""
|
|
159
|
-
Given a text string, return that same text string HTML formatted using the body CSS class.
|
|
160
|
-
|
|
161
|
-
:param text: The text to format.
|
|
162
|
-
:return: The HTML formatted text.
|
|
163
|
-
"""
|
|
164
|
-
return f"<span class=\"{HtmlFormatter.BODY_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
165
|
-
|
|
166
|
-
@staticmethod
|
|
167
|
-
def caption(text: str) -> str:
|
|
168
|
-
"""
|
|
169
|
-
Given a text string, return that same text string HTML formatted using the caption CSS class.
|
|
170
|
-
|
|
171
|
-
:param text: The text to format.
|
|
172
|
-
:return: The HTML formatted text.
|
|
173
|
-
"""
|
|
174
|
-
return f"<span class=\"{HtmlFormatter.CAPTION_TEXT__CSS_CLASS_NAME}\">{text}</span>"
|
|
175
|
-
|
|
176
|
-
@staticmethod
|
|
177
|
-
def replace_newlines(text: str) -> str:
|
|
178
|
-
"""
|
|
179
|
-
Given a text string, return that same text string HTML formatted with newlines replaced by HTML line breaks.
|
|
180
|
-
|
|
181
|
-
:param text: The text to format.
|
|
182
|
-
:return: The HTML formatted text.
|
|
183
|
-
"""
|
|
184
|
-
return re.sub("\r?\n", "<br>", text)
|
|
185
|
-
|
|
186
|
-
|
|
187
105
|
class AiHelper:
|
|
188
106
|
"""
|
|
189
107
|
A class with helper methods for the AI to make use of when creating/updating experiment tabs and entries.
|
|
@@ -611,6 +529,8 @@ class ToolOfToolsHelper:
|
|
|
611
529
|
# Stuff created by this helper.
|
|
612
530
|
_initialized: bool
|
|
613
531
|
"""Whether a tab for this tool has been initialized."""
|
|
532
|
+
_new_tab: bool
|
|
533
|
+
"""Whether a new tab was created for this tool."""
|
|
614
534
|
tab: ElnExperimentTab
|
|
615
535
|
"""The tab that contains the tool's entries."""
|
|
616
536
|
description_entry: ElnEntryStep | None
|
|
@@ -649,6 +569,15 @@ class ToolOfToolsHelper:
|
|
|
649
569
|
self.eln_man = ElnManager(self.user)
|
|
650
570
|
|
|
651
571
|
self._initialized = False
|
|
572
|
+
self._new_tab = False
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def is_initialized(self) -> bool:
|
|
576
|
+
return self._initialized
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def is_new_tab(self) -> bool:
|
|
580
|
+
return self._new_tab
|
|
652
581
|
|
|
653
582
|
def initialize_tab(self) -> ElnExperimentTab:
|
|
654
583
|
if self._initialized:
|
|
@@ -701,6 +630,7 @@ class ToolOfToolsHelper:
|
|
|
701
630
|
|
|
702
631
|
# Otherwise, create the tab for the tool progress and results.
|
|
703
632
|
self.tab = self.helper.create_tab(tab_name)
|
|
633
|
+
self._new_tab = True
|
|
704
634
|
|
|
705
635
|
# Create a hidden entry for tracking the progress of the tool.
|
|
706
636
|
field_sets: list[ElnFieldSetInfo] = self.eln_man.get_field_set_info_list()
|
|
@@ -708,16 +638,18 @@ class ToolOfToolsHelper:
|
|
|
708
638
|
x.field_set_name == "Tool of Tools Progress"]
|
|
709
639
|
if not progress_field_set:
|
|
710
640
|
raise SapioException("Unable to locate the field set for the Tool of Tools progress.")
|
|
711
|
-
progress_entry_crit =
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
641
|
+
progress_entry_crit = _ElnEntryCriteria(ElnEntryType.Form, f"{tab_name} Progress",
|
|
642
|
+
ElnBaseDataType.EXPERIMENT_DETAIL.data_type_name, 1,
|
|
643
|
+
notebook_experiment_tab_id=self.tab.tab_id,
|
|
644
|
+
enb_field_set_id=progress_field_set[0].field_set_id,
|
|
645
|
+
is_hidden=True)
|
|
715
646
|
progress_entry = ElnEntryStep(self.helper.protocol,
|
|
716
647
|
self.eln_man.add_experiment_entry(self.exp_id, progress_entry_crit))
|
|
717
648
|
self.progress_entry = progress_entry
|
|
718
649
|
self.progress_record = progress_entry.get_records()[0]
|
|
719
650
|
|
|
720
651
|
# Hide the progress entry.
|
|
652
|
+
# TODO: Remove once we get this working on entry creation.
|
|
721
653
|
form_update_crit = ElnFormEntryUpdateCriteria()
|
|
722
654
|
form_update_crit.is_hidden = True
|
|
723
655
|
self.eln_man.update_experiment_entry(self.exp_id, self.progress_entry.get_id(), form_update_crit)
|
|
@@ -735,10 +667,11 @@ class ToolOfToolsHelper:
|
|
|
735
667
|
# Create a gauge entry to display the progress.
|
|
736
668
|
gauge_entry: ElnEntryStep = _ELNStepFactory._create_gauge_chart(self.helper.protocol, progress_entry,
|
|
737
669
|
f"{self.name} Progress", "Progress", "StatusMsg",
|
|
738
|
-
column_order=2, column_span=2)
|
|
670
|
+
column_order=2, column_span=2, entry_height=250)
|
|
739
671
|
self.progress_gauge_entry = gauge_entry
|
|
740
672
|
|
|
741
673
|
# Make sure the gauge entry isn't too big and stick it to the right of the text entry.
|
|
674
|
+
# TODO: Remove once we get this working on entry creation.
|
|
742
675
|
dash_update_crit = ElnDashboardEntryUpdateCriteria()
|
|
743
676
|
dash_update_crit.entry_height = 250
|
|
744
677
|
self.eln_man.update_experiment_entry(self.exp_id, self.progress_gauge_entry.get_id(), dash_update_crit)
|
|
@@ -927,8 +860,8 @@ class _ELNStepFactory:
|
|
|
927
860
|
if order is None:
|
|
928
861
|
order = last_step.eln_entry.order + 1
|
|
929
862
|
eln_manager = DataMgmtServer.get_eln_manager(protocol.user)
|
|
930
|
-
entry_criteria =
|
|
931
|
-
|
|
863
|
+
entry_criteria = _ElnEntryCriteria(entry_type, step_name, data_type_name, order,
|
|
864
|
+
notebook_experiment_tab_id=tab_id, **kwargs)
|
|
932
865
|
new_entry: ExperimentEntry = eln_manager.add_experiment_entry(protocol.eln_experiment.notebook_experiment_id,
|
|
933
866
|
entry_criteria)
|
|
934
867
|
return eln_manager, new_entry
|
|
@@ -945,3 +878,40 @@ class _GaugeChartDefinition(GaugeChartDefinition):
|
|
|
945
878
|
"dataFieldName": self.status_field
|
|
946
879
|
}
|
|
947
880
|
return result
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class _ElnEntryCriteria(ElnEntryCriteria):
|
|
884
|
+
is_hidden: bool | None
|
|
885
|
+
entry_height: int | None
|
|
886
|
+
description: str | None
|
|
887
|
+
is_initialization_required: bool | None
|
|
888
|
+
collapse_entry: bool | None
|
|
889
|
+
entry_status: ExperimentEntryStatus | None
|
|
890
|
+
template_item_fulfilled_timestamp: int | None
|
|
891
|
+
|
|
892
|
+
def __init__(self, entry_type: ElnEntryType, entry_name: str | None, data_type_name: str | None, order: int,
|
|
893
|
+
is_hidden: bool | None = None, entry_height: int | None = None, description: str | None = None,
|
|
894
|
+
is_initialization_required: bool | None = None, collapse_entry: bool | None = None,
|
|
895
|
+
entry_status: ExperimentEntryStatus | None = None, template_item_fulfilled_timestamp: int | None = None,
|
|
896
|
+
**kwargs):
|
|
897
|
+
super().__init__(entry_type, entry_name, data_type_name, order, **kwargs)
|
|
898
|
+
self.is_hidden = is_hidden
|
|
899
|
+
self.entry_height = entry_height
|
|
900
|
+
self.description = description
|
|
901
|
+
self.is_initialization_required = is_initialization_required
|
|
902
|
+
self.collapse_entry = collapse_entry
|
|
903
|
+
self.entry_status = entry_status
|
|
904
|
+
self.template_item_fulfilled_timestamp = template_item_fulfilled_timestamp
|
|
905
|
+
|
|
906
|
+
def to_json(self) -> dict[str, Any]:
|
|
907
|
+
ret: dict[str, Any] = super().to_json()
|
|
908
|
+
ret.update({
|
|
909
|
+
"hidden": self.is_hidden,
|
|
910
|
+
"entryHeight": self.entry_height,
|
|
911
|
+
"description": self.description,
|
|
912
|
+
"initializationRequired": self.is_initialization_required,
|
|
913
|
+
"collapsed": self.collapse_entry,
|
|
914
|
+
"entryStatus": self.entry_status,
|
|
915
|
+
"templateItemFulfilledTimestamp": self.template_item_fulfilled_timestamp
|
|
916
|
+
})
|
|
917
|
+
return ret
|
|
@@ -712,7 +712,7 @@ class CallbackUtil:
|
|
|
712
712
|
if not records:
|
|
713
713
|
raise SapioException("No records provided.")
|
|
714
714
|
data_type: str = AliasUtil.to_singular_data_type_name(records)
|
|
715
|
-
field_map_list: list[FieldMap] = AliasUtil.
|
|
715
|
+
field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records)
|
|
716
716
|
|
|
717
717
|
# Convert the group_by parameter to a field name.
|
|
718
718
|
if group_by is not None:
|
|
@@ -1192,7 +1192,7 @@ class CallbackUtil:
|
|
|
1192
1192
|
record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
|
|
1193
1193
|
# This could be either a record, a field map, or null. Convert any records to field maps.
|
|
1194
1194
|
if not isinstance(record, dict) and record is not None:
|
|
1195
|
-
record: FieldMap | None = AliasUtil.
|
|
1195
|
+
record: FieldMap | None = AliasUtil.to_field_map(record)
|
|
1196
1196
|
|
|
1197
1197
|
# Find out if this field had its data type prepended to it. If this is the case, then we need to find
|
|
1198
1198
|
# the true data field name before retrieving the value from the field map.
|
|
@@ -1383,7 +1383,7 @@ class CallbackUtil:
|
|
|
1383
1383
|
if not records:
|
|
1384
1384
|
raise SapioException("No records provided.")
|
|
1385
1385
|
data_type: str = AliasUtil.to_singular_data_type_name(records)
|
|
1386
|
-
field_map_list: list[FieldMap] = AliasUtil.
|
|
1386
|
+
field_map_list: list[FieldMap] = AliasUtil.to_field_map_list(records, include_record_id=True)
|
|
1387
1387
|
|
|
1388
1388
|
# Key fields display their columns in order before all non-key fields.
|
|
1389
1389
|
# Unmark key fields so that the column order is respected exactly as the caller provides it.
|
|
@@ -8,6 +8,7 @@ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, CustomReport
|
|
|
8
8
|
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
9
9
|
from sapiopylib.rest.utils.autopaging import SapioPyAutoPager, PagerResultCriteriaType, _default_report_page_size, \
|
|
10
10
|
_default_record_page_size
|
|
11
|
+
from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
|
|
11
12
|
from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
|
|
12
13
|
|
|
13
14
|
from sapiopycommons.general.aliases import FieldValue, UserIdentifier, AliasUtil, RecordModel
|
|
@@ -110,20 +111,20 @@ class QuickReportDictAutoPager(_DictReportPagerBase):
|
|
|
110
111
|
super().__init__(user, first_page_criteria)
|
|
111
112
|
|
|
112
113
|
|
|
113
|
-
class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria,
|
|
114
|
+
class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria, RecordModel], ABC):
|
|
114
115
|
"""
|
|
115
116
|
A base class for automatically paging through a report and returning the results as a list of records.
|
|
116
117
|
"""
|
|
117
118
|
_columns: list[ReportColumn]
|
|
118
|
-
|
|
119
|
+
_query_type: type[WrappedType] | str
|
|
119
120
|
_data_type: str
|
|
120
121
|
_rec_handler: RecordHandler
|
|
121
122
|
_report_man: CustomReportManager
|
|
122
123
|
|
|
123
|
-
def __init__(self, user: UserIdentifier, first_page_criteria: CustomReportCriteria, wrapper_type: type[WrappedType]):
|
|
124
|
+
def __init__(self, user: UserIdentifier, first_page_criteria: CustomReportCriteria, wrapper_type: type[WrappedType] | str):
|
|
124
125
|
self._columns = first_page_criteria.column_list
|
|
125
|
-
self.
|
|
126
|
-
self._data_type =
|
|
126
|
+
self._query_type = wrapper_type
|
|
127
|
+
self._data_type = AliasUtil.to_data_type_name(wrapper_type)
|
|
127
128
|
self._rec_handler = RecordHandler(user)
|
|
128
129
|
super().__init__(AliasUtil.to_sapio_user(user), first_page_criteria)
|
|
129
130
|
self._report_man = DataMgmtServer.get_custom_report_manager(self.user)
|
|
@@ -139,9 +140,9 @@ class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria, WrappedType]
|
|
|
139
140
|
def default_first_page_criteria(self) -> PagerResultCriteriaType:
|
|
140
141
|
raise ValueError("Cannot generate a default first page criteria for custom reports.")
|
|
141
142
|
|
|
142
|
-
def get_next_page_result(self) -> tuple[CustomReportCriteria | None, Queue[WrappedType]]:
|
|
143
|
+
def get_next_page_result(self) -> tuple[CustomReportCriteria | None, Queue[WrappedType] | Queue[PyRecordModel]]:
|
|
143
144
|
report: CustomReport = self._report_man.run_custom_report(self.next_page_criteria)
|
|
144
|
-
queue
|
|
145
|
+
queue = Queue()
|
|
145
146
|
id_index: int = -1
|
|
146
147
|
for i, column in enumerate(self._columns):
|
|
147
148
|
if column.data_type_name == self._data_type and column.data_field_name == "RecordId":
|
|
@@ -151,7 +152,7 @@ class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria, WrappedType]
|
|
|
151
152
|
raise SapioException(f"This report does not contain a Record ID column for the given record model type "
|
|
152
153
|
f"{self._data_type}.")
|
|
153
154
|
ids: list[int] = [row[id_index] for row in report.result_table]
|
|
154
|
-
for row in self._rec_handler.query_models_by_id(self.
|
|
155
|
+
for row in self._rec_handler.query_models_by_id(self._query_type, ids, page_size=report.page_size):
|
|
155
156
|
queue.put(row)
|
|
156
157
|
if report.has_next_page:
|
|
157
158
|
next_page_criteria = copy(self.next_page_criteria)
|
|
@@ -165,12 +166,15 @@ class CustomReportRecordAutoPager(_RecordReportPagerBase):
|
|
|
165
166
|
"""
|
|
166
167
|
A class that automatically pages through a custom report and returns the results as a list of records.
|
|
167
168
|
"""
|
|
168
|
-
def __init__(self, user: UserIdentifier, report_criteria: CustomReportCriteria,
|
|
169
|
-
|
|
169
|
+
def __init__(self, user: UserIdentifier, report_criteria: CustomReportCriteria,
|
|
170
|
+
wrapper_type: type[WrappedType] | str, page_number: int = 0,
|
|
171
|
+
page_size: int = _default_record_page_size):
|
|
170
172
|
"""
|
|
171
173
|
:param user: The current webhook context or a user object to send requests from.
|
|
172
174
|
:param report_criteria: The custom report criteria to run.
|
|
173
|
-
:param wrapper_type: The record model wrapper type
|
|
175
|
+
:param wrapper_type: The record model wrapper type or data type name of the records being searched for.
|
|
176
|
+
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
177
|
+
instead of WrappedRecordModels.
|
|
174
178
|
:param page_number: The page number to start on. The first page is page 0.
|
|
175
179
|
:param page_size: The number of results to return per page.
|
|
176
180
|
"""
|
|
@@ -188,12 +192,14 @@ class SystemReportRecordAutoPager(_RecordReportPagerBase):
|
|
|
188
192
|
System reports are also known as predefined searches in the system and must be defined in the data designer for
|
|
189
193
|
a specific data type. That is, saved searches created by users cannot be run using this function.
|
|
190
194
|
"""
|
|
191
|
-
def __init__(self, user: UserIdentifier, report_name: str, wrapper_type: type[WrappedType],
|
|
195
|
+
def __init__(self, user: UserIdentifier, report_name: str, wrapper_type: type[WrappedType] | str,
|
|
192
196
|
page_number: int = 0, page_size: int = _default_record_page_size):
|
|
193
197
|
"""
|
|
194
198
|
:param user: The current webhook context or a user object to send requests from.
|
|
195
199
|
:param report_name: The name of the system report to run.
|
|
196
|
-
:param wrapper_type: The record model wrapper type
|
|
200
|
+
:param wrapper_type: The record model wrapper type or data type name of the records being searched for.
|
|
201
|
+
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
202
|
+
instead of WrappedRecordModels.
|
|
197
203
|
:param page_number: The page number to start on. The first page is page 0.
|
|
198
204
|
:param page_size: The number of results to return per page.
|
|
199
205
|
"""
|
|
@@ -208,12 +214,14 @@ class QuickReportRecordAutoPager(_RecordReportPagerBase):
|
|
|
208
214
|
"""
|
|
209
215
|
A class that automatically pages through a quick report and returns the results as a list of records.
|
|
210
216
|
"""
|
|
211
|
-
def __init__(self, user: UserIdentifier, report_term: RawReportTerm, wrapper_type: type[WrappedType],
|
|
217
|
+
def __init__(self, user: UserIdentifier, report_term: RawReportTerm, wrapper_type: type[WrappedType] | str,
|
|
212
218
|
page_number: int = 0, page_size: int = _default_record_page_size):
|
|
213
219
|
"""
|
|
214
220
|
:param user: The current webhook context or a user object to send requests from.
|
|
215
221
|
:param report_term: The raw report term to use for the quick report.
|
|
216
|
-
:param wrapper_type: The record model wrapper type
|
|
222
|
+
:param wrapper_type: The record model wrapper type or data type name of the records being searched for.
|
|
223
|
+
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
224
|
+
instead of WrappedRecordModels.
|
|
217
225
|
:param page_number: The page number to start on. The first page is page 0.
|
|
218
226
|
:param page_size: The number of results to return per page.
|
|
219
227
|
"""
|
|
@@ -225,12 +233,12 @@ class QuickReportRecordAutoPager(_RecordReportPagerBase):
|
|
|
225
233
|
super().__init__(user, first_page_criteria, wrapper_type)
|
|
226
234
|
|
|
227
235
|
|
|
228
|
-
def _add_record_id_column(report: CustomReportCriteria, wrapper_type: type[WrappedType]) -> None:
|
|
236
|
+
def _add_record_id_column(report: CustomReportCriteria, wrapper_type: type[WrappedType] | str) -> None:
|
|
229
237
|
"""
|
|
230
238
|
Given a custom report criteria, ensure that the report contains a Record ID column for the given record model's
|
|
231
239
|
data type. Add one if it is missing.
|
|
232
240
|
"""
|
|
233
|
-
dt: str =
|
|
241
|
+
dt: str = AliasUtil.to_data_type_name(wrapper_type)
|
|
234
242
|
# Ensure that the root data type is the one we're looking for.
|
|
235
243
|
report.root_data_type = dt
|
|
236
244
|
# Enforce that the given custom report has a record ID column.
|
|
@@ -307,7 +307,7 @@ class FieldColumn(ColumnDef):
|
|
|
307
307
|
elif self.search_order == FieldSearchOrder.BUNDLE_ONLY:
|
|
308
308
|
return row.fields.get(self.field_name)
|
|
309
309
|
elif self.search_order == FieldSearchOrder.RECORD_FIRST:
|
|
310
|
-
fields: dict[str, Any] = AliasUtil.
|
|
310
|
+
fields: dict[str, Any] = AliasUtil.to_field_map(record) if record else {}
|
|
311
311
|
if self.field_name not in fields or (self.skip_none_values and fields.get(self.field_name) is None):
|
|
312
312
|
return row.fields.get(self.field_name)
|
|
313
313
|
return fields.get(self.field_name)
|
|
@@ -207,10 +207,12 @@ class AliasUtil:
|
|
|
207
207
|
f"field with the name \"{field}\",")
|
|
208
208
|
|
|
209
209
|
@staticmethod
|
|
210
|
-
def to_field_map(record: SapioRecord) -> FieldMap:
|
|
210
|
+
def to_field_map(record: SapioRecord, include_record_id: bool = False) -> FieldMap:
|
|
211
211
|
"""
|
|
212
|
-
Convert a given record value to a field map.
|
|
212
|
+
Convert a given record value to a field map.
|
|
213
213
|
|
|
214
|
+
:param record: A record which is a DataRecord, PyRecordModel, or WrappedRecordModel.
|
|
215
|
+
:param include_record_id: If true, include the record ID of the record in the field map using the RecordId key.
|
|
214
216
|
:return: The field map for the input record.
|
|
215
217
|
"""
|
|
216
218
|
if isinstance(record, DataRecord):
|
|
@@ -218,20 +220,25 @@ class AliasUtil:
|
|
|
218
220
|
fields: FieldMap = record.get_fields()
|
|
219
221
|
else:
|
|
220
222
|
fields: FieldMap = record.fields.copy_to_dict()
|
|
221
|
-
|
|
223
|
+
# PR-47457: Only include the record ID if the caller requests it, since including the record ID can break
|
|
224
|
+
# callbacks in certain circumstances if the record ID is negative.
|
|
225
|
+
if include_record_id:
|
|
226
|
+
fields["RecordId"] = AliasUtil.to_record_id(record)
|
|
222
227
|
return fields
|
|
223
228
|
|
|
224
229
|
@staticmethod
|
|
225
|
-
def
|
|
230
|
+
def to_field_map_list(records: Iterable[SapioRecord], include_record_id: bool = False) -> list[FieldMap]:
|
|
226
231
|
"""
|
|
227
232
|
Convert a list of variables that could either be DataRecords, PyRecordModels, or WrappedRecordModels
|
|
228
233
|
to a list of their field maps. This includes the given RecordId of the given records.
|
|
229
234
|
|
|
235
|
+
:param records: An iterable of records which are DataRecords, PyRecordModels, or WrappedRecordModels.
|
|
236
|
+
:param include_record_id: If true, include the record ID of the records in the field map using the RecordId key.
|
|
230
237
|
:return: A list of field maps for the input records.
|
|
231
238
|
"""
|
|
232
239
|
field_map_list: list[FieldMap] = []
|
|
233
240
|
for record in records:
|
|
234
|
-
field_map_list.append(AliasUtil.to_field_map(record))
|
|
241
|
+
field_map_list.append(AliasUtil.to_field_map(record, include_record_id))
|
|
235
242
|
return field_map_list
|
|
236
243
|
|
|
237
244
|
@staticmethod
|