sapiopycommons 2024.6.6a249__py3-none-any.whl → 2024.6.18a254__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.
- sapiopycommons/callbacks/callback_util.py +256 -22
- sapiopycommons/files/file_bridge.py +16 -10
- sapiopycommons/files/file_bridge_handler.py +318 -0
- sapiopycommons/files/file_util.py +13 -6
- sapiopycommons/files/file_validator.py +71 -0
- sapiopycommons/general/custom_report_util.py +198 -26
- sapiopycommons/multimodal/multimodal.py +0 -13
- sapiopycommons/multimodal/multimodal_data.py +9 -31
- sapiopycommons/recordmodel/record_handler.py +278 -45
- sapiopycommons/webhook/webhook_handlers.py +58 -1
- {sapiopycommons-2024.6.6a249.dist-info → sapiopycommons-2024.6.18a254.dist-info}/METADATA +1 -1
- {sapiopycommons-2024.6.6a249.dist-info → sapiopycommons-2024.6.18a254.dist-info}/RECORD +14 -13
- {sapiopycommons-2024.6.6a249.dist-info → sapiopycommons-2024.6.18a254.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.6.6a249.dist-info → sapiopycommons-2024.6.18a254.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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) \
|
|
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
|
+
"""
|
|
152
|
+
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
153
|
+
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
154
|
+
|
|
155
|
+
result = None
|
|
156
|
+
has_next_page: bool = True
|
|
157
|
+
rows: list[list[Any]] = []
|
|
158
|
+
cur_page: int = 1
|
|
159
|
+
while has_next_page and (not page_limit or cur_page < page_limit):
|
|
160
|
+
result = report_man.run_system_report_by_name(report_name, page_size, page_number)
|
|
161
|
+
page_size = result.page_size
|
|
162
|
+
page_number = result.page_number
|
|
163
|
+
has_next_page = result.has_next_page
|
|
164
|
+
rows.extend(result.result_table)
|
|
165
|
+
cur_page += 1
|
|
166
|
+
return result.column_list, rows
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def __exhaust_custom_report(context: 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) \
|
|
73
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
|
+
"""
|
|
74
210
|
user: SapioUser = context if isinstance(context, SapioUser) else context.user
|
|
75
211
|
report_man = DataMgmtServer.get_custom_report_manager(user)
|
|
76
212
|
|
|
77
|
-
|
|
78
|
-
page_size: int | None = None
|
|
79
|
-
page_number: int | None = None
|
|
213
|
+
result = None
|
|
80
214
|
has_next_page: bool = True
|
|
81
215
|
rows: list[list[Any]] = []
|
|
82
216
|
cur_page: int = 1
|
|
83
217
|
while has_next_page and (not page_limit or cur_page < page_limit):
|
|
84
|
-
|
|
85
|
-
page_size =
|
|
86
|
-
page_number =
|
|
87
|
-
has_next_page =
|
|
88
|
-
rows.extend(
|
|
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)
|
|
89
223
|
cur_page += 1
|
|
90
|
-
return
|
|
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
|
|
@@ -46,19 +46,6 @@ class MultiModalManager:
|
|
|
46
46
|
self._user.raise_for_status(response)
|
|
47
47
|
return response.json()
|
|
48
48
|
|
|
49
|
-
def register_interactively(self, request: ChemInteractiveRegisterRequestPojo):
|
|
50
|
-
"""
|
|
51
|
-
Prompt user interactively to load the provided file data.
|
|
52
|
-
User will be able to select or define a field map for the assay data included.
|
|
53
|
-
User will also be able to define which one is the SMILES column if csv.
|
|
54
|
-
"""
|
|
55
|
-
# TODO pending client_callback enablement on the webhook => webservice endpoint route.
|
|
56
|
-
payload = dumps(request, ChemInteractiveRegisterRequestPojo)
|
|
57
|
-
response = self._user.plugin_post("chemistry/register_interactively",
|
|
58
|
-
payload=payload, is_payload_plain_text=True)
|
|
59
|
-
self._user.raise_for_status(response)
|
|
60
|
-
return loads(response.text, ChemCompleteImportPojo)
|
|
61
|
-
|
|
62
49
|
def load_compounds(self, request: CompoundLoadRequestPojo):
|
|
63
50
|
"""
|
|
64
51
|
Load compounds from the provided data here.
|
|
@@ -56,18 +56,6 @@ class PyCompound:
|
|
|
56
56
|
props: dict[str, object] | None
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
class ChemDataType(Enum):
|
|
60
|
-
CompoundPart = 'CompoundPart'
|
|
61
|
-
ChemicalReagentPart = 'ChemicalReagentPart'
|
|
62
|
-
|
|
63
|
-
def __init__(self, data_type_name: str):
|
|
64
|
-
self._data_type_name = data_type_name
|
|
65
|
-
|
|
66
|
-
@property
|
|
67
|
-
def data_type_name(self):
|
|
68
|
-
return self._data_type_name
|
|
69
|
-
|
|
70
|
-
|
|
71
59
|
class ChemFileType(Enum):
|
|
72
60
|
CSV = 0
|
|
73
61
|
SDF = 1
|
|
@@ -133,12 +121,12 @@ class ChemCompleteImportPojo:
|
|
|
133
121
|
|
|
134
122
|
@dataclass
|
|
135
123
|
class ChemInteractiveRegisterRequestPojo:
|
|
136
|
-
dataType:
|
|
124
|
+
dataType: str
|
|
137
125
|
fileType: ChemFileType
|
|
138
126
|
fileDataEncodedBase64: str | None
|
|
139
127
|
addingItems: bool
|
|
140
128
|
|
|
141
|
-
def __init__(self, data_type:
|
|
129
|
+
def __init__(self, data_type: str, file_type: ChemFileType, is_adding_items: bool, file_data: bytes):
|
|
142
130
|
self.dataType = data_type
|
|
143
131
|
self.fileType = file_type
|
|
144
132
|
self.addingItems = is_adding_items
|
|
@@ -156,12 +144,12 @@ class CompoundLoadRequestPojo:
|
|
|
156
144
|
dataList: If the source data is not a file, here you specify a list of string describing molecule for that src.
|
|
157
145
|
fileDataBase64: If the source data is a file, the file's base64 data content.
|
|
158
146
|
"""
|
|
159
|
-
dataType:
|
|
147
|
+
dataType: str
|
|
160
148
|
loadType: ChemLoadType
|
|
161
149
|
dataList: list[str] | None
|
|
162
150
|
fileDataBase64: str | None
|
|
163
151
|
|
|
164
|
-
def __init__(self, data_type:
|
|
152
|
+
def __init__(self, data_type: str, load_type: ChemLoadType, data_list: list[str] | None = None,
|
|
165
153
|
file_data: bytes | None = None):
|
|
166
154
|
self.dataType = data_type
|
|
167
155
|
self.loadType = load_type
|
|
@@ -184,10 +172,10 @@ class ChemRegisterRequestPojo:
|
|
|
184
172
|
dataType: The data type of records to be registered in Sapio.
|
|
185
173
|
registrationList: This list must be of correct data structure suitable for the type. For example, for CompoundPart data type the canonical form must be resolved by earlier call.
|
|
186
174
|
"""
|
|
187
|
-
dataType:
|
|
175
|
+
dataType: str
|
|
188
176
|
registrationList: list[PyCompound]
|
|
189
177
|
|
|
190
|
-
def __init__(self, data_type:
|
|
178
|
+
def __init__(self, data_type: str, registration_list: list[PyCompound]):
|
|
191
179
|
self.dataType = data_type
|
|
192
180
|
self.registrationList = registration_list
|
|
193
181
|
|
|
@@ -228,16 +216,6 @@ class PyIndigoReactionPojo:
|
|
|
228
216
|
reactionRenderRxn: str
|
|
229
217
|
|
|
230
218
|
|
|
231
|
-
class CartridgeMolJoinMethod(Enum):
|
|
232
|
-
"""
|
|
233
|
-
The structure search join method to filter results against Sapio registry.
|
|
234
|
-
Since we have multiple registries in Sapio, you will need to specify which one to join against.
|
|
235
|
-
"""
|
|
236
|
-
COMPOUND_REGISTRY = 0
|
|
237
|
-
REAGENT_REGISTRY = 1
|
|
238
|
-
HELM_STRUCTURE = 2
|
|
239
|
-
|
|
240
|
-
|
|
241
219
|
@dataclass
|
|
242
220
|
class ChemQuickSearchContextData:
|
|
243
221
|
"""
|
|
@@ -250,7 +228,7 @@ class ChemQuickSearchContextData:
|
|
|
250
228
|
nextPageSearchAfter: str | None
|
|
251
229
|
pitId: str | None
|
|
252
230
|
query: str | None
|
|
253
|
-
|
|
231
|
+
joinSapioPartType: str | None
|
|
254
232
|
simUpperLimit: float | None
|
|
255
233
|
|
|
256
234
|
|
|
@@ -269,11 +247,11 @@ class ChemSearchRequestPojo:
|
|
|
269
247
|
"""
|
|
270
248
|
searchStr: str
|
|
271
249
|
searchType: ChemSearchType
|
|
272
|
-
|
|
250
|
+
joinSapioType: str | None
|
|
273
251
|
contextData: ChemQuickSearchContextData | None
|
|
274
252
|
simSearchUpperLimit: float | None
|
|
275
253
|
|
|
276
|
-
def __init__(self, search_str: str, search_type: ChemSearchType, join_method:
|
|
254
|
+
def __init__(self, search_str: str, search_type: ChemSearchType, join_method: str | None = None,
|
|
277
255
|
context_data: ChemQuickSearchContextData | None = None, sim_search_upper: float | None = None):
|
|
278
256
|
self.searchStr = search_str
|
|
279
257
|
self.searchType = search_type
|