sapiopycommons 2024.6.29a275__py3-none-any.whl → 2024.7.3a280__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

@@ -0,0 +1,214 @@
1
+ from sapiopylib.rest.User import SapioUser
2
+ from sapiopylib.rest.pojo.CustomReport import (
3
+ CompositeReportTerm,
4
+ CompositeTermOperation,
5
+ CustomReportCriteria,
6
+ ExplicitJoinDefinition,
7
+ FieldCompareReportTerm,
8
+ RawReportTerm,
9
+ RawTermOperation,
10
+ ReportColumn,
11
+ )
12
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
13
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
14
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
15
+
16
+ from sapiopycommons.general.aliases import SapioRecord
17
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
18
+ from sapiopycommons.recordmodel.record_handler import RecordHandler
19
+
20
+ _NOTEBOOK_ID = "EXPERIMENTID"
21
+ _RECORD_ID = "RECORDID"
22
+
23
+
24
+ # FR-46908 - Provide a utility class that holds experiment related custom reports e.g. getting all the experiments
25
+ # that given records were used in or getting all records of a datatype used in given experiments.
26
+ class ExperimentReportUtil:
27
+ @staticmethod
28
+ def map_records_to_experiment_ids(
29
+ context: SapioWebhookContext | SapioUser,
30
+ records: list[SapioRecord],
31
+ ) -> dict[SapioRecord, list[int]]:
32
+ """
33
+ Return a dictionary mapping each record to a list of ids of experiments that they were used in.
34
+ If a record wasn't used in any experiments then it will be mapped to an empty list.
35
+
36
+ :param context: The current webhook context or a user object to send requests from.
37
+ :param records: a list of records of the same data type.
38
+ :return: a dictionary mapping each record to a list of ids of each experiment it was used in.
39
+ """
40
+ if not records:
41
+ return {}
42
+
43
+ user: SapioUser = context if isinstance(context, SapioUser) else context.user
44
+
45
+ data_type_name = records[0].data_type_name
46
+
47
+ record_ids = [record.record_id for record in records]
48
+
49
+ rows = ExperimentReportUtil.__get_record_experiment_relation_rows(
50
+ user, data_type_name, record_ids=record_ids
51
+ )
52
+
53
+ id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
54
+
55
+ record_to_exps: dict[SapioRecord, set[int]] = {
56
+ record: set() for record in records
57
+ }
58
+
59
+ for row in rows:
60
+ record_id: int = row[_RECORD_ID]
61
+ exp_id: int = row[_NOTEBOOK_ID]
62
+
63
+ record = id_to_record[record_id]
64
+
65
+ record_to_exps[record].add(exp_id)
66
+
67
+ return {record: list(exps) for record, exps in record_to_exps.items()}
68
+
69
+ @staticmethod
70
+ def map_experiments_to_records_of_type(
71
+ context: SapioWebhookContext | SapioUser,
72
+ exp_ids: list[int],
73
+ wrapper_type: type[WrappedType],
74
+ ) -> dict[int, list[WrappedType]]:
75
+ """
76
+ Return a dictionary mapping each experiment id to a list of records of the given type that were used in each experiment.
77
+ If an experiment didn't use any records of the given type then it will be mapped to an empty list.
78
+
79
+ :param context: The current webhook context or a user object to send requests from.
80
+ :param exp_ids: a list of experiment ids. These are specifically the Notebook Experiment ids which can be found in the title of the experiment.
81
+ :param wrapper_type: The record model wrapper to use, corresponds to which data type we will query for.
82
+ :return: a dictionary mapping each experiment id to a list of records of the given type that were used in that experiment.
83
+ """
84
+ if not exp_ids:
85
+ return {}
86
+
87
+ user = context if isinstance(context, SapioUser) else context.user
88
+
89
+ record_handler = RecordHandler(user)
90
+
91
+ data_type_name: str = wrapper_type.get_wrapper_data_type_name()
92
+
93
+ rows = ExperimentReportUtil.__get_record_experiment_relation_rows(
94
+ user, data_type_name, exp_ids=exp_ids
95
+ )
96
+
97
+ record_ids: set[int] = {row[_RECORD_ID] for row in rows}
98
+
99
+ records = record_handler.query_models_by_id(wrapper_type, record_ids)
100
+
101
+ id_to_record: dict[int, WrappedType] = RecordHandler.map_by_id(records)
102
+
103
+ exp_to_records: dict[int, set[SapioRecord]] = {exp: set() for exp in exp_ids}
104
+
105
+ for row in rows:
106
+ record_id: int = row[_RECORD_ID]
107
+ exp_id: int = row[_NOTEBOOK_ID]
108
+
109
+ record = id_to_record[record_id]
110
+
111
+ exp_to_records[exp_id].add(record)
112
+
113
+ return {exp: list(records) for exp, records in exp_to_records.items()}
114
+
115
+ @staticmethod
116
+ def __get_record_experiment_relation_rows(
117
+ user: SapioUser,
118
+ data_type_name: str,
119
+ record_ids: list[int] | None = None,
120
+ exp_ids: list[int] | None = None,
121
+ ) -> list[dict[str, int]]:
122
+ """
123
+ Return a list of dicts mapping \"RECORDID\" to the record id and \"EXPERIMENTID\" to the experiment id.
124
+ At least one of record_ids and exp_ids should be provided.
125
+ """
126
+ assert (record_ids or exp_ids)
127
+
128
+ if record_ids:
129
+ rec_ids = [str(record_id) for record_id in record_ids]
130
+
131
+ ids_str = "{" + ", ".join(rec_ids) + "}"
132
+
133
+ records_term = RawReportTerm(
134
+ data_type_name, "RECORDID", RawTermOperation.EQUAL_TO_OPERATOR, ids_str
135
+ )
136
+
137
+ else:
138
+ # Get all records of the given type
139
+ records_term = RawReportTerm(
140
+ data_type_name,
141
+ "RECORDID",
142
+ RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR,
143
+ "0",
144
+ )
145
+
146
+ if exp_ids:
147
+ exp_ids = [str(exp_id) for exp_id in exp_ids]
148
+
149
+ ids_str = "{" + ", ".join(exp_ids) + "}"
150
+
151
+ exp_term = RawReportTerm(
152
+ "NOTEBOOKEXPERIMENT",
153
+ "EXPERIMENTID",
154
+ RawTermOperation.EQUAL_TO_OPERATOR,
155
+ ids_str,
156
+ )
157
+
158
+ else:
159
+ # Get all experiments
160
+ exp_term = RawReportTerm(
161
+ "NOTEBOOKEXPERIMENT",
162
+ "EXPERIMENTID",
163
+ RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR,
164
+ "0",
165
+ )
166
+
167
+ root_term = CompositeReportTerm(
168
+ records_term, CompositeTermOperation.AND_OPERATOR, exp_term
169
+ )
170
+
171
+ # The columns the resulting dataframe will have
172
+ column_list = [
173
+ ReportColumn(data_type_name, "RECORDID", FieldType.LONG),
174
+ ReportColumn("NOTEBOOKEXPERIMENT", "EXPERIMENTID", FieldType.LONG),
175
+ ]
176
+
177
+ # Join records on the experiment entry records that correspond to them.
178
+ records_entry_join = FieldCompareReportTerm(
179
+ data_type_name,
180
+ "RECORDID",
181
+ RawTermOperation.EQUAL_TO_OPERATOR,
182
+ "EXPERIMENTENTRYRECORD",
183
+ "RECORDID",
184
+ )
185
+
186
+ # Join entry records on the experiment entries they are in.
187
+ experiment_entry_enb_entry_join = FieldCompareReportTerm(
188
+ "EXPERIMENTENTRYRECORD",
189
+ "ENTRYID",
190
+ RawTermOperation.EQUAL_TO_OPERATOR,
191
+ "ENBENTRY",
192
+ "ENTRYID",
193
+ )
194
+
195
+ # Join entries on the experiments they are in.
196
+ enb_entry_experiment_join = FieldCompareReportTerm(
197
+ "ENBENTRY",
198
+ "EXPERIMENTID",
199
+ RawTermOperation.EQUAL_TO_OPERATOR,
200
+ "NOTEBOOKEXPERIMENT",
201
+ "EXPERIMENTID",
202
+ )
203
+
204
+ report_criteria = CustomReportCriteria(
205
+ column_list,
206
+ root_term,
207
+ join_list=[
208
+ ExplicitJoinDefinition("EXPERIMENTENTRYRECORD", records_entry_join),
209
+ ExplicitJoinDefinition("ENBENTRY", experiment_entry_enb_entry_join),
210
+ ExplicitJoinDefinition("NOTEBOOKEXPERIMENT", enb_entry_experiment_join),
211
+ ],
212
+ )
213
+
214
+ return CustomReportUtil.run_custom_report(user, report_criteria)
@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+ from weakref import WeakValueDictionary
6
+
7
+ from sapiopylib.rest.User import SapioUser
8
+
9
+ _STR_JAVA_TYPE = "java.lang.String"
10
+ _INT_JAVA_TYPE = "java.lang.Integer"
11
+ _BOOL_JAVA_TYPE = "java.lang.Boolean"
12
+
13
+
14
+ class AbstractAccessionServiceOperator(ABC):
15
+ """
16
+ Abstract class to define an accession service operator.
17
+ The default one in sapiopycommon only includes the out of box operators.
18
+ More can be added via java plugins for global operators.
19
+ """
20
+
21
+ @property
22
+ @abstractmethod
23
+ def op_class_name(self) -> str:
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def op_param_value_list(self) -> list[Any] | None:
29
+ pass
30
+
31
+ @property
32
+ @abstractmethod
33
+ def op_param_class_name_list(self) -> list[str] | None:
34
+ pass
35
+
36
+ @property
37
+ @abstractmethod
38
+ def default_accessor_name(self) -> str:
39
+ pass
40
+
41
+
42
+ class AccessionWithPrefixSuffix(AbstractAccessionServiceOperator):
43
+ """
44
+ Local operator for accessioning prefix and suffix format.
45
+ """
46
+ _prefix: str | None
47
+ _suffix: str | None
48
+ _num_of_digits: int | None
49
+ _start_num: int
50
+ _strict_mode: bool
51
+
52
+ @property
53
+ def prefix(self):
54
+ return self._prefix
55
+
56
+ @property
57
+ def suffix(self):
58
+ return self._suffix
59
+
60
+ @property
61
+ def num_of_digits(self):
62
+ return self._num_of_digits
63
+
64
+ @property
65
+ def start_num(self):
66
+ return self._start_num
67
+
68
+ @property
69
+ def strict_mode(self):
70
+ return self._strict_mode
71
+
72
+ def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
73
+ start_num: int = 1, strict_mode: bool = False):
74
+ if prefix is None:
75
+ prefix = ""
76
+ if suffix is None:
77
+ suffix = ""
78
+ self._prefix = prefix
79
+ self._suffix = suffix
80
+ self._num_of_digits = num_of_digits
81
+ self._start_num = start_num
82
+ self._strict_mode = strict_mode
83
+
84
+ @property
85
+ def op_param_value_list(self):
86
+ return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
87
+
88
+ @property
89
+ def op_param_class_name_list(self):
90
+ return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
91
+
92
+ @property
93
+ def op_class_name(self):
94
+ return "com.velox.accessionservice.operators.AccessionWithPrefixSuffix"
95
+
96
+ @property
97
+ def default_accessor_name(self):
98
+ return "PREFIX_AND_SUFFIX" + "(" + self.prefix + "," + self.suffix + ")";
99
+
100
+
101
+ class AccessionGlobalPrefixSuffix(AbstractAccessionServiceOperator):
102
+ """
103
+ Global operator for accessioning prefix and suffix format.
104
+ """
105
+ _prefix: str | None
106
+ _suffix: str | None
107
+ _num_of_digits: int | None
108
+ _start_num: int
109
+ _strict_mode: bool
110
+
111
+ @property
112
+ def prefix(self):
113
+ return self._prefix
114
+
115
+ @property
116
+ def suffix(self):
117
+ return self._suffix
118
+
119
+ @property
120
+ def num_of_digits(self):
121
+ return self._num_of_digits
122
+
123
+ @property
124
+ def start_num(self):
125
+ return self._start_num
126
+
127
+ @property
128
+ def strict_mode(self):
129
+ return self._strict_mode
130
+
131
+ def __init__(self, prefix: str | None, suffix: str | None, num_of_digits: int | None = None,
132
+ start_num: int = 1, strict_mode: bool = False):
133
+ if prefix is None:
134
+ prefix = ""
135
+ if suffix is None:
136
+ suffix = ""
137
+ self._prefix = prefix
138
+ self._suffix = suffix
139
+ self._num_of_digits = num_of_digits
140
+ self._start_num = start_num
141
+ self._strict_mode = strict_mode
142
+
143
+ @property
144
+ def op_param_value_list(self):
145
+ return [self._prefix, self._suffix, self._num_of_digits, self._start_num, self._strict_mode]
146
+
147
+ @property
148
+ def op_param_class_name_list(self):
149
+ return [_STR_JAVA_TYPE, _STR_JAVA_TYPE, _INT_JAVA_TYPE, _INT_JAVA_TYPE, _BOOL_JAVA_TYPE]
150
+
151
+ @property
152
+ def op_class_name(self):
153
+ return "com.velox.accessionservice.operators.sapio.AccessionGlobalPrefixSuffix"
154
+
155
+ @property
156
+ def default_accessor_name(self):
157
+ return "PREFIX_AND_SUFFIX" + "(" + self._prefix + "," + self._suffix + ")"
158
+
159
+
160
+ class AccessionNextBarcode(AbstractAccessionServiceOperator):
161
+ """
162
+ From Java description:
163
+ This will start accessioning at the getNextBarcode() when there's no system preference to be backward compatible.
164
+ However, once it completes setting the first ID, it will start increment by its own preference and disregards getNextBarcode().
165
+
166
+ Recommend using AccessionServiceBasicManager to accession next barcode.
167
+ To avoid ambiguity in preference cache.
168
+
169
+ This should not be used unless we are using something legacy such as plate mapping template record creation
170
+ (Note: not 3D plating, I'm talking about the older aliquoter).
171
+ """
172
+
173
+ @property
174
+ def op_param_value_list(self):
175
+ return []
176
+
177
+ @property
178
+ def op_param_class_name_list(self):
179
+ return []
180
+
181
+ @property
182
+ def op_class_name(self):
183
+ return "com.velox.accessionservice.operators.sapio.AccessionNextBarcode"
184
+
185
+ @property
186
+ def default_accessor_name(self):
187
+ return "Barcode"
188
+
189
+
190
+ class AccessionRequestId(AbstractAccessionServiceOperator):
191
+ """
192
+ This class implements the accessioning operator for com.velox.sapioutils.shared.managers.DataRecordUtilManager.getNextRequestId()
193
+ and getNextRequestId(int numberOfCharacters).
194
+
195
+ Operation: For 4 characters start with A001, increment by 1 until A999. Then We use B001.
196
+ After Z999 we start with AA01 until we get to AA99, etc.
197
+
198
+ Exception: Skips I and O to prevent confusions with 1 and 0 when incrementing letters.
199
+
200
+ Properties:
201
+ numberOfCharacters: Number of characters maximum in the request ID.
202
+ accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compability patch for converting these to the new preference format.
203
+ """
204
+ _num_of_characters: int
205
+ _accessor_name: str
206
+
207
+ @property
208
+ def num_of_characters(self):
209
+ return self._num_of_characters
210
+
211
+ @property
212
+ def accessor_name(self):
213
+ return self._accessor_name
214
+
215
+ def __init__(self, num_of_characters: int = 4, accessor_name: str = None):
216
+ self._num_of_characters = num_of_characters
217
+ if not accessor_name:
218
+ accessor_name = self.default_accessor_name
219
+ self._accessor_name = accessor_name
220
+
221
+ @property
222
+ def op_class_name(self):
223
+ return "com.velox.accessionservice.operators.sapio.AccessionRequestId"
224
+
225
+ @property
226
+ def op_param_value_list(self):
227
+ return [self._num_of_characters, self._accessor_name]
228
+
229
+ @property
230
+ def op_param_class_name_list(self):
231
+ return [_INT_JAVA_TYPE, _STR_JAVA_TYPE]
232
+
233
+ @property
234
+ def default_accessor_name(self):
235
+ return "SapioNextRequestIdMap"
236
+
237
+
238
+ class AccessionServiceDescriptor:
239
+ """
240
+ Describes a single accession service's accessioning request
241
+
242
+ Attributes:
243
+ opClassName: The accession service operator class name as in Java
244
+ opParamValueList: Ordered list of parameter values to construct the accession service operator.
245
+ opParamClassNameList: Ordered list of FQCN of java classes in order of parameter value list.
246
+ dataTypeName: The data type to accession. Should be blank if opClassName resolves to a global operator.
247
+ dataFieldName: The data field to accession. Should be blank if opClassName resolves to a global operator.
248
+ accessorName: The accessor cache name to be used for accessioning.
249
+ numIds: The number of IDs to accession.
250
+ """
251
+ op: AbstractAccessionServiceOperator
252
+ dataTypeName: str | None
253
+ dataFieldName: str | None
254
+ accessorName: str
255
+ numIds: int
256
+
257
+ def __init__(self, accessor_name: str, op: AbstractAccessionServiceOperator, num_ids: int,
258
+ data_type_name: str | None, data_field_name: str | None):
259
+ self.accessorName = accessor_name
260
+ self.op = op
261
+ self.dataTypeName = data_type_name
262
+ self.dataFieldName = data_field_name
263
+ self.numIds = num_ids
264
+
265
+ def to_json(self):
266
+ return {
267
+ "opClassName": self.op.op_class_name,
268
+ "opParamValueList": self.op.op_param_value_list,
269
+ "opParamClassNameList": self.op.op_param_class_name_list,
270
+ "accessorName": self.accessorName,
271
+ "numIds": self.numIds,
272
+ "dataTypeName": self.dataTypeName,
273
+ "dataFieldName": self.dataFieldName
274
+ }
275
+
276
+
277
+ class AccessionService:
278
+ """
279
+ Provides Sapio Foundations Accession Service functionalities.
280
+ """
281
+ _user: SapioUser
282
+
283
+ __instances: WeakValueDictionary[SapioUser, AccessionService] = WeakValueDictionary()
284
+ __initialized: bool
285
+
286
+ @property
287
+ def user(self) -> SapioUser:
288
+ return self._user
289
+
290
+ def __new__(cls, user: SapioUser):
291
+ """
292
+ Observes singleton pattern per record model manager object.
293
+
294
+ :param user: The user that will make the webservice request to the application.
295
+ """
296
+ obj = cls.__instances.get(user)
297
+ if not obj:
298
+ obj = object.__new__(cls)
299
+ obj.__initialized = False
300
+ cls.__instances[user] = obj
301
+ return obj
302
+
303
+ def __init__(self, user: SapioUser):
304
+ if self.__initialized:
305
+ return
306
+ self._user = user
307
+ self.__initialized = True
308
+
309
+ def accession_with_config(self, data_type_name: str, data_field_name: str, num_ids: int) -> list[str]:
310
+ """
311
+ Accession with Configuration Manager => Accession Service configuration (This is not visible to regular users in SaaS)
312
+ """
313
+ payload = {
314
+ "dataTypeName": data_type_name,
315
+ "dataFieldName": data_field_name,
316
+ "numIds": num_ids
317
+ }
318
+ response = self.user.plugin_post("accessionservice/accession_with_config", payload=payload)
319
+ self.user.raise_for_status(response)
320
+ return list(response.json())
321
+
322
+ def accession_in_batch(self, descriptor: AccessionServiceDescriptor) -> list[str]:
323
+ """
324
+ This is the most flexible way to make use of accession service: directly via a descriptor object.
325
+ """
326
+ payload = descriptor.to_json()
327
+ response = self.user.plugin_post("accessionservice/accession", payload=payload)
328
+ self.user.raise_for_status(response)
329
+ return list(response.json())
330
+
331
+ def accession_next_request_id_list(self, num_of_characters: int, num_ids: int) -> list[str]:
332
+ """
333
+ Accession Request ID by old LIMS format. This is usually deprecated today.
334
+ :param num_of_characters: Number of characters minimum in request ID.
335
+ :param num_ids: Number of request IDs to accession.
336
+ """
337
+ op = AccessionRequestId(num_of_characters)
338
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
339
+ return self.accession_in_batch(descriptor)
340
+
341
+ def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
342
+ suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
343
+ """
344
+ Get the batch affixed IDs that are maximal in cache and contiguious for a particular datatype.datafield under a given format.
345
+ :param data_type_name: The datatype name to look for max ID
346
+ :param data_field_name: The datafield name to look for max ID
347
+ :param num_ids: The number of IDs to accession.
348
+ :param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
349
+ :param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
350
+ :param num_digits: None if unlimited with no leading zeros.
351
+ :param start_num The number to begin accessioning if this is the first time.
352
+ :return:
353
+ """
354
+ op = AccessionWithPrefixSuffix(prefix, suffix, num_digits, start_num)
355
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, data_type_name, data_field_name)
356
+ return self.accession_in_batch(descriptor)
357
+
358
+ def get_global_affixed_id_in_batch(
359
+ self, num_ids: int, prefix: str | None, suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
360
+ """
361
+ Get the next numOfIds affixed IDs using system preference cache that's maximum across all datatype and datafields and maximal for the format.
362
+ This method allows users to customize a start number instead of always starting at 1.
363
+ :param num_ids: The number of IDs to accession.
364
+ :param prefix: leave it empty string "" if no prefix. Otherwise, specifies the prefix of ID.
365
+ :param suffix: leave it empty string "" if no suffix. Otherwise, specifies the suffix of ID.
366
+ :param num_digits: None if unlimited with no leading zeros.
367
+ :param start_num The number to begin accessioning if this is the first time.
368
+ """
369
+ op: AbstractAccessionServiceOperator
370
+ if not prefix and not suffix:
371
+ op = AccessionNextBarcode()
372
+ else:
373
+ op = AccessionGlobalPrefixSuffix(prefix, suffix, num_digits, start_num)
374
+ descriptor = AccessionServiceDescriptor(op.default_accessor_name, op, num_ids, None, None)
375
+ return self.accession_in_batch(descriptor)
@@ -1,11 +1,13 @@
1
1
  # Multimodal registration client
2
2
  from __future__ import annotations
3
3
 
4
+ import io
4
5
  from weakref import WeakValueDictionary
5
6
 
6
7
  from databind.json import dumps, loads
7
8
  from sapiopylib.rest.User import SapioUser
8
9
 
10
+ from sapiopycommons.general.exceptions import SapioException
9
11
  from sapiopycommons.multimodal.multimodal_data import *
10
12
 
11
13
 
@@ -103,7 +105,7 @@ class MultiModalManager:
103
105
  self._user.raise_for_status(response)
104
106
  return loads(response.text, ChemSearchResponsePojo)
105
107
 
106
- def run_multi_sequence_alignment(self, request: MultiSequenceAlignmentRequestPojo):
108
+ def run_multi_sequence_alignment(self, request: MultiSequenceAlignmentRequestPojo) -> list[MultiSequenceAlignmentSeqPojo]:
107
109
  """
108
110
  Run a multi-sequence alignment using the specified tool and strategy.
109
111
  :param request: The request object containing the sequences and alignment parameters. The parameters inside it can be the pojo dict of one of the options.
@@ -115,7 +117,7 @@ class MultiModalManager:
115
117
  self._user.raise_for_status(response)
116
118
  return loads(response.text, list[MultiSequenceAlignmentSeqPojo])
117
119
 
118
- def register_bio(self, request: BioFileRegistrationRequest):
120
+ def register_bio(self, request: BioFileRegistrationRequest) -> BioFileRegistrationResponse:
119
121
  """
120
122
  Register to bioregistry of a file.
121
123
  """
@@ -124,3 +126,21 @@ class MultiModalManager:
124
126
  self._user.raise_for_status(response)
125
127
  return loads(response.text, BioFileRegistrationResponse)
126
128
 
129
+ def export_to_sdf(self, request: ChemExportSDFRequest) -> str:
130
+ """
131
+ Export the SDF files
132
+ :param request: The request for exporting SDF file.
133
+ :return: the SDF plain text data.
134
+ """
135
+ payload = dumps(request, ChemExportSDFRequest)
136
+ response = self._user.plugin_post("chemistry/export_sdf", payload=payload, is_payload_plain_text=True)
137
+ self._user.raise_for_status(response)
138
+ gzip_base64: str = response.text
139
+ if not gzip_base64:
140
+ raise SapioException("Returning data from server is blank for export SDF.")
141
+ decoded_bytes = base64.b64decode(gzip_base64)
142
+ with io.BytesIO(decoded_bytes) as bytes_io:
143
+ import gzip
144
+ with gzip.GzipFile(fileobj=bytes_io, mode='rb') as f:
145
+ ret: str = f.read().decode()
146
+ return ret
@@ -462,3 +462,26 @@ class BioFileRegistrationResponse:
462
462
  newRecordIdList: list[int]
463
463
  oldRecordIdList: list[int]
464
464
 
465
+
466
+ @dataclass
467
+ class ChemExportSDFRequest:
468
+ """
469
+ A request to export SDF data from Sapio to Python REST client.
470
+ """
471
+ partDataTypeName: str
472
+ partRecordIdList: list[int]
473
+
474
+ forceV3000: bool
475
+ fieldNameList: list[str] | None
476
+ assayNameList: list[str] | None
477
+ additionalPropertiesByRecordId: dict[int, dict[str, Any]] | None
478
+
479
+ def __init__(self, partDataTypeName: str, partRecordIdList: list[int], forceV3000: bool = True,
480
+ fieldNameList: list[str] | None = None, assayNameList: list[str] | None = None,
481
+ additionalPropertiesByRecordId: dict[int, dict[str, Any]] | None = None):
482
+ self.partDataTypeName = partDataTypeName
483
+ self.partRecordIdList = partRecordIdList
484
+ self.forceV3000 = forceV3000
485
+ self.fieldNameList = fieldNameList
486
+ self.assayNameList = assayNameList
487
+ self.additionalPropertiesByRecordId = additionalPropertiesByRecordId
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.6.29a275
3
+ Version: 2024.7.3a280
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -8,6 +8,7 @@ sapiopycommons/datatype/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
8
8
  sapiopycommons/datatype/attachment_util.py,sha256=YlnMprj5IGBbAZDLG2khS1P7JIYTw_NYfpJAfRZfP3M,3219
9
9
  sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  sapiopycommons/eln/experiment_handler.py,sha256=v1pG4qtZb8OSNWfKtFo6NjnEkReqnu5R9i_hqWh_xxg,57198
11
+ sapiopycommons/eln/experiment_report_util.py,sha256=FTLw-6SLAMeoWTOO-qhGROE9g54pZdyoQJIhiIzlwGw,7848
11
12
  sapiopycommons/eln/plate_designer.py,sha256=FYJfhhNq8hdfuXgDYOYHy6g0m2zNwQXZWF_MTPzElDg,7184
12
13
  sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
14
  sapiopycommons/files/complex_data_loader.py,sha256=XSJOl676mIklJo78v07-70u1b015a5DI4sqZPI3C-Tw,1475
@@ -18,14 +19,15 @@ sapiopycommons/files/file_util.py,sha256=44mzhn3M_QltoncBB-ooX7_yO6u5k-XU_bzUXHG
18
19
  sapiopycommons/files/file_validator.py,sha256=BhXB2XnoNEzdBXuwul1s2RNoj-3ZoiMmephUCU_0o3Y,28113
19
20
  sapiopycommons/files/file_writer.py,sha256=5u_iZXTQvuUU7ceHZr8Q001_tvgJhOqBwAnB_pxcAbQ,16027
20
21
  sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ sapiopycommons/general/accession_service.py,sha256=HYgyOsH_UaoRnoury-c2yTW8SeG4OtjLemdpCzoV4R8,13484
21
23
  sapiopycommons/general/aliases.py,sha256=i6af5o2oVFGNcyk7GkvTWXQs0H9xTbFKc_GIah8NKVU,3594
22
24
  sapiopycommons/general/custom_report_util.py,sha256=cLgIR5Fn3M9uyAtgfTYRv3JRk2SKNevnsb_R5zidSYs,15557
23
25
  sapiopycommons/general/exceptions.py,sha256=DOlLKnpCatxQF-lVCToa8ryJgusWLvip6N_1ALN00QE,1679
24
26
  sapiopycommons/general/popup_util.py,sha256=-mN5IgYPrLrOEHJ4CHPi2rec4_WAN6X0yMxHwD5h3Bs,30126
25
27
  sapiopycommons/general/storage_util.py,sha256=ovmK_jN7v09BoX07XxwShpBUC5WYQOM7dbKV_VeLXJU,8892
26
28
  sapiopycommons/general/time_util.py,sha256=jiJUh7jc1ZRCOem880S3HaLPZ4RboBtSl4_U9sqAQuM,7290
27
- sapiopycommons/multimodal/multimodal.py,sha256=w9_M3EYH_cwIbBS0abl-Y0Os-PMR633nYQLkt8UP-Nk,5724
28
- sapiopycommons/multimodal/multimodal_data.py,sha256=Lz34CkmDapVi2TiNIiG-d0CdSEY9Z80r0dBOTi0Ev3Q,14107
29
+ sapiopycommons/multimodal/multimodal.py,sha256=A1QsC8QTPmgZyPr7KtMbPRedn2Ie4WIErodUvQ9otgU,6724
30
+ sapiopycommons/multimodal/multimodal_data.py,sha256=zqgYHO-ULaPKV0POFWZVY9N-Sfm1RQWwdsfwFxe5DjI,15038
29
31
  sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
32
  sapiopycommons/processtracking/endpoints.py,sha256=g5h_uCVByqacYm9zWAz8TyAdRsGfaO2o0b5RSJdOaSA,10926
31
33
  sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -35,7 +37,7 @@ sapiopycommons/rules/eln_rule_handler.py,sha256=qfkBZtck0KK1i9s9Xe2UZqkzQOgPCzDx
35
37
  sapiopycommons/rules/on_save_rule_handler.py,sha256=JY9F30IcHwFVdgPAMQtTYuRastV1jeezhVktyrzNASU,10763
36
38
  sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  sapiopycommons/webhook/webhook_handlers.py,sha256=ibpBY3Sk3Eij919bIdW0awzlogYoQSWYDDOg--NwsQE,13431
38
- sapiopycommons-2024.6.29a275.dist-info/METADATA,sha256=aD2Pj7SdHF81Anbi9uVLMOh-KJAE4_Sa5rZK2ATedOA,3176
39
- sapiopycommons-2024.6.29a275.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
40
- sapiopycommons-2024.6.29a275.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
41
- sapiopycommons-2024.6.29a275.dist-info/RECORD,,
40
+ sapiopycommons-2024.7.3a280.dist-info/METADATA,sha256=bdT3mlji7J-b8ayWJrDCel9qHdQUA2qAx3oO4xMFq5k,3175
41
+ sapiopycommons-2024.7.3a280.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
42
+ sapiopycommons-2024.7.3a280.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
43
+ sapiopycommons-2024.7.3a280.dist-info/RECORD,,