sapiopycommons 2024.3.19a157__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 +46 -1
  5. sapiopycommons/chem/Molecules.py +100 -21
  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 +14 -15
  11. sapiopycommons/datatype/data_fields.py +61 -0
  12. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  13. sapiopycommons/eln/experiment_handler.py +355 -91
  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 +149 -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 +263 -163
  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 +250 -15
  27. sapiopycommons/general/audit_log.py +185 -0
  28. sapiopycommons/general/custom_report_util.py +251 -31
  29. sapiopycommons/general/directive_util.py +86 -0
  30. sapiopycommons/general/exceptions.py +69 -7
  31. sapiopycommons/general/popup_util.py +59 -7
  32. sapiopycommons/general/sapio_links.py +50 -0
  33. sapiopycommons/general/storage_util.py +148 -0
  34. sapiopycommons/general/time_util.py +91 -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 +621 -148
  41. sapiopycommons/rules/eln_rule_handler.py +87 -8
  42. sapiopycommons/rules/on_save_rule_handler.py +87 -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 +614 -71
  47. sapiopycommons/webhook/webservice_handlers.py +317 -0
  48. {sapiopycommons-2024.3.19a157.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.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/WHEEL +1 -1
  51. sapiopycommons-2024.3.19a157.dist-info/RECORD +0 -28
  52. {sapiopycommons-2024.3.19a157.dist-info → sapiopycommons-2025.1.17a402.dist-info}/licenses/LICENSE +0 -0
@@ -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
  """
@@ -43,7 +48,7 @@ class TimeUtil:
43
48
  __timezone = TimeUtil.__to_tz(new_timezone)
44
49
 
45
50
  @staticmethod
46
- def __to_tz(timezone: str | int = None):
51
+ def __to_tz(timezone: str | int = None) -> Any:
47
52
  """
48
53
  :param timezone: Either the name of a timezone, a UTC offset in seconds, or None if the default should be used.
49
54
  :return: The timezone object to use for the given input. If the input is None, uses the default timezone.
@@ -53,11 +58,28 @@ class TimeUtil:
53
58
  # because pytz may return timezones from strings in Local Mean Time instead of a timezone with a UTC offset.
54
59
  # LMT may be a few minutes off of the actual time in that timezone right now.
55
60
  # https://stackoverflow.com/questions/35462876
56
- offset: int = int(datetime.now(pytz.timezone(timezone)).utcoffset().total_seconds() / 60)
57
- 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)
58
64
  if isinstance(timezone, int):
59
65
  return pytz.FixedOffset(timezone // 60)
60
- 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())
61
83
 
62
84
  @staticmethod
63
85
  def current_time(timezone: str | int = None) -> datetime:
@@ -92,9 +114,10 @@ class TimeUtil:
92
114
  return TimeUtil.current_time(timezone).strftime(time_format)
93
115
 
94
116
  @staticmethod
95
- 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:
96
118
  """
97
- 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
98
121
 
99
122
  :param millis: The time in milliseconds to convert from.
100
123
  :param time_format: The format to display the input time in. Documentation for how the time formatting works
@@ -103,6 +126,9 @@ class TimeUtil:
103
126
  timezone variable set by the TimeUtil. A list of valid timezones can be found at
104
127
  https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
105
128
  """
129
+ if millis is None:
130
+ return None
131
+
106
132
  tz = TimeUtil.__to_tz(timezone)
107
133
  return datetime.fromtimestamp(millis / 1000, tz).strftime(time_format)
108
134
 
@@ -121,6 +147,64 @@ class TimeUtil:
121
147
  tz = TimeUtil.__to_tz(timezone)
122
148
  return int(datetime.strptime(time_point, time_format).replace(tzinfo=tz).timestamp() * 1000)
123
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
+
124
208
  # FR-46154: Create a function that determines if a string matches a time format.
125
209
  @staticmethod
126
210
  def str_matches_format(time_point: str, time_format: str) -> bool:
@@ -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