sapiopycommons 2025.3.6a453__py3-none-any.whl → 2025.3.10a455__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/callbacks/callback_util.py +366 -1220
- sapiopycommons/chem/Molecules.py +2 -0
- sapiopycommons/datatype/data_fields.py +1 -1
- sapiopycommons/eln/experiment_handler.py +1 -2
- sapiopycommons/eln/experiment_report_util.py +7 -7
- sapiopycommons/files/file_bridge.py +0 -76
- sapiopycommons/files/file_bridge_handler.py +110 -325
- sapiopycommons/files/file_data_handler.py +2 -2
- sapiopycommons/files/file_util.py +11 -36
- sapiopycommons/files/file_validator.py +5 -6
- sapiopycommons/files/file_writer.py +1 -1
- sapiopycommons/flowcyto/flow_cyto.py +1 -1
- sapiopycommons/general/accession_service.py +1 -1
- sapiopycommons/general/aliases.py +28 -48
- sapiopycommons/general/audit_log.py +2 -2
- sapiopycommons/general/custom_report_util.py +1 -24
- sapiopycommons/general/exceptions.py +2 -41
- sapiopycommons/general/popup_util.py +2 -2
- sapiopycommons/general/sapio_links.py +4 -12
- sapiopycommons/multimodal/multimodal.py +0 -1
- sapiopycommons/processtracking/custom_workflow_handler.py +3 -3
- sapiopycommons/recordmodel/record_handler.py +108 -156
- sapiopycommons/webhook/webhook_handlers.py +55 -445
- {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/METADATA +1 -1
- {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/RECORD +27 -33
- sapiopycommons/ai/__init__.py +0 -0
- sapiopycommons/ai/tool_of_tools.py +0 -917
- sapiopycommons/customreport/auto_pagers.py +0 -278
- sapiopycommons/general/directive_util.py +0 -86
- sapiopycommons/general/html_formatter.py +0 -456
- sapiopycommons/samples/aliquot.py +0 -48
- {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/WHEEL +0 -0
- {sapiopycommons-2025.3.6a453.dist-info → sapiopycommons-2025.3.10a455.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import warnings
|
|
4
3
|
from collections.abc import Iterable
|
|
5
4
|
from weakref import WeakValueDictionary
|
|
6
5
|
|
|
@@ -70,66 +69,56 @@ class RecordHandler:
|
|
|
70
69
|
self.rel_man = self.rec_man.relationship_manager
|
|
71
70
|
self.an_man = RecordModelAncestorManager(self.rec_man)
|
|
72
71
|
|
|
73
|
-
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]
|
|
74
|
-
-> WrappedType | PyRecordModel:
|
|
72
|
+
def wrap_model(self, record: DataRecord, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
75
73
|
"""
|
|
76
74
|
Shorthand for adding a single data record as a record model.
|
|
77
75
|
|
|
78
76
|
:param record: The data record to wrap.
|
|
79
|
-
:param wrapper_type: The record model wrapper to use.
|
|
80
|
-
PyRecordModel instead of WrappedRecordModels.
|
|
77
|
+
:param wrapper_type: The record model wrapper to use.
|
|
81
78
|
:return: The record model for the input.
|
|
82
79
|
"""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
86
|
-
return self.inst_man.add_existing_record(record)
|
|
80
|
+
self.__verify_data_type([record], wrapper_type)
|
|
81
|
+
return self.inst_man.add_existing_record_of_type(record, wrapper_type)
|
|
87
82
|
|
|
88
|
-
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]
|
|
89
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
83
|
+
def wrap_models(self, records: Iterable[DataRecord], wrapper_type: type[WrappedType]) -> list[WrappedType]:
|
|
90
84
|
"""
|
|
91
85
|
Shorthand for adding a list of data records as record models.
|
|
92
86
|
|
|
93
87
|
:param records: The data records to wrap.
|
|
94
|
-
:param wrapper_type: The record model wrapper to use.
|
|
95
|
-
PyRecordModels instead of WrappedRecordModels.
|
|
88
|
+
:param wrapper_type: The record model wrapper to use.
|
|
96
89
|
:return: The record models for the input.
|
|
97
90
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
101
|
-
return self.inst_man.add_existing_records(list(records))
|
|
91
|
+
self.__verify_data_type(records, wrapper_type)
|
|
92
|
+
return self.inst_man.add_existing_records_of_type(list(records), wrapper_type)
|
|
102
93
|
|
|
103
|
-
def query_models(self, wrapper_type: type[WrappedType]
|
|
104
|
-
|
|
105
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
94
|
+
def query_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier, value_list: Iterable[FieldValue],
|
|
95
|
+
page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
|
|
106
96
|
"""
|
|
107
97
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
108
98
|
and then converting the results into a list of record models.
|
|
109
99
|
|
|
110
|
-
:param wrapper_type: The record model wrapper to use
|
|
100
|
+
:param wrapper_type: The record model wrapper to use.
|
|
111
101
|
:param field: The field to query on.
|
|
112
102
|
:param value_list: The values of the field to query on.
|
|
113
103
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
114
104
|
only functions if you set a page size or the platform enforces a page size.
|
|
115
105
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
116
|
-
:return: The record models for the queried records.
|
|
117
|
-
then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
106
|
+
:return: The record models for the queried records.
|
|
118
107
|
"""
|
|
119
108
|
criteria: DataRecordPojoPageCriteria | None = None
|
|
120
109
|
if page_size is not None:
|
|
121
110
|
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
122
111
|
return self.query_models_with_criteria(wrapper_type, field, value_list, criteria, page_limit)[0]
|
|
123
112
|
|
|
124
|
-
def query_and_map_models(self, wrapper_type: type[WrappedType]
|
|
113
|
+
def query_and_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
125
114
|
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
126
115
|
page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
|
|
127
|
-
-> dict[FieldValue, list[WrappedType]
|
|
116
|
+
-> dict[FieldValue, list[WrappedType]]:
|
|
128
117
|
"""
|
|
129
118
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
130
119
|
map_by_field to turn the returned list into a dictionary mapping field values to records.
|
|
131
120
|
|
|
132
|
-
:param wrapper_type: The record model wrapper to use
|
|
121
|
+
:param wrapper_type: The record model wrapper to use.
|
|
133
122
|
:param field: The field to query and map on.
|
|
134
123
|
:param value_list: The values of the field to query on.
|
|
135
124
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
@@ -137,24 +126,22 @@ class RecordHandler:
|
|
|
137
126
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
138
127
|
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
139
128
|
:return: The record models for the queried records mapped by field values to the records with that value.
|
|
140
|
-
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
141
|
-
instead of WrappedRecordModels.
|
|
142
129
|
"""
|
|
143
130
|
if mapping_field is None:
|
|
144
131
|
mapping_field = field
|
|
145
132
|
return self.map_by_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
146
133
|
mapping_field)
|
|
147
134
|
|
|
148
|
-
def query_and_unique_map_models(self, wrapper_type: type[WrappedType]
|
|
135
|
+
def query_and_unique_map_models(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
149
136
|
value_list: Iterable[FieldValue], page_limit: int | None = None,
|
|
150
137
|
page_size: int | None = None, *, mapping_field: FieldIdentifier | None = None) \
|
|
151
|
-
-> dict[FieldValue, WrappedType
|
|
138
|
+
-> dict[FieldValue, WrappedType]:
|
|
152
139
|
"""
|
|
153
140
|
Shorthand for using query_models to search for records given values on a specific field and then using
|
|
154
141
|
map_by_unique_field to turn the returned list into a dictionary mapping field values to records.
|
|
155
142
|
If any two records share the same field value, throws an exception.
|
|
156
143
|
|
|
157
|
-
:param wrapper_type: The record model wrapper to use
|
|
144
|
+
:param wrapper_type: The record model wrapper to use.
|
|
158
145
|
:param field: The field to query and map on.
|
|
159
146
|
:param value_list: The values of the field to query on.
|
|
160
147
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
@@ -162,150 +149,134 @@ class RecordHandler:
|
|
|
162
149
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
163
150
|
:param mapping_field: If provided, use this field to map against instead of the field that was queried on.
|
|
164
151
|
:return: The record models for the queried records mapped by field values to the record with that value.
|
|
165
|
-
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
166
|
-
instead of WrappedRecordModels.
|
|
167
152
|
"""
|
|
168
153
|
if mapping_field is None:
|
|
169
154
|
mapping_field = field
|
|
170
155
|
return self.map_by_unique_field(self.query_models(wrapper_type, field, value_list, page_limit, page_size),
|
|
171
156
|
mapping_field)
|
|
172
157
|
|
|
173
|
-
def query_models_with_criteria(self, wrapper_type: type[WrappedType]
|
|
158
|
+
def query_models_with_criteria(self, wrapper_type: type[WrappedType], field: FieldIdentifier,
|
|
174
159
|
value_list: Iterable[FieldValue],
|
|
175
160
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
176
161
|
page_limit: int | None = None) \
|
|
177
|
-
-> tuple[list[WrappedType]
|
|
162
|
+
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
178
163
|
"""
|
|
179
164
|
Shorthand for using the data record manager to query for a list of data records by field value
|
|
180
165
|
and then converting the results into a list of record models.
|
|
181
166
|
|
|
182
|
-
:param wrapper_type: The record model wrapper to use
|
|
167
|
+
:param wrapper_type: The record model wrapper to use.
|
|
183
168
|
:param field: The field to query on.
|
|
184
169
|
:param value_list: The values of the field to query on.
|
|
185
170
|
:param paging_criteria: The paging criteria to start the query with.
|
|
186
171
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
187
172
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
188
173
|
enforces a page size.
|
|
189
|
-
:return: The record models for the queried records and the final paging criteria.
|
|
190
|
-
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
174
|
+
:return: The record models for the queried records and the final paging criteria.
|
|
191
175
|
"""
|
|
192
|
-
dt: str =
|
|
193
|
-
if isinstance(wrapper_type, str):
|
|
194
|
-
wrapper_type = None
|
|
176
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
195
177
|
field: str = AliasUtil.to_data_field_name(field)
|
|
196
178
|
pager = QueryDataRecordsAutoPager(dt, field, list(value_list), self.user, paging_criteria)
|
|
197
179
|
pager.max_page = page_limit
|
|
198
180
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
199
181
|
|
|
200
|
-
def query_models_by_id(self, wrapper_type: type[WrappedType]
|
|
201
|
-
page_limit: int | None = None, page_size: int | None = None)
|
|
202
|
-
-> list[WrappedType] | list[PyRecordModel]:
|
|
182
|
+
def query_models_by_id(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
183
|
+
page_limit: int | None = None, page_size: int | None = None) -> list[WrappedType]:
|
|
203
184
|
"""
|
|
204
185
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
205
186
|
and then converting the results into a list of record models.
|
|
206
187
|
|
|
207
|
-
:param wrapper_type: The record model wrapper to use
|
|
188
|
+
:param wrapper_type: The record model wrapper to use.
|
|
208
189
|
:param ids: The list of record IDs to query.
|
|
209
190
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
210
191
|
only functions if you set a page size or the platform enforces a page size.
|
|
211
192
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
212
|
-
:return: The record models for the queried records.
|
|
213
|
-
then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
193
|
+
:return: The record models for the queried records.
|
|
214
194
|
"""
|
|
215
195
|
criteria: DataRecordPojoPageCriteria | None = None
|
|
216
196
|
if page_size is not None:
|
|
217
197
|
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
218
198
|
return self.query_models_by_id_with_criteria(wrapper_type, ids, criteria, page_limit)[0]
|
|
219
199
|
|
|
220
|
-
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType]
|
|
200
|
+
def query_models_by_id_with_criteria(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
221
201
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
222
202
|
page_limit: int | None = None) \
|
|
223
|
-
-> tuple[list[WrappedType]
|
|
203
|
+
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
224
204
|
"""
|
|
225
205
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
226
206
|
and then converting the results into a list of record models.
|
|
227
207
|
|
|
228
|
-
:param wrapper_type: The record model wrapper to use
|
|
208
|
+
:param wrapper_type: The record model wrapper to use.
|
|
229
209
|
:param ids: The list of record IDs to query.
|
|
230
210
|
:param paging_criteria: The paging criteria to start the query with.
|
|
231
211
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
232
212
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
233
213
|
enforces a page size.
|
|
234
|
-
:return: The record models for the queried records and the final paging criteria.
|
|
235
|
-
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
214
|
+
:return: The record models for the queried records and the final paging criteria.
|
|
236
215
|
"""
|
|
237
|
-
dt: str =
|
|
238
|
-
if isinstance(wrapper_type, str):
|
|
239
|
-
wrapper_type = None
|
|
216
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
240
217
|
pager = QueryDataRecordByIdListAutoPager(dt, list(ids), self.user, paging_criteria)
|
|
241
218
|
pager.max_page = page_limit
|
|
242
219
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
243
220
|
|
|
244
|
-
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType]
|
|
221
|
+
def query_models_by_id_and_map(self, wrapper_type: type[WrappedType], ids: Iterable[int],
|
|
245
222
|
page_limit: int | None = None, page_size: int | None = None) \
|
|
246
|
-
-> dict[int, WrappedType
|
|
223
|
+
-> dict[int, WrappedType]:
|
|
247
224
|
"""
|
|
248
225
|
Shorthand for using the data record manager to query for a list of data records by record ID
|
|
249
226
|
and then converting the results into a dictionary of record ID to the record model for that ID.
|
|
250
227
|
|
|
251
|
-
:param wrapper_type: The record model wrapper to use
|
|
228
|
+
:param wrapper_type: The record model wrapper to use.
|
|
252
229
|
:param ids: The list of record IDs to query.
|
|
253
230
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
254
231
|
only functions if you set a page size or the platform enforces a page size.
|
|
255
232
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
256
233
|
:return: The record models for the queried records mapped in a dictionary by their record ID.
|
|
257
|
-
If a data type name was used instead of a model wrapper, then the returned records will be PyRecordModels
|
|
258
|
-
instead of WrappedRecordModels.
|
|
259
234
|
"""
|
|
260
|
-
return {
|
|
235
|
+
return {x.record_id: x for x in self.query_models_by_id(wrapper_type, ids, page_limit, page_size)}
|
|
261
236
|
|
|
262
|
-
def query_all_models(self, wrapper_type: type[WrappedType]
|
|
263
|
-
page_size: int | None = None) -> list[WrappedType]
|
|
237
|
+
def query_all_models(self, wrapper_type: type[WrappedType], page_limit: int | None = None,
|
|
238
|
+
page_size: int | None = None) -> list[WrappedType]:
|
|
264
239
|
"""
|
|
265
240
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
266
241
|
and then converting the results into a list of record models.
|
|
267
242
|
|
|
268
|
-
:param wrapper_type: The record model wrapper to use
|
|
243
|
+
:param wrapper_type: The record model wrapper to use.
|
|
269
244
|
:param page_limit: The maximum number of pages to query. If None, exhausts all possible pages. This parameter
|
|
270
245
|
only functions if you set a page size or the platform enforces a page size.
|
|
271
246
|
:param page_size: The size of the pages to query. If None, the page size may be limited by the platform.
|
|
272
|
-
:return: The record models for the queried records.
|
|
273
|
-
then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
247
|
+
:return: The record models for the queried records.
|
|
274
248
|
"""
|
|
275
249
|
criteria: DataRecordPojoPageCriteria | None = None
|
|
276
250
|
if page_size is not None:
|
|
277
251
|
criteria = DataRecordPojoPageCriteria(page_size=page_size)
|
|
278
252
|
return self.query_all_models_with_criteria(wrapper_type, criteria, page_limit)[0]
|
|
279
253
|
|
|
280
|
-
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType]
|
|
254
|
+
def query_all_models_with_criteria(self, wrapper_type: type[WrappedType],
|
|
281
255
|
paging_criteria: DataRecordPojoPageCriteria | None = None,
|
|
282
256
|
page_limit: int | None = None) \
|
|
283
|
-
-> tuple[list[WrappedType]
|
|
257
|
+
-> tuple[list[WrappedType], DataRecordPojoPageCriteria]:
|
|
284
258
|
"""
|
|
285
259
|
Shorthand for using the data record manager to query for all data records of a given type
|
|
286
260
|
and then converting the results into a list of record models.
|
|
287
261
|
|
|
288
|
-
:param wrapper_type: The record model wrapper to use
|
|
262
|
+
:param wrapper_type: The record model wrapper to use.
|
|
289
263
|
:param paging_criteria: The paging criteria to start the query with.
|
|
290
264
|
:param page_limit: The maximum number of pages to query from the starting criteria. If None, exhausts all
|
|
291
265
|
possible pages. This parameter only functions if you set a page size in the paging criteria or the platform
|
|
292
266
|
enforces a page size.
|
|
293
|
-
:return: The record models for the queried records and the final paging criteria.
|
|
294
|
-
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
267
|
+
:return: The record models for the queried records and the final paging criteria.
|
|
295
268
|
"""
|
|
296
|
-
dt: str =
|
|
297
|
-
if isinstance(wrapper_type, str):
|
|
298
|
-
wrapper_type = None
|
|
269
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
299
270
|
pager = QueryAllRecordsOfTypeAutoPager(dt, self.user, paging_criteria)
|
|
300
271
|
pager.max_page = page_limit
|
|
301
272
|
return self.wrap_models(pager.get_all_at_once(), wrapper_type), pager.next_page_criteria
|
|
302
273
|
|
|
303
|
-
def query_models_by_report(self, wrapper_type: type[WrappedType]
|
|
274
|
+
def query_models_by_report(self, wrapper_type: type[WrappedType],
|
|
304
275
|
report_name: str | RawReportTerm | CustomReportCriteria,
|
|
305
276
|
filters: dict[FieldIdentifierKey, Iterable[FieldValue]] | None = None,
|
|
306
277
|
page_limit: int | None = None,
|
|
307
278
|
page_size: int | None = None,
|
|
308
|
-
page_number: int | None = None) -> list[WrappedType]
|
|
279
|
+
page_number: int | None = None) -> list[WrappedType]:
|
|
309
280
|
"""
|
|
310
281
|
Run a report and use the results of that report to query for and return the records in the report results.
|
|
311
282
|
First runs the report, then runs a data record manager query on the results of the custom report.
|
|
@@ -315,7 +286,7 @@ class RecordHandler:
|
|
|
315
286
|
|
|
316
287
|
Any given custom report criteria should only have columns from a single data type.
|
|
317
288
|
|
|
318
|
-
:param wrapper_type: The record model wrapper to use
|
|
289
|
+
:param wrapper_type: The record model wrapper to use.
|
|
319
290
|
:param report_name: The name of a system report, or a raw report term for a quick report, or custom report
|
|
320
291
|
criteria for a custom report.
|
|
321
292
|
:param filters: If provided, filter the results of the report using the given mapping of headers to values to
|
|
@@ -327,10 +298,8 @@ class RecordHandler:
|
|
|
327
298
|
:param page_number: The page number to start the search from, If None, starts on the first page.
|
|
328
299
|
If the input report is a custom report criteria, uses the value from the criteria, unless this value is
|
|
329
300
|
not None, in which case it overwrites the given report's value. Note that the number of the first page is 0.
|
|
330
|
-
:return: The record models for the queried records that matched the given report.
|
|
331
|
-
instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
301
|
+
:return: The record models for the queried records that matched the given report.
|
|
332
302
|
"""
|
|
333
|
-
warnings.warn("Deprecated in favor of the [System/Custom/Quick]ReportRecordAutoPager classes.", DeprecationWarning)
|
|
334
303
|
if isinstance(report_name, str):
|
|
335
304
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_system_report(self.user, report_name, filters,
|
|
336
305
|
page_limit, page_size, page_number)
|
|
@@ -338,7 +307,7 @@ class RecordHandler:
|
|
|
338
307
|
results: list[dict[str, FieldValue]] = CustomReportUtil.run_quick_report(self.user, report_name, filters,
|
|
339
308
|
page_limit, page_size, page_number)
|
|
340
309
|
elif isinstance(report_name, CustomReportCriteria):
|
|
341
|
-
dt: str =
|
|
310
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
342
311
|
# Ensure that the root data type is the one we're looking for.
|
|
343
312
|
report_name.root_data_type = dt
|
|
344
313
|
# Raise an exception if any column in the report doesn't match the given data type.
|
|
@@ -358,38 +327,35 @@ class RecordHandler:
|
|
|
358
327
|
ids: list[int] = [row["RecordId"] for row in results]
|
|
359
328
|
return self.query_models_by_id(wrapper_type, ids)
|
|
360
329
|
|
|
361
|
-
def add_model(self, wrapper_type: type[WrappedType]
|
|
330
|
+
def add_model(self, wrapper_type: type[WrappedType]) -> WrappedType:
|
|
362
331
|
"""
|
|
363
332
|
Shorthand for using the instance manager to add a new record model of the given type.
|
|
364
333
|
|
|
365
|
-
:param wrapper_type: The record model wrapper to use
|
|
366
|
-
:return: The newly added record model.
|
|
367
|
-
returned record will be a PyRecordModel instead of a WrappedRecordModel.
|
|
334
|
+
:param wrapper_type: The record model wrapper to use.
|
|
335
|
+
:return: The newly added record model.
|
|
368
336
|
"""
|
|
369
337
|
return self.inst_man.add_new_record_of_type(wrapper_type)
|
|
370
338
|
|
|
371
|
-
def add_models(self, wrapper_type: type[WrappedType]
|
|
339
|
+
def add_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
372
340
|
"""
|
|
373
341
|
Shorthand for using the instance manager to add new record models of the given type.
|
|
374
342
|
|
|
375
|
-
:param wrapper_type: The record model wrapper to use
|
|
343
|
+
:param wrapper_type: The record model wrapper to use.
|
|
376
344
|
:param num: The number of models to create.
|
|
377
|
-
:return: The newly added record models.
|
|
378
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
345
|
+
:return: The newly added record models.
|
|
379
346
|
"""
|
|
380
347
|
return self.inst_man.add_new_records_of_type(num, wrapper_type)
|
|
381
348
|
|
|
382
|
-
def add_models_with_data(self, wrapper_type: type[WrappedType]
|
|
383
|
-
-> list[WrappedType]
|
|
349
|
+
def add_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
350
|
+
-> list[WrappedType]:
|
|
384
351
|
"""
|
|
385
352
|
Shorthand for using the instance manager to add new models of the given type, and then initializing all those
|
|
386
353
|
models with the given fields.
|
|
387
354
|
|
|
388
|
-
:param wrapper_type: The record model wrapper to use
|
|
355
|
+
:param wrapper_type: The record model wrapper to use.
|
|
389
356
|
:param fields: A list of field maps to initialize the record models with.
|
|
390
357
|
:return: The newly added record models with the provided fields set. The records will be in the same order as
|
|
391
|
-
the fields in the fields list.
|
|
392
|
-
records will be PyRecordModels instead of WrappedRecordModels.
|
|
358
|
+
the fields in the fields list.
|
|
393
359
|
"""
|
|
394
360
|
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
395
361
|
models: list[WrappedType] = self.add_models(wrapper_type, len(fields))
|
|
@@ -397,9 +363,8 @@ class RecordHandler:
|
|
|
397
363
|
model.set_field_values(field_list)
|
|
398
364
|
return models
|
|
399
365
|
|
|
400
|
-
def find_or_add_model(self, wrapper_type: type[WrappedType]
|
|
401
|
-
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None)
|
|
402
|
-
-> WrappedType | PyRecordModel:
|
|
366
|
+
def find_or_add_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
367
|
+
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType:
|
|
403
368
|
"""
|
|
404
369
|
Find a unique record that matches the given field values. If no such records exist, add a record model to the
|
|
405
370
|
cache with the identifying fields set to the desired values. This record will be created in the system when
|
|
@@ -410,14 +375,12 @@ class RecordHandler:
|
|
|
410
375
|
|
|
411
376
|
Makes a webservice call to query for the existing record.
|
|
412
377
|
|
|
413
|
-
:param wrapper_type: The record model wrapper to use
|
|
378
|
+
:param wrapper_type: The record model wrapper to use.
|
|
414
379
|
:param primary_identifier: The data field name of the field to search on.
|
|
415
380
|
:param id_value: The value of the identifying field to search for.
|
|
416
381
|
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
417
382
|
the primary identifier.
|
|
418
383
|
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
419
|
-
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
420
|
-
instead of a WrappedRecordModel.
|
|
421
384
|
"""
|
|
422
385
|
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
423
386
|
# If no secondary identifiers were provided, use an empty dictionary.
|
|
@@ -438,25 +401,22 @@ class RecordHandler:
|
|
|
438
401
|
secondary_identifiers.update({primary_identifier: id_value})
|
|
439
402
|
return self.add_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
440
403
|
|
|
441
|
-
def create_models(self, wrapper_type: type[WrappedType]
|
|
404
|
+
def create_models(self, wrapper_type: type[WrappedType], num: int) -> list[WrappedType]:
|
|
442
405
|
"""
|
|
443
406
|
Shorthand for creating new records via the data record manager and then returning them as wrapped
|
|
444
407
|
record models. Useful in cases where your record model needs to have a valid record ID.
|
|
445
408
|
|
|
446
409
|
Makes a webservice call to create the data records.
|
|
447
410
|
|
|
448
|
-
:param wrapper_type: The record model wrapper to use
|
|
411
|
+
:param wrapper_type: The record model wrapper to use.
|
|
449
412
|
:param num: The number of new records to create.
|
|
450
|
-
:return: The newly created record models.
|
|
451
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
413
|
+
:return: The newly created record models.
|
|
452
414
|
"""
|
|
453
|
-
dt: str =
|
|
454
|
-
if isinstance(wrapper_type, str):
|
|
455
|
-
wrapper_type = None
|
|
415
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
456
416
|
return self.wrap_models(self.dr_man.add_data_records(dt, num), wrapper_type)
|
|
457
417
|
|
|
458
|
-
def create_models_with_data(self, wrapper_type: type[WrappedType]
|
|
459
|
-
-> list[WrappedType]
|
|
418
|
+
def create_models_with_data(self, wrapper_type: type[WrappedType], fields: list[FieldIdentifierMap]) \
|
|
419
|
+
-> list[WrappedType]:
|
|
460
420
|
"""
|
|
461
421
|
Shorthand for creating new records via the data record manager with field data to initialize the records with
|
|
462
422
|
and then returning them as wrapped record models. Useful in cases where your record model needs to have a valid
|
|
@@ -464,20 +424,17 @@ class RecordHandler:
|
|
|
464
424
|
|
|
465
425
|
Makes a webservice call to create the data records.
|
|
466
426
|
|
|
467
|
-
:param wrapper_type: The record model wrapper to use
|
|
427
|
+
:param wrapper_type: The record model wrapper to use.
|
|
468
428
|
:param fields: The field map list to initialize the new data records with.
|
|
469
|
-
:return: The newly created record models.
|
|
470
|
-
returned records will be PyRecordModels instead of WrappedRecordModels.
|
|
429
|
+
:return: The newly created record models.
|
|
471
430
|
"""
|
|
472
|
-
dt: str =
|
|
473
|
-
if isinstance(wrapper_type, str):
|
|
474
|
-
wrapper_type = None
|
|
431
|
+
dt: str = wrapper_type.get_wrapper_data_type_name()
|
|
475
432
|
fields: list[FieldMap] = AliasUtil.to_data_field_names_list_dict(fields)
|
|
476
433
|
return self.wrap_models(self.dr_man.add_data_records_with_data(dt, fields), wrapper_type)
|
|
477
434
|
|
|
478
|
-
def find_or_create_model(self, wrapper_type: type[WrappedType]
|
|
435
|
+
def find_or_create_model(self, wrapper_type: type[WrappedType], primary_identifier: FieldIdentifier,
|
|
479
436
|
id_value: FieldValue, secondary_identifiers: FieldIdentifierMap | None = None) \
|
|
480
|
-
-> WrappedType
|
|
437
|
+
-> WrappedType:
|
|
481
438
|
"""
|
|
482
439
|
Find a unique record that matches the given field values. If no such records exist, create one with the
|
|
483
440
|
identifying fields set to the desired values. If more than one record with the identifying values exists,
|
|
@@ -489,14 +446,12 @@ class RecordHandler:
|
|
|
489
446
|
Makes a webservice call to query for the existing record. Makes an additional webservice call if the record
|
|
490
447
|
needs to be created.
|
|
491
448
|
|
|
492
|
-
:param wrapper_type: The record model wrapper to use
|
|
449
|
+
:param wrapper_type: The record model wrapper to use.
|
|
493
450
|
:param primary_identifier: The data field name of the field to search on.
|
|
494
451
|
:param id_value: The value of the identifying field to search for.
|
|
495
452
|
:param secondary_identifiers: Optional fields used to filter the records that are returned after searching on
|
|
496
453
|
the primary identifier.
|
|
497
454
|
:return: The record model with the identifying field value, either pulled from the system or newly created.
|
|
498
|
-
If a data type name was used instead of a model wrapper, then the returned record will be a PyRecordModel
|
|
499
|
-
instead of a WrappedRecordModel.
|
|
500
455
|
"""
|
|
501
456
|
# PR-46335: Initialize the secondary identifiers parameter if None is provided to avoid an exception.
|
|
502
457
|
# If no secondary identifiers were provided, use an empty dictionary.
|
|
@@ -518,8 +473,7 @@ class RecordHandler:
|
|
|
518
473
|
return self.create_models_with_data(wrapper_type, [secondary_identifiers])[0]
|
|
519
474
|
|
|
520
475
|
@staticmethod
|
|
521
|
-
def map_to_parent(models: Iterable[
|
|
522
|
-
-> dict[WrappedRecordModel, WrappedType]:
|
|
476
|
+
def map_to_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) -> dict[RecordModel, WrappedType]:
|
|
523
477
|
"""
|
|
524
478
|
Map a list of record models to a single parent of a given type. The parents must already be loaded.
|
|
525
479
|
|
|
@@ -528,14 +482,14 @@ class RecordHandler:
|
|
|
528
482
|
:return: A dict[ModelType, ParentType]. If an input model doesn't have a parent of the given parent type, then
|
|
529
483
|
it will map to None.
|
|
530
484
|
"""
|
|
531
|
-
return_dict: dict[
|
|
485
|
+
return_dict: dict[RecordModel, WrappedType] = {}
|
|
532
486
|
for model in models:
|
|
533
487
|
return_dict[model] = model.get_parent_of_type(parent_type)
|
|
534
488
|
return return_dict
|
|
535
489
|
|
|
536
490
|
@staticmethod
|
|
537
|
-
def map_to_parents(models: Iterable[
|
|
538
|
-
-> dict[
|
|
491
|
+
def map_to_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
492
|
+
-> dict[RecordModel, list[WrappedType]]:
|
|
539
493
|
"""
|
|
540
494
|
Map a list of record models to a list parents of a given type. The parents must already be loaded.
|
|
541
495
|
|
|
@@ -544,14 +498,14 @@ class RecordHandler:
|
|
|
544
498
|
:return: A dict[ModelType, list[ParentType]]. If an input model doesn't have a parent of the given parent type,
|
|
545
499
|
then it will map to an empty list.
|
|
546
500
|
"""
|
|
547
|
-
return_dict: dict[
|
|
501
|
+
return_dict: dict[RecordModel, list[WrappedType]] = {}
|
|
548
502
|
for model in models:
|
|
549
503
|
return_dict[model] = model.get_parents_of_type(parent_type)
|
|
550
504
|
return return_dict
|
|
551
505
|
|
|
552
506
|
@staticmethod
|
|
553
|
-
def map_by_parent(models: Iterable[
|
|
554
|
-
-> dict[WrappedType,
|
|
507
|
+
def map_by_parent(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
508
|
+
-> dict[WrappedType, RecordModel]:
|
|
555
509
|
"""
|
|
556
510
|
Take a list of record models and map them by their parent. Essentially an inversion of map_to_parent.
|
|
557
511
|
If two records share the same parent, an exception will be thrown. The parents must already be loaded.
|
|
@@ -561,8 +515,8 @@ class RecordHandler:
|
|
|
561
515
|
:return: A dict[ParentType, ModelType]. If an input model doesn't have a parent of the given parent type,
|
|
562
516
|
then it will not be in the resulting dictionary.
|
|
563
517
|
"""
|
|
564
|
-
to_parent: dict[
|
|
565
|
-
by_parent: dict[WrappedType,
|
|
518
|
+
to_parent: dict[RecordModel, WrappedType] = RecordHandler.map_to_parent(models, parent_type)
|
|
519
|
+
by_parent: dict[WrappedType, RecordModel] = {}
|
|
566
520
|
for record, parent in to_parent.items():
|
|
567
521
|
if parent is None:
|
|
568
522
|
continue
|
|
@@ -573,8 +527,8 @@ class RecordHandler:
|
|
|
573
527
|
return by_parent
|
|
574
528
|
|
|
575
529
|
@staticmethod
|
|
576
|
-
def map_by_parents(models: Iterable[
|
|
577
|
-
-> dict[WrappedType, list[
|
|
530
|
+
def map_by_parents(models: Iterable[RecordModel], parent_type: type[WrappedType]) \
|
|
531
|
+
-> dict[WrappedType, list[RecordModel]]:
|
|
578
532
|
"""
|
|
579
533
|
Take a list of record models and map them by their parents. Essentially an inversion of map_to_parents. Input
|
|
580
534
|
models that share a parent will end up in the same list. The parents must already be loaded.
|
|
@@ -584,16 +538,15 @@ class RecordHandler:
|
|
|
584
538
|
:return: A dict[ParentType, list[ModelType]]. If an input model doesn't have a parent of the given parent type,
|
|
585
539
|
then it will not be in the resulting dictionary.
|
|
586
540
|
"""
|
|
587
|
-
to_parents: dict[
|
|
588
|
-
by_parents: dict[WrappedType, list[
|
|
541
|
+
to_parents: dict[RecordModel, list[WrappedType]] = RecordHandler.map_to_parents(models, parent_type)
|
|
542
|
+
by_parents: dict[WrappedType, list[RecordModel]] = {}
|
|
589
543
|
for record, parents in to_parents.items():
|
|
590
544
|
for parent in parents:
|
|
591
545
|
by_parents.setdefault(parent, []).append(record)
|
|
592
546
|
return by_parents
|
|
593
547
|
|
|
594
548
|
@staticmethod
|
|
595
|
-
def map_to_child(models: Iterable[
|
|
596
|
-
-> dict[WrappedRecordModel, WrappedType]:
|
|
549
|
+
def map_to_child(models: Iterable[RecordModel], child_type: type[WrappedType]) -> dict[RecordModel, WrappedType]:
|
|
597
550
|
"""
|
|
598
551
|
Map a list of record models to a single child of a given type. The children must already be loaded.
|
|
599
552
|
|
|
@@ -602,14 +555,14 @@ class RecordHandler:
|
|
|
602
555
|
:return: A dict[ModelType, ChildType]. If an input model doesn't have a child of the given child type, then
|
|
603
556
|
it will map to None.
|
|
604
557
|
"""
|
|
605
|
-
return_dict: dict[
|
|
558
|
+
return_dict: dict[RecordModel, WrappedType] = {}
|
|
606
559
|
for model in models:
|
|
607
560
|
return_dict[model] = model.get_child_of_type(child_type)
|
|
608
561
|
return return_dict
|
|
609
562
|
|
|
610
563
|
@staticmethod
|
|
611
|
-
def map_to_children(models: Iterable[
|
|
612
|
-
-> dict[
|
|
564
|
+
def map_to_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
565
|
+
-> dict[RecordModel, list[WrappedType]]:
|
|
613
566
|
"""
|
|
614
567
|
Map a list of record models to a list children of a given type. The children must already be loaded.
|
|
615
568
|
|
|
@@ -618,14 +571,14 @@ class RecordHandler:
|
|
|
618
571
|
:return: A dict[ModelType, list[ChildType]]. If an input model doesn't have children of the given child type,
|
|
619
572
|
then it will map to an empty list.
|
|
620
573
|
"""
|
|
621
|
-
return_dict: dict[
|
|
574
|
+
return_dict: dict[RecordModel, list[WrappedType]] = {}
|
|
622
575
|
for model in models:
|
|
623
576
|
return_dict[model] = model.get_children_of_type(child_type)
|
|
624
577
|
return return_dict
|
|
625
578
|
|
|
626
579
|
@staticmethod
|
|
627
|
-
def map_by_child(models: Iterable[
|
|
628
|
-
-> dict[WrappedType,
|
|
580
|
+
def map_by_child(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
581
|
+
-> dict[WrappedType, RecordModel]:
|
|
629
582
|
"""
|
|
630
583
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_child.
|
|
631
584
|
If two records share the same child, an exception will be thrown. The children must already be loaded.
|
|
@@ -635,8 +588,8 @@ class RecordHandler:
|
|
|
635
588
|
:return: A dict[ChildType, ModelType]. If an input model doesn't have a child of the given child type,
|
|
636
589
|
then it will not be in the resulting dictionary.
|
|
637
590
|
"""
|
|
638
|
-
to_child: dict[
|
|
639
|
-
by_child: dict[WrappedType,
|
|
591
|
+
to_child: dict[RecordModel, WrappedType] = RecordHandler.map_to_child(models, child_type)
|
|
592
|
+
by_child: dict[WrappedType, RecordModel] = {}
|
|
640
593
|
for record, child in to_child.items():
|
|
641
594
|
if child is None:
|
|
642
595
|
continue
|
|
@@ -647,8 +600,8 @@ class RecordHandler:
|
|
|
647
600
|
return by_child
|
|
648
601
|
|
|
649
602
|
@staticmethod
|
|
650
|
-
def map_by_children(models: Iterable[
|
|
651
|
-
-> dict[WrappedType, list[
|
|
603
|
+
def map_by_children(models: Iterable[RecordModel], child_type: type[WrappedType]) \
|
|
604
|
+
-> dict[WrappedType, list[RecordModel]]:
|
|
652
605
|
"""
|
|
653
606
|
Take a list of record models and map them by their children. Essentially an inversion of map_to_children. Input
|
|
654
607
|
models that share a child will end up in the same list. The children must already be loaded.
|
|
@@ -658,8 +611,8 @@ class RecordHandler:
|
|
|
658
611
|
:return: A dict[ChildType, list[ModelType]]. If an input model doesn't have children of the given child type,
|
|
659
612
|
then it will not be in the resulting dictionary.
|
|
660
613
|
"""
|
|
661
|
-
to_children: dict[
|
|
662
|
-
by_children: dict[WrappedType, list[
|
|
614
|
+
to_children: dict[RecordModel, list[WrappedType]] = RecordHandler.map_to_children(models, child_type)
|
|
615
|
+
by_children: dict[WrappedType, list[RecordModel]] = {}
|
|
663
616
|
for record, children in to_children.items():
|
|
664
617
|
for child in children:
|
|
665
618
|
by_children.setdefault(child, []).append(record)
|
|
@@ -940,14 +893,14 @@ class RecordHandler:
|
|
|
940
893
|
|
|
941
894
|
@staticmethod
|
|
942
895
|
def values_to_field_maps(field_name: FieldIdentifier, values: Iterable[FieldValue],
|
|
943
|
-
existing_fields: list[
|
|
896
|
+
existing_fields: list[FieldIdentifier] | None = None) -> list[FieldMap]:
|
|
944
897
|
"""
|
|
945
898
|
Add a list of values for a specific field to a list of dictionaries pairing each value to that field name.
|
|
946
899
|
|
|
947
900
|
:param field_name: The name of the field that the values are from.
|
|
948
901
|
:param values: A list of field values.
|
|
949
902
|
:param existing_fields: An optional existing fields map list to add the new values to. Values are added in the
|
|
950
|
-
|
|
903
|
+
list in the same order that they appear. If no existing fields are provided, returns a new fields map list.
|
|
951
904
|
:return: A fields map list that contains the given values mapped by the given field name.
|
|
952
905
|
"""
|
|
953
906
|
# Update the existing fields map list if one is given.
|
|
@@ -1117,19 +1070,18 @@ class RecordHandler:
|
|
|
1117
1070
|
ret_dict.update({model: self.inst_man.wrap(current[0], wrapper_type) if current else None})
|
|
1118
1071
|
return ret_dict
|
|
1119
1072
|
|
|
1120
|
-
def __find_model(self, wrapper_type: type[WrappedType]
|
|
1121
|
-
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType |
|
|
1073
|
+
def __find_model(self, wrapper_type: type[WrappedType], primary_identifier: str, id_value: FieldValue,
|
|
1074
|
+
secondary_identifiers: FieldIdentifierMap | None = None) -> WrappedType | None:
|
|
1122
1075
|
"""
|
|
1123
1076
|
Find a record from the system that matches the given field values. The primary identifier and value is used
|
|
1124
1077
|
to query for the record, then the secondary identifiers may be optionally provided to further filter the
|
|
1125
1078
|
returned results. If no record is found with these filters, returns None.
|
|
1126
1079
|
"""
|
|
1127
1080
|
# Query for all records that match the primary identifier.
|
|
1128
|
-
results: list[WrappedType]
|
|
1129
|
-
[id_value])
|
|
1081
|
+
results: list[WrappedType] = self.query_models(wrapper_type, primary_identifier, [id_value])
|
|
1130
1082
|
|
|
1131
1083
|
# Find the one record, if any, that matches the secondary identifiers.
|
|
1132
|
-
unique_record: WrappedType |
|
|
1084
|
+
unique_record: WrappedType | None = None
|
|
1133
1085
|
for result in results:
|
|
1134
1086
|
matches_all: bool = True
|
|
1135
1087
|
for field, value in secondary_identifiers.items():
|
|
@@ -1139,7 +1091,7 @@ class RecordHandler:
|
|
|
1139
1091
|
if matches_all:
|
|
1140
1092
|
# If a previous record in the results already matched all identifiers, then throw an exception.
|
|
1141
1093
|
if unique_record is not None:
|
|
1142
|
-
raise SapioException(f"More than one record of type {
|
|
1094
|
+
raise SapioException(f"More than one record of type {wrapper_type.get_wrapper_data_type_name()} "
|
|
1143
1095
|
f"encountered in system that matches all provided identifiers.")
|
|
1144
1096
|
unique_record = result
|
|
1145
1097
|
return unique_record
|