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.
- sapiopycommons/callbacks/__init__.py +0 -0
- sapiopycommons/callbacks/callback_util.py +2041 -0
- sapiopycommons/callbacks/field_builder.py +545 -0
- sapiopycommons/chem/IndigoMolecules.py +52 -5
- sapiopycommons/chem/Molecules.py +114 -30
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +60 -0
- sapiopycommons/customreport/custom_report_builder.py +137 -0
- sapiopycommons/customreport/term_builder.py +315 -0
- sapiopycommons/datatype/attachment_util.py +17 -15
- sapiopycommons/datatype/data_fields.py +61 -0
- sapiopycommons/datatype/pseudo_data_types.py +440 -0
- sapiopycommons/eln/experiment_handler.py +390 -90
- sapiopycommons/eln/experiment_report_util.py +649 -0
- sapiopycommons/eln/plate_designer.py +152 -0
- sapiopycommons/files/complex_data_loader.py +31 -0
- sapiopycommons/files/file_bridge.py +153 -25
- sapiopycommons/files/file_bridge_handler.py +555 -0
- sapiopycommons/files/file_data_handler.py +633 -0
- sapiopycommons/files/file_util.py +270 -158
- sapiopycommons/files/file_validator.py +569 -0
- sapiopycommons/files/file_writer.py +377 -0
- sapiopycommons/flowcyto/flow_cyto.py +77 -0
- sapiopycommons/flowcyto/flowcyto_data.py +75 -0
- sapiopycommons/general/accession_service.py +375 -0
- sapiopycommons/general/aliases.py +259 -18
- sapiopycommons/general/audit_log.py +185 -0
- sapiopycommons/general/custom_report_util.py +252 -31
- sapiopycommons/general/directive_util.py +86 -0
- sapiopycommons/general/exceptions.py +69 -7
- sapiopycommons/general/popup_util.py +85 -18
- sapiopycommons/general/sapio_links.py +50 -0
- sapiopycommons/general/storage_util.py +148 -0
- sapiopycommons/general/time_util.py +97 -7
- sapiopycommons/multimodal/multimodal.py +146 -0
- sapiopycommons/multimodal/multimodal_data.py +490 -0
- sapiopycommons/processtracking/__init__.py +0 -0
- sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
- sapiopycommons/processtracking/endpoints.py +192 -0
- sapiopycommons/recordmodel/record_handler.py +653 -149
- sapiopycommons/rules/eln_rule_handler.py +89 -8
- sapiopycommons/rules/on_save_rule_handler.py +89 -12
- sapiopycommons/sftpconnect/__init__.py +0 -0
- sapiopycommons/sftpconnect/sftp_builder.py +70 -0
- sapiopycommons/webhook/webhook_context.py +39 -0
- sapiopycommons/webhook/webhook_handlers.py +617 -69
- sapiopycommons/webhook/webservice_handlers.py +317 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/METADATA +5 -4
- sapiopycommons-2025.1.17a402.dist-info/RECORD +60 -0
- {sapiopycommons-2024.3.18a156.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
- sapiopycommons-2024.3.18a156.dist-info/RECORD +0 -28
- {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 =
|
|
56
|
-
|
|
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
|
-
|
|
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
|