sapiopycommons 2024.3.18a156__py3-none-any.whl → 2025.1.17a402__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.

Files changed (52) hide show
  1. sapiopycommons/callbacks/__init__.py +0 -0
  2. sapiopycommons/callbacks/callback_util.py +2041 -0
  3. sapiopycommons/callbacks/field_builder.py +545 -0
  4. sapiopycommons/chem/IndigoMolecules.py +52 -5
  5. sapiopycommons/chem/Molecules.py +114 -30
  6. sapiopycommons/customreport/__init__.py +0 -0
  7. sapiopycommons/customreport/column_builder.py +60 -0
  8. sapiopycommons/customreport/custom_report_builder.py +137 -0
  9. sapiopycommons/customreport/term_builder.py +315 -0
  10. sapiopycommons/datatype/attachment_util.py +17 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +390 -90
  14. sapiopycommons/eln/experiment_report_util.py +649 -0
  15. sapiopycommons/eln/plate_designer.py +152 -0
  16. sapiopycommons/files/complex_data_loader.py +31 -0
  17. sapiopycommons/files/file_bridge.py +153 -25
  18. sapiopycommons/files/file_bridge_handler.py +555 -0
  19. sapiopycommons/files/file_data_handler.py +633 -0
  20. sapiopycommons/files/file_util.py +270 -158
  21. sapiopycommons/files/file_validator.py +569 -0
  22. sapiopycommons/files/file_writer.py +377 -0
  23. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  24. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  25. sapiopycommons/general/accession_service.py +375 -0
  26. sapiopycommons/general/aliases.py +259 -18
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +252 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +85 -18
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +97 -7
  35. sapiopycommons/multimodal/multimodal.py +146 -0
  36. sapiopycommons/multimodal/multimodal_data.py +490 -0
  37. sapiopycommons/processtracking/__init__.py +0 -0
  38. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  39. sapiopycommons/processtracking/endpoints.py +192 -0
  40. sapiopycommons/recordmodel/record_handler.py +653 -149
  41. sapiopycommons/rules/eln_rule_handler.py +89 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +89 -12
  43. sapiopycommons/sftpconnect/__init__.py +0 -0
  44. sapiopycommons/sftpconnect/sftp_builder.py +70 -0
  45. sapiopycommons/webhook/webhook_context.py +39 -0
  46. sapiopycommons/webhook/webhook_handlers.py +617 -69
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
  49. sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
  50. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,148 @@
