sapiopycommons 2024.11.11a364__py3-none-any.whl → 2024.11.18a366__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 (47) hide show
  1. sapiopycommons/callbacks/callback_util.py +532 -83
  2. sapiopycommons/callbacks/field_builder.py +537 -0
  3. sapiopycommons/chem/IndigoMolecules.py +2 -0
  4. sapiopycommons/chem/Molecules.py +77 -18
  5. sapiopycommons/customreport/__init__.py +0 -0
  6. sapiopycommons/customreport/column_builder.py +60 -0
  7. sapiopycommons/customreport/custom_report_builder.py +130 -0
  8. sapiopycommons/customreport/term_builder.py +299 -0
  9. sapiopycommons/datatype/attachment_util.py +11 -10
  10. sapiopycommons/datatype/data_fields.py +61 -0
  11. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  12. sapiopycommons/eln/experiment_handler.py +272 -70
  13. sapiopycommons/eln/experiment_report_util.py +653 -0
  14. sapiopycommons/files/complex_data_loader.py +5 -4
  15. sapiopycommons/files/file_bridge.py +31 -24
  16. sapiopycommons/files/file_bridge_handler.py +340 -0
  17. sapiopycommons/files/file_data_handler.py +2 -5
  18. sapiopycommons/files/file_util.py +59 -9
  19. sapiopycommons/files/file_validator.py +92 -6
  20. sapiopycommons/files/file_writer.py +44 -15
  21. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  22. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  23. sapiopycommons/general/accession_service.py +375 -0
  24. sapiopycommons/general/aliases.py +207 -6
  25. sapiopycommons/general/audit_log.py +189 -0
  26. sapiopycommons/general/custom_report_util.py +212 -37
  27. sapiopycommons/general/exceptions.py +21 -8
  28. sapiopycommons/general/popup_util.py +21 -0
  29. sapiopycommons/general/sapio_links.py +50 -0
  30. sapiopycommons/general/time_util.py +8 -2
  31. sapiopycommons/multimodal/multimodal.py +146 -0
  32. sapiopycommons/multimodal/multimodal_data.py +490 -0
  33. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  34. sapiopycommons/processtracking/endpoints.py +22 -22
  35. sapiopycommons/recordmodel/record_handler.py +481 -97
  36. sapiopycommons/rules/eln_rule_handler.py +34 -25
  37. sapiopycommons/rules/on_save_rule_handler.py +34 -31
  38. sapiopycommons/sftpconnect/__init__.py +0 -0
  39. sapiopycommons/sftpconnect/sftp_builder.py +69 -0
  40. sapiopycommons/webhook/webhook_context.py +39 -0
  41. sapiopycommons/webhook/webhook_handlers.py +201 -42
  42. sapiopycommons/webhook/webservice_handlers.py +67 -0
  43. {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/METADATA +5 -2
  44. sapiopycommons-2024.11.18a366.dist-info/RECORD +59 -0
  45. {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/WHEEL +1 -1
  46. sapiopycommons-2024.11.11a364.dist-info/RECORD +0 -38
  47. {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from weakref import WeakValueDictionary
4
+
5
+ from sapiopylib.rest.User import SapioUser
1
6
  from sapiopylib.rest.pojo.DataRecord import DataRecord
2
- from sapiopylib.rest.pojo.webhook.VeloxRules import VeloxRuleType, VeloxRuleParser
7
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
3
8
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
4
9
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager
5
10
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
6
11
 
7
- from sapiopycommons.general.aliases import FieldMap
12
+ from sapiopycommons.general.aliases import FieldMap, AliasUtil, DataTypeIdentifier
8
13
  from sapiopycommons.general.exceptions import SapioException
9
14
 
10
15
 
@@ -12,7 +17,6 @@ from sapiopycommons.general.exceptions import SapioException
12
17
  class ElnRuleHandler:
13
18
  """
14
19
  A class which helps with the parsing and navigation of the ELN rule result map of a webhook context.
15
- TODO: Add functionality around the VeloxRuleType of the rule results.
16
20
  """
17
21
  __context: SapioWebhookContext
18
22
  """The context that this handler is working from."""
@@ -34,7 +38,25 @@ class ElnRuleHandler:
34
38
  """A mapping of entry name to the lists of field maps for that entry, each grouping of field maps being mapped by
35
39
  its data type."""
36
40
 
41
+ __instances: WeakValueDictionary[SapioUser, ElnRuleHandler] = WeakValueDictionary()
42
+ __initialized: bool
43
+
44
+ def __new__(cls, context: SapioWebhookContext):
45
+ if context.velox_eln_rule_result_map is None:
46
+ raise SapioException("No Velox ELN rule result map in context for ElnRuleHandler to parse.")
47
+ user = context if isinstance(context, SapioUser) else context.user
48
+ obj = cls.__instances.get(user)
49
+ if not obj:
50
+ obj = object.__new__(cls)
51
+ obj.__initialized = False
52
+ cls.__instances[user] = obj
53
+ return obj
54
+
37
55
  def __init__(self, context: SapioWebhookContext):
56
+ if self.__initialized:
57
+ return
58
+ self.__initialized = True
59
+
38
60
  if context.velox_eln_rule_result_map is None:
39
61
  raise SapioException("No Velox ELN rule result map in context for ElnRuleHandler to parse.")
40
62
  self.__context = context
@@ -64,13 +86,8 @@ class ElnRuleHandler:
64
86
  # Get the data type of this record. If this is an ELN type, ignore the digits.
65
87
  data_type: str = record.data_type_name
66
88
  # PR-46331: Ensure that all ELN types are converted to their base data type name.
67
- # TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
68
- if data_type.startswith("ELNExperiment_"):
69
- data_type = "ELNExperiment"
70
- elif data_type.startswith("ELNExperimentDetail_"):
71
- data_type = "ELNExperimentDetail"
72
- elif data_type.startswith("ELNSampleDetail_"):
73
- data_type = "ELNSampleDetail"
89
+ if ElnBaseDataType.is_eln_type(data_type):
90
+ data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
74
91
  # Update the list of records of this type that exist so far globally.
75
92
  self.__records.setdefault(data_type, set()).add(record)
76
93
  # Do the same for the list of records of this type for this specific entry.
@@ -85,19 +102,9 @@ class ElnRuleHandler:
85
102
  entry_dict: dict[str, dict[int, FieldMap]] = {}
86
103
  for record_result in entry_results:
87
104
  for result in record_result.velox_type_rule_field_map_result_list:
88
- # TODO: sapiopylib currently has a bug where this velox_type_pojo variable is stored as a dict instead
89
- # of as the intended VeloxRuleType object. Parse that dict as a VeloxRuleType before use.
90
- velox_type: VeloxRuleType | dict = result.velox_type_pojo
91
- if isinstance(velox_type, dict):
92
- velox_type: VeloxRuleType = VeloxRuleParser.parse_velox_rule_type(velox_type)
93
- data_type: str = velox_type.data_type_name
94
- # TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
95
- if data_type.startswith("ELNExperiment_"):
96
- data_type = "ELNExperiment"
97
- elif data_type.startswith("ELNExperimentDetail_"):
98
- data_type = "ELNExperimentDetail"
99
- elif data_type.startswith("ELNSampleDetail_"):
100
- data_type = "ELNSampleDetail"
105
+ data_type: str = result.velox_type_pojo.data_type_name
106
+ if ElnBaseDataType.is_eln_type(data_type):
107
+ data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
101
108
  for field_map in result.field_map_list:
102
109
  rec_id: int = field_map.get("RecordId")
103
110
  self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
@@ -118,7 +125,7 @@ class ElnRuleHandler:
118
125
  """
119
126
  return list(self.__entry_to_field_maps.keys())
120
127
 
121
- def get_records(self, data_type: str, entry: str | None = None) -> list[DataRecord]:
128
+ def get_records(self, data_type: DataTypeIdentifier, entry: str | None = None) -> list[DataRecord]:
122
129
  """
123
130
  Get records from the cached context with the given data type. Capable of being filtered to searching within
124
131
  the context of an entry name. If the given data type or entry does not exist in the context,
@@ -129,11 +136,12 @@ class ElnRuleHandler:
129
136
  type from every entry. If an entry is provided, but it does not exist in the context, returns an empty list.
130
137
  :return: The records from the context that match the input parameters.
131
138
  """
139
+ data_type: str = AliasUtil.to_data_type_name(data_type)
132
140
  records: dict[str, set[DataRecord]] = self.__entry_to_records.get(entry, {}) if entry else self.__records
133
141
  return list(records.get(data_type, []))
134
142
 
135
143
  # FR-46701: Add functions to the rule handlers for accessing the field maps of inaccessible records in the context.
136
- def get_field_maps(self, data_type: str, entry: str | None = None) -> list[FieldMap]:
144
+ def get_field_maps(self, data_type: DataTypeIdentifier, entry: str | None = None) -> list[FieldMap]:
137
145
  """
138
146
  Get field maps from the cached context with the given data type. Capable of being filtered to searching within
139
147
  the context of an entry name. If the given data type or entry does not exist in the context,
@@ -149,6 +157,7 @@ class ElnRuleHandler:
149
157
  list.
150
158
  :return: The field maps from the context that match the input parameters.
151
159
  """
160
+ data_type: str = AliasUtil.to_data_type_name(data_type)
152
161
  field_maps: dict[str, dict[int, FieldMap]] = self.__entry_to_field_maps.get(entry, {}) if entry else self.__field_maps
153
162
  return list(field_maps.get(data_type, {}).values())
154
163
 
@@ -1,10 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from weakref import WeakValueDictionary
4
+
5
+ from sapiopylib.rest.User import SapioUser
1
6
  from sapiopylib.rest.pojo.DataRecord import DataRecord
2
- from sapiopylib.rest.pojo.webhook.VeloxRules import VeloxRuleType, VeloxRuleParser
7
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
3
8
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
4
9
  from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelManager, RecordModelInstanceManager
5
10
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
6
11
 
7
- from sapiopycommons.general.aliases import FieldMap
12
+ from sapiopycommons.general.aliases import FieldMap, DataTypeIdentifier, AliasUtil
8
13
  from sapiopycommons.general.exceptions import SapioException
9
14
 
10
15
 
@@ -12,7 +17,6 @@ from sapiopycommons.general.exceptions import SapioException
12
17
  class OnSaveRuleHandler:
13
18
  """
14
19
  A class which helps with the parsing and navigation of the on save rule result map of a webhook context.
15
- TODO: Add functionality around the VeloxRuleType of the rule results.
16
20
  """
17
21
  __context: SapioWebhookContext
18
22
  """The context that this handler is working from."""
@@ -34,7 +38,25 @@ class OnSaveRuleHandler:
34
38
  """A mapping of record IDs of records in the context.data_record_list to the field maps related to that
35
39
  record, each grouping of field maps being mapped by its data type."""
36
40
 
41
+ __instances: WeakValueDictionary[SapioUser, OnSaveRuleHandler] = WeakValueDictionary()
42
+ __initialized: bool
43
+
44
+ def __new__(cls, context: SapioWebhookContext):
45
+ if context.velox_on_save_result_map is None:
46
+ raise SapioException("No Velox on save rule result map in context for OnSaveRuleHandler to parse.")
47
+ user = context if isinstance(context, SapioUser) else context.user
48
+ obj = cls.__instances.get(user)
49
+ if not obj:
50
+ obj = object.__new__(cls)
51
+ obj.__initialized = False
52
+ cls.__instances[user] = obj
53
+ return obj
54
+
37
55
  def __init__(self, context: SapioWebhookContext):
56
+ if self.__initialized:
57
+ return
58
+ self.__initialized = True
59
+
38
60
  if context.velox_on_save_result_map is None:
39
61
  raise SapioException("No Velox on save rule result map in context for OnSaveRuleHandler to parse.")
40
62
  self.__context = context
@@ -51,9 +73,6 @@ class OnSaveRuleHandler:
51
73
  self.__base_id_to_records = {}
52
74
  # Each record ID in the context has a list of results for that record.
53
75
  for record_id, rule_results in self.__context.velox_on_save_result_map.items():
54
- # TODO: Record IDs are currently being stored in the map as strings instead of ints. This can be removed
55
- # once sapiopylib is fixed.
56
- record_id = int(record_id)
57
76
  # Keep track of the records for this specific record ID.
58
77
  id_dict: dict[str, set[DataRecord]] = {}
59
78
  # The list of results for a record consist of a list of data records and a VeloxType that specifies
@@ -64,13 +83,8 @@ class OnSaveRuleHandler:
64
83
  # Get the data type of this record. If this is an ELN type, ignore the digits.
65
84
  data_type: str = record.data_type_name
66
85
  # PR-46331: Ensure that all ELN types are converted to their base data type name.
67
- # TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
68
- if data_type.startswith("ELNExperiment_"):
69
- data_type = "ELNExperiment"
70
- elif data_type.startswith("ELNExperimentDetail_"):
71
- data_type = "ELNExperimentDetail"
72
- elif data_type.startswith("ELNSampleDetail_"):
73
- data_type = "ELNSampleDetail"
86
+ if ElnBaseDataType.is_eln_type(data_type):
87
+ data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
74
88
  # Update the list of records of this type that exist so far globally.
75
89
  self.__records.setdefault(data_type, set()).add(record)
76
90
  # Do the same for the list of records of this type that relate to this record ID.
@@ -82,24 +96,11 @@ class OnSaveRuleHandler:
82
96
  self.__base_id_to_field_maps = {}
83
97
  # Repeat the same thing for the field map results.
84
98
  for record_id, rule_results in self.__context.velox_on_save_field_map_result_map.items():
85
- # TODO: Record IDs are currently being stored in the map as strings instead of ints. This can be removed
86
- # once sapiopylib is fixed.
87
- record_id = int(record_id)
88
99
  id_dict: dict[str, dict[int, FieldMap]] = {}
89
100
  for record_result in rule_results:
90
- # TODO: sapiopylib currently has a bug where this velox_type_pojo variable is stored as a dict instead
91
- # of as the intended VeloxRuleType object. Parse that dict as a VeloxRuleType before use.
92
- velox_type: VeloxRuleType | dict = record_result.velox_type_pojo
93
- if isinstance(velox_type, dict):
94
- velox_type: VeloxRuleType = VeloxRuleParser.parse_velox_rule_type(velox_type)
95
- data_type: str = velox_type.data_type_name
96
- # TODO: Use ElnBaseDataType.is_eln_type when it is no longer bugged in sapiopylib.
97
- if data_type.startswith("ELNExperiment_"):
98
- data_type = "ELNExperiment"
99
- elif data_type.startswith("ELNExperimentDetail_"):
100
- data_type = "ELNExperimentDetail"
101
- elif data_type.startswith("ELNSampleDetail_"):
102
- data_type = "ELNSampleDetail"
101
+ data_type: str = record_result.velox_type_pojo.data_type_name
102
+ if ElnBaseDataType.is_eln_type(data_type):
103
+ data_type = ElnBaseDataType.get_base_type(data_type).data_type_name
103
104
  for field_map in record_result.field_map_list:
104
105
  rec_id: int = field_map.get("RecordId")
105
106
  self.__field_maps.setdefault(data_type, {}).update({rec_id: field_map})
@@ -120,7 +121,7 @@ class OnSaveRuleHandler:
120
121
  """
121
122
  return list(self.__base_id_to_field_maps.keys())
122
123
 
123
- def get_records(self, data_type: str, record_id: int | None = None) -> list[DataRecord]:
124
+ def get_records(self, data_type: DataTypeIdentifier, record_id: int | None = None) -> list[DataRecord]:
124
125
  """
125
126
  Get records from the cached context with the given data type. Capable of being filtered to searching within
126
127
  the context of a record ID. If the given data type or record ID does not exist in the context,
@@ -131,11 +132,12 @@ class OnSaveRuleHandler:
131
132
  data type from every ID. If an ID is provided, but it does not exist in the context, returns an empty list.
132
133
  :return: The records from the context that match the input parameters.
133
134
  """
135
+ data_type: str = AliasUtil.to_data_type_name(data_type)
134
136
  records: dict[str, set[DataRecord]] = self.__base_id_to_records.get(record_id, {}) if record_id else self.__records
135
137
  return list(records.get(data_type, []))
136
138
 
137
139
  # FR-46701: Add functions to the rule handlers for accessing the field maps of inaccessible records in the context.
138
- def get_field_maps(self, data_type: str, record_id: int | None = None) -> list[FieldMap]:
140
+ def get_field_maps(self, data_type: DataTypeIdentifier, record_id: int | None = None) -> list[FieldMap]:
139
141
  """
140
142
  Get field maps from the cached context with the given data type. Capable of being filtered to searching within
141
143
  the context of a record ID. If the given data type or record ID does not exist in the context,
@@ -151,6 +153,7 @@ class OnSaveRuleHandler:
151
153
  list.
152
154
  :return: The field maps from the context that match the input parameters.
153
155
  """
156
+ data_type: str = AliasUtil.to_data_type_name(data_type)
154
157
  field_maps: dict[str, dict[int, FieldMap]] = self.__base_id_to_field_maps.get(record_id, {}) if record_id else self.__field_maps
155
158
  return list(field_maps.get(data_type, {}).values())
156
159
 
File without changes
@@ -0,0 +1,69 @@
1
+ import io
2
+ from enum import Enum
3
+
4
+ import paramiko
5
+ from paramiko import pkey
6
+ from paramiko.sftp_client import SFTPClient
7
+ from sapiopycommons.general.exceptions import SapioException
8
+
9
+
10
+ class SFTPAuthMethod(Enum):
11
+ """
12
+ An enum being used to specify connection type to the target server.
13
+ """
14
+ PASSWORD = 0
15
+ """Connection is being done via Password."""
16
+ FILEPATH = 1
17
+ """Connection is being done using a private key file in the codebase."""
18
+ KEY_STRING = 2
19
+ """Connection is being done using a private key in string form."""
20
+
21
+
22
+ class SFTPBuilder:
23
+ """
24
+ A class for making SFTP connections.
25
+ """
26
+
27
+ @staticmethod
28
+ def open_sftp(username: str, host: str, port: int, authentication: str,
29
+ connection_type: SFTPAuthMethod = SFTPAuthMethod.PASSWORD) -> SFTPClient:
30
+ """
31
+ Builds a SFTP client from user input.
32
+
33
+ :param username: The username of the individual trying to connect to the target server.
34
+ :param host: The hostname/IP address of the target server.
35
+ :param port: The port number used to connect to the target server.
36
+ :param authentication: The string used to connect to the target server. This could hold a filepath, a password
37
+ or a private key in string form depending on the connection_type parameter.
38
+
39
+ If authentication is a private key string, they are generally formated like this:
40
+ -----BEGIN OPENSSH PRIVATE KEY-----\n
41
+ asdfh;hjadfh;jghajdg54646+5fasdfadlajklgajd'gj'ajg654564\n
42
+ asdkjfhj;kghj;ahj;wh41234hjadjkhhdsgadshjkdghjshdlsds468\n
43
+ ....
44
+
45
+ :param connection_type: This enum is used to specify how the connection to the target server is being made.
46
+ The options are:
47
+ (0) PASSWORD: This means that the authentication parameter contains a password that will be used to connect to the server
48
+ (1) FILEPATH: This means that the authentication parameter contains a filepath leading to a private key file stored in the codebase
49
+ (2) KEY_STRING: This means that the authentication parameter contains the private key in string form
50
+
51
+ """
52
+
53
+ client = paramiko.SSHClient()
54
+ client.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy)
55
+
56
+ if connection_type == SFTPAuthMethod.FILEPATH:
57
+ client.connect(host, username=username, port=port, key_filename=authentication)
58
+ return client.open_sftp()
59
+
60
+ if connection_type == SFTPAuthMethod.KEY_STRING:
61
+ private_key: pkey = paramiko.RSAKey.from_private_key(io.StringIO(authentication))
62
+ client.connect(host, username=username, port=port, pkey=private_key)
63
+ return client.open_sftp()
64
+
65
+ if connection_type == SFTPAuthMethod.PASSWORD:
66
+ client.connect(host, username=username, password=authentication, port=port)
67
+ return client.open_sftp()
68
+
69
+ raise SapioException("The SFTPAuthMethod enumerator was not properly specified.")
@@ -0,0 +1,39 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
5
+
6
+
7
+ class CustomWebhookContext(SapioWebhookContext):
8
+ """
9
+ CustomWebhookContext is a wrapper for the default SapioWebhookContext to be used by custom invocation types to
10
+ convert the context_data that the server sends from a JSON string to usable parameters. This class works as a
11
+ middleman to cleanly convert a SapioWebhookContext object into a CustomWebhookContext by copying all the parameters
12
+ of the given context into the custom context.
13
+ """
14
+ def __init__(self, context: SapioWebhookContext):
15
+ # Use __dict__ so that we don't need t maintain this class due to future changes to SapioWebhookContext.
16
+ self.__dict__ = context.__dict__
17
+ super().__init__(self.user, self.end_point_type)
18
+
19
+
20
+ class ProcessQueueContext(CustomWebhookContext):
21
+ """
22
+ When a custom process queue endpoint is invoked, the context from the queue is sent in a context_data parameter
23
+ on the SapioWebhookContext object, stored as a JSON string. This class parses that JSON into fields for the
24
+ caller to make use of.
25
+ """
26
+ process_name: str
27
+ """The name of the process that the user invoked this webhook from."""
28
+ step_name: str
29
+ """The name of the step in the process that the user invoked this webhook from."""
30
+ process_queue_item_record_ids: list[int]
31
+ """The record IDs of the process queue items related to the records that were selected by the user when this
32
+ webhook was invoked."""
33
+
34
+ def __init__(self, context: SapioWebhookContext):
35
+ super().__init__(context)
36
+ context_data: dict[str, Any] = json.loads(self.context_data)
37
+ self.process_name = context_data["processName"]
38
+ self.step_name = context_data["workflowName"]
39
+ self.process_queue_item_record_ids = context_data["processQueueItemRecordIds"]