1
+ class StorageUtil:
2
+ """
3
+ A collection of utilities intended for converting to and from various forms of representing positions on a storage
4
+ unit, such as character + integer (e.g. A1), two integers (e.g. (0, 0)), or a single integer index (e.g. 1).
5
+ Integers are also sometimes zero-indexed and sometimes one-indexed, so both are supported.
6
+ """
7
+ @staticmethod
8
+ def map_index_to_coordinate(index: int, size: int, fill_by_row: bool = True, zero_indexed_input: bool = False,
9
+ zero_indexed_output: bool = False, char_row_output: bool = True) \
10
+ -> tuple[int | str, int]:
11
+ """
12
+ Convert an index representing a position on a plate/storage unit to a coordinate pair on that plate/storage
13
+ unit, able to be used in row/column storage fields on records. This will output a value for any input index;
14
+ it is up to the caller to determine if the output will actually fit on the plate/storage unit.
15
+ (The index should be within the range [1 < index < rows * columns] for one-indexed values.)
16
+
17
+ By default, expects the input to be a one-indexed integer filled row-by-row with the output being a
18
+ character, integer pair where the output integer is one-indexed.
19
+
20
+ :param index: The index to map to a coordinate position.
21
+ :param size: The number of columns or rows in the plate/storage unit, depending on whether you are filling by
22
+ row or by column.
23
+ :param fill_by_row: If true, map positions row-by-row (A1, A2, A3... B1, B2...) and use the above size as the
24
+ number of columns in the plate/storage unit. If false, map positions column-by-column (A1, B1, C1... A2,
25
+ B2...) and use the above size as the number of rows.
26
+ :param zero_indexed_input: If true, the input index is zero-indexed. If false, then they are one-indexed.
27
+ This does not influence the output, only the function's understanding of the input.
28
+ :param zero_indexed_output: If true, the output index is zero-indexed. If false, then it is one-indexed.
29
+ Has no effect on the column output if the column is set to output as a character.
30
+ :param char_row_output: If true, the output row value is converted to a character where 0 = A, 1 = B, 25 = Z,
31
+ 26 = AA, 27 = AB, etc. If false, then it is returned as an integer.
32
+ :return: A tuple representing a coordinate pair (row value, column value). The row value may be either an
33
+ integer or a string, while the column value is always an integer, influenced by the input parameters.
34
+ """
35
+ # If the given index isn't zero-indexed, then make it zero-indexed by subtracting one.
36
+ if not zero_indexed_input:
37
+ index -= 1
38
+
39
+ row: int = index // size
40
+ col: int = index % size
41
+
42
+ # If fill by row is false, then the above calculations are flipped,
43
+ # meaning the row is actually the column and vice versa.
44
+ if not fill_by_row:
45
+ temp = row
46
+ row = col
47
+ col = temp
48
+ # The column and row are zero-indexed by default. If it should be one-indexed, add one.
49
+ if not zero_indexed_output:
50
+ col += 1
51
+ # Only add one to the row if it won't be converted to a character.
52
+ if not char_row_output:
53
+ row += 1
54
+ return StorageUtil.map_index_to_char(row, True) if char_row_output else row, col
55
+
56
+ @staticmethod
57
+ def map_coordinate_to_index(row: int | str, col: int | str, size: int, fill_by_row: bool = True,
58
+ zero_indexed_input: bool = False, zero_indexed_output: bool = False) -> int:
59
+ """
60
+ Map row and column coordinates on a plate/storage unit to the index of that position.
61
+
62
+ By default, expects the input to be provided as a character, integer pair and outputs a single row-by-row
63
+ one-indexed integer.
64
+
65
+ :param row: The row coordinate of the position as a string of characters from A-Z or a zero-indexed integer.
66
+ :param col: The column coordinate of the position as an integer which may be zero-indexed or one-indexed.
67
+ (This integer may be in string form, such as is the case with column fields on storable records.)
68
+ :param size: The number of columns or rows in the plate/storage unit, depending on whether you are filling by
69
+ row or by column.
70
+ :param fill_by_row: If true, map positions row-by-row (A1, A2, A3... B1, B2...) and use the above size as the
71
+ number of columns in the plate/storage unit. If false, map positions column-by-column (A1, B1, C1... A2,
72
+ B2...) and use the above size as the number of rows.
73
+ :param zero_indexed_input: If true, the input coordinates for the row and column is zero-indexed. If false,
74
+ then they are one-indexed. This does not influence the output, only the function's understanding of the
75
+ input. This also has no effect if the input row is a string.
76
+ :param zero_indexed_output: If true, the output index is zero-indexed. If false, then it is one-indexed.
77
+ :return: The index of the storage position at the input row and column.
78
+ """
79
+ # If the column was provided as a string, cast it to an int.
80
+ if isinstance(col, str):
81
+ col: int = int(col)
82
+ # If the input isn't zero-indexed, then make it zero-indexed.
83
+ if not zero_indexed_input:
84
+ col -= 1
85
+ # Only subtract from the row if it's already in integer form.
86
+ # If it's a string, it'll be converted to a zero-indexed integer.
87
+ if isinstance(row, int):
88
+ row -= 1
89
+ # If the input row is a string, convert it to a zero-indexed integer.
90
+ if isinstance(row, str):
91
+ row: int = StorageUtil.map_char_to_index(row, True)
92
+
93
+ # Convert the row and column indices to a singular index across the entire storage unit.
94
+ index: int = row * size + col if fill_by_row else col * size + row
95
+
96
+ # The index is zero-indexed by default. If it should be one-indexed, add one.
97
+ if not zero_indexed_output:
98
+ index += 1
99
+ return index
100
+
101
+ @staticmethod
102
+ def map_index_to_char(index: int, zero_indexed_input: bool = False) -> str:
103
+ """
104
+ Map a given base-10 integer to a base-26 value where 0 = A, 1 = B, 25 = Z, 26 = AA, 27 = AB, etc.
105
+ Useful for mapping the index of a row to the character(s) representing that row in a storage unit.
106
+ May also be used for mapping the index to an Excel sheet's columns.
107
+
108
+ By default, expects the input as a one-indexed value.
109
+
110
+ :param index: The index to map to a character.
111
+ :param zero_indexed_input: If true, the input index is zero-indexed. If false, then they are one-indexed.
112
+ This does not influence the output, only the function's understanding of the input.
113
+ :return: The input integer mapped to a string representing that integer's position.
114
+ """
115
+ # If the given index isn't zero-indexed, then make it zero-indexed by subtracting one.
116
+ if not zero_indexed_input:
117
+ index -= 1
118
+ chars: str = ""
119
+ while index >= 0:
120
+ # Add new characters to the front of the string.
121
+ chars = chr(ord("A") + index % 26) + chars
122
+ # Reduce the index by the amount accounted for by the character that was just added.
123
+ index = index // 26 - 1
124
+ return chars
125
+
126
+ @staticmethod
127
+ def map_char_to_index(chars: str, zero_indexed_output: bool = False) -> int:
128
+ """
129
+ Map a given base-26 value of characters to a base-10 integer where A = 0, B = 1, Z = 25, AA = 26, AB = 27, etc.
130
+ Useful for mapping the character(s) representing a row in a storage unit to that row's index.
131
+ May also be used for mapping the index of an Excel sheet's columns.
132
+
133
+ By default, provides the output as a one-indexed value.
134
+
135
+ :param chars: A string of characters to be converted to an index. Characters are expected to be uppercase
136
+ characters in the range A to Z.
137
+ :param zero_indexed_output: If true, the output index is zero-indexed. If false, then it is one-indexed.
138
+ :return: The input character(s) converted to an index.
139
+ """
140
+ # Reverse iterate over the characters of the string and determine the value of each individual character.
141
+ # The value is multiplied by the base of the character given its digit position (26^0, 26^1, etc.)
142
+ value: int = 0
143
+ for i, c in enumerate(reversed(chars)):
144
+ value += (ord(c) - ord("A") + 1) * (26 ** i)
145
+ # The character value is one-indexed by default. If it should be zero-indexed, subtract one.
146
+ if zero_indexed_output:
147
+ value -= 1
148
+ return value
@@ -1,8 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  from datetime import datetime
5
+ from typing import Any
3
6
 
4
7
  import pytz
5
8
 
9
+ from sapiopycommons.general.exceptions import SapioException
10
+
6
11
  __timezone = None
7
12
  """The default timezone. Use TimeUtil.set_default_timezone in a global context before making use of TimeUtil."""
8
13
 
@@ -24,7 +29,7 @@ class TimeUtil:
24
29
  with static date fields, use "UTC" as your input timezone.
25
30
  """
26
31
  @staticmethod
27
- def get_default_timezone():
32
+ def get_default_timezone() -> Any:
28
33
  """
29
34
  Returns the timezone that TimeUtil is currently using as its default.
30
35
  """
@@ -35,6 +40,7 @@ class TimeUtil:
35
40
  def set_default_timezone(new_timezone: str | int) -> None:
36
41
  """
37
42
  Set the timezone used by TimeUtil to something new.
43
+
38
44
  :param new_timezone: The timezone to set the default to. A list of valid timezones can be found at
39
45
  https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
40
46
  """
@@ -42,7 +48,7 @@ class TimeUtil:
42
48
  __timezone = TimeUtil.__to_tz(new_timezone)
43
49
 
44
50
  @staticmethod
45
- def __to_tz(timezone: str | int = None):
51
+ def __to_tz(timezone: str | int = None) -> Any:
46
52
  """
47
53
  :param timezone: Either the name of a timezone, a UTC offset in seconds, or None if the default should be used.
48
54
  :return: The timezone object to use for the given input. If the input is None, uses the default timezone.
@@ -52,16 +58,34 @@ class TimeUtil:
52
58
  # because pytz may return timezones from strings in Local Mean Time instead of a timezone with a UTC offset.
53
59
  # LMT may be a few minutes off of the actual time in that timezone right now.
54
60
  # https://stackoverflow.com/questions/35462876
55
- offset: int = int(datetime.now(pytz.timezone(timezone)).utcoffset().total_seconds() / 60)
56
- return pytz.FixedOffset(offset)
61
+ offset: int = TimeUtil.__get_timezone_offset(timezone)
62
+ # This function takes an offset in minutes, so divide the provided offset seconds by 60.
63
+ return pytz.FixedOffset(offset // 60)
57
64
  if isinstance(timezone, int):
58
65
  return pytz.FixedOffset(timezone // 60)
59
- return TimeUtil.get_default_timezone()
66
+ if timezone is None:
67
+ return TimeUtil.get_default_timezone()
68
+ raise SapioException(f"Unhandled timezone object of type {type(timezone)}: {timezone}")
69
+
70
+ @staticmethod
71
+ def __get_timezone_offset(timezone: str | int | None) -> int:
72
+ """
73
+ :param timezone: Either the name of a timezone, a UTC offset in seconds, or None if the default should be used.
74
+ :return: The UTC offset in seconds of the provided timezone.
75
+ """
76
+ if isinstance(timezone, int):
77
+ return timezone
78
+ if isinstance(timezone, str):
79
+ timezone = pytz.timezone(timezone)
80
+ if timezone is None:
81
+ timezone = TimeUtil.get_default_timezone()
82
+ return int(datetime.now(timezone).utcoffset().total_seconds())
60
83
 
61
84
  @staticmethod
62
85
  def current_time(timezone: str | int = None) -> datetime:
63
86
  """
64
87
  The current time as a datetime object.
88
+
65
89
  :param timezone: The timezone to initialize the current time with. If no timezone is provided, uses the global
66
90
  timezone variable set by the TimeUtil. A list of valid timezones can be found at
67
91
  https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
@@ -80,6 +104,7 @@ class TimeUtil:
80
104
  def now_in_format(time_format: str, timezone: str | int = None) -> str:
81
105
  """
82
106
  The current time in some date format.
107
+
83
108
  :param time_format: The format to display the current time in. Documentation for how the time formatting works
84
109
  can be found at https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior
85
110
  :param timezone: The timezone to initialize the current time with. If no timezone is provided, uses the global
@@ -89,9 +114,11 @@ class TimeUtil:
89
114
  return TimeUtil.current_time(timezone).strftime(time_format)
90
115
 
91
116
  @staticmethod
92
- def millis_to_format(millis: int, time_format: str, timezone: str | int = None) -> str:
117
+ def millis_to_format(millis: int, time_format: str, timezone: str | int = None) -> str | None:
93
118
  """
94
- Convert the input time in milliseconds to the provided format.
119
+ Convert the input time in milliseconds to the provided format. If None is passed to the millis parameter,
120
+ None will be returned
121
+
95
122
  :param millis: The time in milliseconds to convert from.
96
123
  :param time_format: The format to display the input time in. Documentation for how the time formatting works
97
124
  can be found at https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior
@@ -99,6 +126,9 @@ class TimeUtil:
99
126
  timezone variable set by the TimeUtil. A list of valid timezones can be found at
100
127
  https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
101
128
  """
129
+ if millis is None:
130
+ return None
131
+
102
132
  tz = TimeUtil.__to_tz(timezone)
103
133
  return datetime.fromtimestamp(millis / 1000, tz).strftime(time_format)
104
134
 
@@ -106,6 +136,7 @@ class TimeUtil:
106
136
  def format_to_millis(time_point: str, time_format: str, timezone: str | int = None) -> int:
107
137
  """
108
138
  Convert the input time from the provided format to milliseconds.
139
+
109
140
  :param time_point: The time in some date/time format to convert from.
110
141
  :param time_format: The format that the time_point is in. Documentation for how the time formatting works
111
142
  can be found at https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior
@@ -116,11 +147,70 @@ class TimeUtil:
116
147
  tz = TimeUtil.__to_tz(timezone)
117
148
  return int(datetime.strptime(time_point, time_format).replace(tzinfo=tz).timestamp() * 1000)
118
149
 
150
+ # FR-47296: Provide functions for shifting between timezones.
151
+ @staticmethod
152
+ def shift_now(to_timezone: str = "UTC", from_timezone: str | None = None) -> int:
153
+ """
154
+ Take the current time in from_timezone and output the epoch timestamp that would display that same time in
155
+ to_timezone. A use case for this is when dealing with static date fields to convert a provided timestamp to the
156
+ value necessary to display that timestamp in the same way when viewed in the static date field.
157
+
158
+ :param to_timezone: The timezone to shift to. If not provided, uses UTC.
159
+ :param from_timezone: The timezone to shift from. If no timezone is provided, uses the global
160
+ timezone variable set by the TimeUtil. A list of valid timezones can be found at
161
+ https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
162
+ :return: The epoch timestamp that would display as the same time in to_timezone as the current time in
163
+ from_timezone.
164
+ """
165
+ millis: int = TimeUtil.now_in_millis()
166
+ return TimeUtil.shift_millis(millis, to_timezone, from_timezone)
167
+
168
+ @staticmethod
169
+ def shift_millis(millis: int, to_timezone: str = "UTC", from_timezone: str | None = None) -> int:
170
+ """
171
+ Take a number of milliseconds for a time in from_timezone and output the epoch timestamp that would display that
172
+ same time in to_timezone. A use case for this is when dealing with static date fields to convert a provided
173
+ timestamp to the value necessary to display that timestamp in the same way when viewed in the static date field.
174
+
175
+ :param millis: The time in milliseconds to convert from.
176
+ :param to_timezone: The timezone to shift to. If not provided, uses UTC.
177
+ :param from_timezone: The timezone to shift from. If no timezone is provided, uses the global
178
+ timezone variable set by the TimeUtil. A list of valid timezones can be found at
179
+ https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
180
+ :return: The epoch timestamp that would display as the same time in to_timezone as the given time in
181
+ from_timezone.
182
+ """
183
+ to_offset: int = TimeUtil.__get_timezone_offset(to_timezone) * 1000
184
+ from_offset: int = TimeUtil.__get_timezone_offset(from_timezone) * 1000
185
+ return millis + from_offset - to_offset
186
+
187
+ @staticmethod
188
+ def shift_format(time_point: str, time_format: str, to_timezone: str = "UTC", from_timezone: str | None = None) \
189
+ -> int:
190
+ """
191
+ Take a timestamp for a time in from_timezone and output the epoch timestamp that would display that same time
192
+ in to_timezone. A use case for this is when dealing with static date fields to convert a provided timestamp to
193
+ the value necessary to display that timestamp in the same way when viewed in the static date field.
194
+
195
+ :param time_point: The time in some date/time format to convert from.
196
+ :param time_format: The format that the time_point is in. Documentation for how the time formatting works
197
+ can be found at https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior
198
+ :param to_timezone: The timezone to shift to. If not provided, uses UTC.
199
+ :param from_timezone: The timezone to shift from. If no timezone is provided, uses the global
200
+ timezone variable set by the TimeUtil. A list of valid timezones can be found at
201
+ https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
202
+ :return: The epoch timestamp that would display as the same time in to_timezone as the given time in
203
+ from_timezone.
204
+ """
205
+ millis: int = TimeUtil.format_to_millis(time_point, time_format, from_timezone)
206
+ return TimeUtil.shift_millis(millis, to_timezone, from_timezone)
207
+
119
208
  # FR-46154: Create a function that determines if a string matches a time format.
120
209
  @staticmethod
121
210
  def str_matches_format(time_point: str, time_format: str) -> bool:
122
211
  """
123
212
  Determine if the given string is recognized as a valid time in the given format.
213
+
124
214
  :param time_point: The time in some date/time format to check.
125
215
  :param time_format: The format that the time_point should be in. Documentation for how the time formatting works
126
216
  can be found at https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior
@@ -0,0 +1,146 @@
1
+ # Multimodal registration client
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ from weakref import WeakValueDictionary
6
+
7
+ from databind.json import dumps, loads
8
+ from sapiopylib.rest.User import SapioUser
9
+
10
+ from sapiopycommons.general.exceptions import SapioException
11
+ from sapiopycommons.multimodal.multimodal_data import *
12
+
13
+
14
+ class MultiModalManager:
15
+ _user: SapioUser
16
+
17
+ __instances: WeakValueDictionary[SapioUser, MultiModalManager] = WeakValueDictionary()
18
+ __initialized: bool
19
+
20
+ def __new__(cls, user: SapioUser):
21
+ """
22
+ Observes singleton pattern per record model manager object.
23
+
24
+ :param user: The user that will make the webservice request to the application.
25
+ """
26
+ obj = cls.__instances.get(user)
27
+ if not obj:
28
+ obj = object.__new__(cls)
29
+ obj.__initialized = False
30
+ cls.__instances[user] = obj
31
+ return obj
32
+
33
+ def __init__(self, user:SapioUser):
34
+ if self.__initialized:
35
+ return
36
+ self._user = user
37
+ self.__initialized = True
38
+
39
+ def load_image_data(self, request: ImageDataRequestPojo) -> list[str]:
40
+ """
41
+ Loading of image data of a compound or a reaction in Sapio's unified drawing format.
42
+ :param request:
43
+ :return:
44
+ """
45
+ payload = dumps(request, ImageDataRequestPojo)
46
+ response = self._user.plugin_post("chemistry/request_image_data",
47
+ payload=payload, is_payload_plain_text=True)
48
+ self._user.raise_for_status(response)
49
+ return response.json()
50
+
51
+ def load_compounds(self, request: CompoundLoadRequestPojo):
52
+ """
53
+ Load compounds from the provided data here.
54
+ The compounds will not be registered but returned to you "the script".
55
+ To complete registration, you need to call register_compounds method after obtaining result.
56
+ """
57
+ payload = dumps(request, CompoundLoadRequestPojo)
58
+ response = self._user.plugin_post("chemistry/load",
59
+ payload=payload, is_payload_plain_text=True)
60
+ self._user.raise_for_status(response)
61
+ return loads(response.text, PyMoleculeLoaderResult)
62
+
63
+ def register_compounds(self, request: ChemRegisterRequestPojo) -> ChemCompleteImportPojo:
64
+ """
65
+ Register the filled compounds that are previously loaded via load_compounds operation.
66
+ """
67
+ payload = dumps(request, ChemRegisterRequestPojo)
68
+ response = self._user.plugin_post("chemistry/register",
69
+ payload=payload, is_payload_plain_text=True)
70
+ self._user.raise_for_status(response)
71
+ return loads(response.text, ChemCompleteImportPojo)
72
+
73
+ def load_reactions(self, reaction_str: str) -> PyIndigoReactionPojo:
74
+ """
75
+ Load a reaction and return the loaded reaction result.
76
+ :param reaction_str: A reaction string, in format of mrv, rxn, or smiles.
77
+ """
78
+ response = self._user.plugin_post("chemistry/reaction/load",
79
+ payload=reaction_str, is_payload_plain_text=True)
80
+ self._user.raise_for_status(response)
81
+ return loads(response.text, PyIndigoReactionPojo)
82
+
83
+ def register_reactions(self, reaction_str: str) -> DataRecord:
84
+ """
85
+ Register a single reaction provided.
86
+ Note: if the rxn has already specified a 2D coordinate, it may not be recomputed when generating record image.
87
+ :param reaction_str: The rxn of a reaction.
88
+ :return: The registered data record. This can be a record that already exists or new.
89
+ """
90
+ response = self._user.plugin_post("chemistry/reaction/register",
91
+ payload=reaction_str, is_payload_plain_text=True)
92
+ self._user.raise_for_status(response)
93
+ return loads(response.text, DataRecord)
94
+
95
+ def search_structures(self, request: ChemSearchRequestPojo) -> ChemSearchResponsePojo:
96
+ """
97
+ Perform structure search against the Sapio registries.
98
+ An error can be thrown as exception if search is structurally invalid.
99
+ :param request: The request object containing the detailed context of this search.
100
+ :return: The response object of the result.
101
+ """
102
+ payload = dumps(request, ChemSearchRequestPojo)
103
+ response = self._user.plugin_post("chemistry/search",
104
+ payload=payload, is_payload_plain_text=True)
105
+ self._user.raise_for_status(response)
106
+ return loads(response.text, ChemSearchResponsePojo)
107
+
108
+ def run_multi_sequence_alignment(self, request: MultiSequenceAlignmentRequestPojo) -> list[MultiSequenceAlignmentSeqPojo]:
109
+ """
110
+ Run a multi-sequence alignment using the specified tool and strategy.
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.
112
+ :return: The result of the multi-sequence alignment.
113
+ """
114
+ payload = dumps(request, MultiSequenceAlignmentRequestPojo)
115
+ response = self._user.plugin_post("bio/multisequencealignment",
116
+ payload=payload, is_payload_plain_text=True)
117
+ self._user.raise_for_status(response)
118
+ return loads(response.text, list[MultiSequenceAlignmentSeqPojo])
119
+
120
+ def register_bio(self, request: BioFileRegistrationRequest) -> BioFileRegistrationResponse:
121
+ """
122
+ Register to bioregistry of a file.
123
+ """
124
+ payload = dumps(request, BioFileRegistrationRequest)
125
+ response = self._user.plugin_post("bio/register/file", payload=payload, is_payload_plain_text=True)
126
+ self._user.raise_for_status(response)
127
+ return loads(response.text, BioFileRegistrationResponse)
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