sapiopycommons 2024.10.24a342__py3-none-any.whl → 2024.10.29a346__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.

@@ -5,8 +5,10 @@ from sapiopylib.rest.User import SapioUser
5
5
  from sapiopylib.rest.pojo.DataRecord import DataRecord
6
6
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
7
7
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
8
+ from sapiopylib.rest.pojo.eln.ExperimentEntry import ExperimentEntry
9
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnBaseDataType
8
10
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
9
- from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
11
+ from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol, ElnEntryStep
10
12
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
11
13
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
12
14
 
@@ -31,8 +33,10 @@ dictionaries.."""
31
33
  HasFieldWrappers = type[WrappedType] | WrappedRecordModel
32
34
  """An identifier for classes that have wrapper fields."""
33
35
  ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
34
- """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
36
+ """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for the experiment's notebook
35
37
  ID."""
38
+ ExperimentEntryIdentifier = ElnEntryStep | ExperimentEntry | int
39
+ """An ExperimentEntryIdentifier is either an ELN entry step, experiment entry, or an integer for the entry's ID."""
36
40
  FieldMap = dict[str, FieldValue]
37
41
  """A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
38
42
  any random dict in a webhook from one which is explicitly used for record fields."""
@@ -86,44 +90,50 @@ class AliasUtil:
86
90
  return record if isinstance(record, int) else record.record_id
87
91
 
88
92
  @staticmethod
89
- def to_data_type_name(value: DataTypeIdentifier) -> str:
93
+ def to_data_type_name(value: DataTypeIdentifier, convert_eln_dts: bool = True) -> str:
90
94
  """
91
95
  Convert a given value to a data type name.
92
96
 
93
97
  :param value: A value which is a string, record, or record model type.
98
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
94
99
  :return: A string of the data type name of the input value.
95
100
  """
96
- if isinstance(value, str):
97
- return value
98
101
  if isinstance(value, SapioRecord):
99
- return value.data_type_name
100
- return value.get_wrapper_data_type_name()
102
+ value = value.data_type_name
103
+ elif not isinstance(value, str):
104
+ value = value.get_wrapper_data_type_name()
105
+ if convert_eln_dts and ElnBaseDataType.is_eln_type(value):
106
+ return ElnBaseDataType.get_base_type(value).data_type_name
107
+ return value
101
108
 
102
109
  @staticmethod
103
- def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False) -> list[str] | set[str]:
110
+ def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False,
111
+ convert_eln_dts: bool = True) -> list[str] | set[str]:
104
112
  """
105
113
  Convert a given iterable of values to a list or set of data type names.
106
114
 
107
115
  :param values: An iterable of values which are strings, records, or record model types.
108
116
  :param return_set: If true, return a set instead of a list.
117
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
109
118
  :return: A list or set of strings of the data type name of the input value.
110
119
  """
111
- values = [AliasUtil.to_data_type_name(x) for x in values]
120
+ values = [AliasUtil.to_data_type_name(x, convert_eln_dts) for x in values]
112
121
  return set(values) if return_set else values
113
122
 
114
123
  @staticmethod
115
- def to_singular_data_type_name(values: Iterable[DataTypeIdentifier]) -> str:
124
+ def to_singular_data_type_name(values: Iterable[DataTypeIdentifier], convert_eln_dts: bool = True) -> str:
116
125
  """
117
126
  Convert a given iterable of values to a singular data type name that they share. Throws an exception if more
118
127
  than one data type name exists in the provided list of identifiers.
119
128
 
120
129
  :param values: An iterable of values which are strings, records, or record model types.
130
+ :param convert_eln_dts: If true, convert ELN data types to their base data type name.
121
131
  :return: The single data type name that the input vales share. Returns an empty string if an empty iterable
122
132
  was provided.
123
133
  """
124
134
  if not values:
125
135
  return ""
126
- data_types: set[str] = AliasUtil.to_data_type_names(values, True)
136
+ data_types: set[str] = AliasUtil.to_data_type_names(values, True, convert_eln_dts)
127
137
  if len(data_types) > 1:
128
138
  raise SapioException(f"Provided values contain multiple data types: {data_types}. "
129
139
  f"Only expecting a single data type.")
@@ -223,7 +233,51 @@ class AliasUtil:
223
233
  if isinstance(experiment, ElnExperiment):
224
234
  return experiment.notebook_experiment_id
225
235
  return experiment.get_id()
226
-
236
+
237
+ @staticmethod
238
+ def to_notebook_ids(experiments: list[ExperimentIdentifier]) -> list[int]:
239
+ """
240
+ Convert a list of objects that identify ELN experiments to their notebook IDs.
241
+
242
+ :return: The list of notebook IDs for the experiment identifiers.
243
+ """
244
+ notebook_ids: list[int] = []
245
+ for experiment in experiments:
246
+ notebook_ids.append(AliasUtil.to_notebook_id(experiment))
247
+ return notebook_ids
248
+
249
+ @staticmethod
250
+ def to_entry_id(entry: ExperimentEntryIdentifier) -> int:
251
+ """
252
+ Convert an object that identifies an experiment entry to its entry ID.
253
+
254
+ :return: The entry ID for the entry identifier.
255
+ """
256
+ if isinstance(entry, int):
257
+ return entry
258
+ elif isinstance(entry, ExperimentEntry):
259
+ return entry.entry_id
260
+ elif isinstance(entry, ElnEntryStep):
261
+ return entry.get_id()
262
+ raise SapioException(f"Unrecognized entry identifier of type {type(entry)}")
263
+
264
+ @staticmethod
265
+ def to_entry_ids(entries: list[ExperimentEntryIdentifier]) -> list[int]:
266
+ """
267
+ Convert a list of objects that identify experiment entries to their entry IDs.
268
+
269
+ :return: The list of entry IDs for the entry identifiers.
270
+ """
271
+ entry_ids: list[int] = []
272
+ for entry in entries:
273
+ entry_ids.append(AliasUtil.to_entry_id(entry))
274
+ return entry_ids
275
+
227
276
  @staticmethod
228
277
  def to_sapio_user(context: UserIdentifier) -> SapioUser:
278
+ """
279
+ Convert an object that could be either a SapioUser or SapioWebhookContext to just a SapioUser.
280
+
281
+ :return: A SapioUser object.
282
+ """
229
283
  return context if isinstance(context, SapioUser) else context.user
@@ -2,24 +2,13 @@ from enum import Enum
2
2
 
3
3
  from sapiopylib.rest.User import SapioUser
4
4
  from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria
5
- from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
6
5
 
6
+ from sapiopycommons.customreport.column_builder import ColumnBuilder
7
7
  from sapiopycommons.customreport.term_builder import TermBuilder
8
+ from sapiopycommons.datatype.pseudo_data_types import AuditLogPseudoDef
8
9
  from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
9
10
  from sapiopycommons.general.custom_report_util import CustomReportUtil
10
11
 
11
- EVENTTYPE_COLUMN = "EVENTTYPE"
12
- TIMESTAMP_COLUMN = "TIMESTAMP"
13
- DATATYPENAME_COLUMN = "DATATYPENAME"
14
- RECORDID_COLUMN = "RECORDID"
15
- DESCRIPTION_COLUMN = "DESCRIPTION"
16
- USERNAME_COLUMN = "USERNAME"
17
- USERCOMMENT_COLUMN = "USERCOMMENT"
18
- RECORDNAME_COLUMN = "RECORDNAME"
19
- DATAFIELDNAME_COLUMN = "DATAFIELDNAME"
20
- ORIGINALVALUE_COLUMN = "ORIGINALVALUE"
21
- NEWVALUE_COLUMN = "NEWVALUE"
22
-
23
12
 
24
13
  class EventType(Enum):
25
14
  """An enum to represent the possible event type values with the event type column in the audit log table."""
@@ -39,7 +28,6 @@ class EventType(Enum):
39
28
 
40
29
 
41
30
  class AuditLogEntry:
42
-
43
31
  __event_type: EventType
44
32
  __date: int
45
33
  __data_type_name: str
@@ -97,40 +85,41 @@ class AuditLogEntry:
97
85
  return self.__new_value
98
86
 
99
87
  def __init__(self, report_row: dict[str, FieldValue]):
100
- self.__event_type = EventType((report_row[EVENTTYPE_COLUMN]))
101
- self.__date = report_row[TIMESTAMP_COLUMN]
102
- self.__data_type_name = report_row[DATATYPENAME_COLUMN]
103
- self.__record_id = report_row[RECORDID_COLUMN]
104
- self.__description = report_row[DESCRIPTION_COLUMN]
105
- self.__users_login_name = report_row[USERNAME_COLUMN]
106
- self.__comment = report_row[USERCOMMENT_COLUMN]
107
- self.__data_record_name = report_row[RECORDNAME_COLUMN]
108
- self.__data_field_name = report_row[DATAFIELDNAME_COLUMN]
109
- self.__original_value = report_row[ORIGINALVALUE_COLUMN]
110
- self.__new_value = report_row[NEWVALUE_COLUMN]
111
-
112
-
113
- class AuditLog:
114
- AUDIT_LOG_PSEUDO_DATATYPE: str = "AUDITLOG"
115
- EVENT_TYPE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, EVENTTYPE_COLUMN, FieldType.ENUM)
116
- DATE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, TIMESTAMP_COLUMN, FieldType.DATE)
117
- DATA_TYPE_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATATYPENAME_COLUMN, FieldType.STRING)
118
- RECORD_ID: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, FieldType.LONG)
119
- DESCRIPTION: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DESCRIPTION_COLUMN, FieldType.STRING)
120
- USERS_LOGIN_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERNAME_COLUMN, FieldType.STRING)
121
- COMMENT: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERCOMMENT_COLUMN, FieldType.STRING)
122
- DATA_RECORD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDNAME_COLUMN, FieldType.STRING)
123
- DATA_FIELD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, FieldType.STRING)
124
- ORIGINAL_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, ORIGINALVALUE_COLUMN, FieldType.STRING)
125
- NEW_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, NEWVALUE_COLUMN, FieldType.STRING)
126
-
127
- AUDIT_LOG_COLUMNS = [EVENT_TYPE, DATE, DATA_TYPE_NAME, RECORD_ID, DESCRIPTION, USERS_LOGIN_NAME, COMMENT,
128
- DATA_RECORD_NAME, DATA_FIELD_NAME, ORIGINAL_VALUE, NEW_VALUE]
88
+ self.__event_type = EventType((report_row[AuditLogPseudoDef.EVENT_TYPE__FIELD_NAME.field_name]))
89
+ self.__date = report_row[AuditLogPseudoDef.TIME_STAMP__FIELD_NAME.field_name]
90
+ self.__data_type_name = report_row[AuditLogPseudoDef.DATA_TYPE_NAME__FIELD_NAME.field_name]
91
+ self.__record_id = report_row[AuditLogPseudoDef.RECORD_ID__FIELD_NAME.field_name]
92
+ self.__description = report_row[AuditLogPseudoDef.DESCRIPTION__FIELD_NAME.field_name]
93
+ self.__users_login_name = report_row[AuditLogPseudoDef.USER_NAME__FIELD_NAME.field_name]
94
+ self.__comment = report_row[AuditLogPseudoDef.USER_COMMENT__FIELD_NAME.field_name]
95
+ self.__data_record_name = report_row[AuditLogPseudoDef.RECORD_NAME__FIELD_NAME]
96
+ self.__data_field_name = report_row[AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME.field_name]
97
+ self.__original_value = report_row[AuditLogPseudoDef.ORIGINAL_VALUE__FIELD_NAME.field_name]
98
+ self.__new_value = report_row[AuditLogPseudoDef.NEW_VALUE__FIELD_NAME.field_name]
99
+
100
+
101
+ class AuditLogUtil:
129
102
  user: SapioUser
130
103
 
131
104
  def __init__(self, context: UserIdentifier):
132
105
  self.user = AliasUtil.to_sapio_user(context)
133
106
 
107
+ @staticmethod
108
+ def report_columns() -> list[ReportColumn]:
109
+ return [
110
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.EVENT_TYPE__FIELD_NAME),
111
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.TIME_STAMP__FIELD_NAME),
112
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DATA_TYPE_NAME__FIELD_NAME),
113
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.RECORD_ID__FIELD_NAME),
114
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DESCRIPTION__FIELD_NAME),
115
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.USER_NAME__FIELD_NAME),
116
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.USER_COMMENT__FIELD_NAME),
117
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.RECORD_NAME__FIELD_NAME),
118
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME),
119
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.ORIGINAL_VALUE__FIELD_NAME),
120
+ ColumnBuilder.build_column(AuditLogPseudoDef.DATA_TYPE_NAME, AuditLogPseudoDef.NEW_VALUE__FIELD_NAME)
121
+ ]
122
+
134
123
  @staticmethod
135
124
  def create_data_record_audit_log_report(records: list[RecordIdentifier],
136
125
  fields: list[FieldIdentifier] | None = None) -> CustomReportCriteria:
@@ -148,15 +137,19 @@ class AuditLog:
148
137
  # Build the raw report term querying for any entry with a matching record ID value to the record ID's
149
138
  # passed in.
150
139
  record_ids = AliasUtil.to_record_ids(records)
151
- root_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, record_ids)
140
+ root_term = TermBuilder.is_term(AuditLogPseudoDef.DATA_TYPE_NAME,
141
+ AuditLogPseudoDef.RECORD_ID__FIELD_NAME,
142
+ record_ids)
152
143
 
153
144
  # If the user passed in any specific fields, then we should limit the query to those fields.
154
145
  if fields:
155
146
  fields: list[str] = AliasUtil.to_data_field_names(fields)
156
- field_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, fields)
147
+ field_term = TermBuilder.is_term(AuditLogPseudoDef.DATA_TYPE_NAME,
148
+ AuditLogPseudoDef.DATA_FIELD_NAME__FIELD_NAME,
149
+ fields)
157
150
  root_term = TermBuilder.and_terms(root_term, field_term)
158
151
 
159
- return CustomReportCriteria(AuditLog.AUDIT_LOG_COLUMNS, root_term)
152
+ return CustomReportCriteria(AuditLogUtil.report_columns(), root_term)
160
153
 
161
154
  def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
162
155
  fields: list[FieldIdentifier] | None = None) \
@@ -172,7 +165,7 @@ class AuditLog:
172
165
  """
173
166
  fields: list[str] = AliasUtil.to_data_field_names(fields)
174
167
  # First, we must build our report criteria for running the Custom Report.
175
- criteria = AuditLog.create_data_record_audit_log_report(records, fields)
168
+ criteria = AuditLogUtil.create_data_record_audit_log_report(records, fields)
176
169
 
177
170
  # Then we must run the custom report using that criteria.
178
171
  raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
@@ -5,8 +5,6 @@ from datetime import datetime
5
5
 
6
6
  import pytz
7
7
 
8
- from sapiopycommons.general.exceptions import SapioException
9
-
10
8
  __timezone = None
11
9
  """The default timezone. Use TimeUtil.set_default_timezone in a global context before making use of TimeUtil."""
12
10
 
@@ -96,9 +94,10 @@ class TimeUtil:
96
94
  return TimeUtil.current_time(timezone).strftime(time_format)
97
95
 
98
96
  @staticmethod
99
- def millis_to_format(millis: int, time_format: str, timezone: str | int = None) -> str:
97
+ def millis_to_format(millis: int, time_format: str, timezone: str | int = None) -> str | None:
100
98
  """
101
- Convert the input time in milliseconds to the provided format.
99
+ Convert the input time in milliseconds to the provided format. If None is passed to the millis parameter,
100
+ None will be returned
102
101
 
103
102
  :param millis: The time in milliseconds to convert from.
104
103
  :param time_format: The format to display the input time in. Documentation for how the time formatting works
@@ -107,6 +106,9 @@ class TimeUtil:
107
106
  timezone variable set by the TimeUtil. A list of valid timezones can be found at
108
107
  https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
109
108
  """
109
+ if millis is None:
110
+ return None
111
+
110
112
  tz = TimeUtil.__to_tz(timezone)
111
113
  return datetime.fromtimestamp(millis / 1000, tz).strftime(time_format)
112
114
 
@@ -141,39 +143,3 @@ class TimeUtil:
141
143
  return True
142
144
  except Exception:
143
145
  return False
144
-
145
-
146
- class DateRange:
147
- start: int | None
148
- end: int | None
149
-
150
- @staticmethod
151
- def from_string(value: str | None) -> DateRange:
152
- """
153
- Construct a DateRange object from a string. The field value of date range fields is a string of the form
154
- <start timestamp>/<end timestamp>.
155
-
156
- :param value: A date range field value.
157
- :return: A DateRange object matching the input field value.
158
- """
159
- if not value:
160
- return DateRange(None, None)
161
- values: list[str] = value.split("/")
162
- return DateRange(int(values[0]), int(values[1]))
163
-
164
- def __init__(self, start: int | None, end: int | None):
165
- """
166
- :param start: The timestamp for the start of the date range.
167
- :param end: The timestamp for the end of the date rate.
168
- """
169
- if (start and end is None) or (end and start is None):
170
- raise SapioException("Both start and end values must be present in a date range.")
171
- if start and end and end < start:
172
- raise SapioException(f"End timestamp {end} is earlier than the start timestamp {start}.")
173
- self.start = start
174
- self.end = end
175
-
176
- def __str__(self) -> str | None:
177
- if not self.start and not self.end:
178
- return None
179
- return f"{self.start}/{self.end}"
@@ -38,9 +38,6 @@ class PyMolecule:
38
38
  normError: str | None
39
39
  desaltError: str | None
40
40
  desaltedList: list[str] | None
41
- registrationHash: str | None
42
- hasOrGroup: bool
43
- CXSMILESHash: str | None
44
41
 
45
42
 
46
43
  @dataclass
@@ -103,9 +100,9 @@ class PyMoleculeLoaderResult:
103
100
  compoundList: the compounds successfully loaded.
104
101
  errorList: an error record is added here for each one we failed to load in Sapio.
105
102
  """
106
- compoundByStr: dict[str, PyCompound] | None
107
- compoundList: list[PyCompound] | None
108
- errorList: list[ChemLoadingError] | None
103
+ compoundByStr: dict[str, PyCompound]
104
+ compoundList: list[PyCompound]
105
+ errorList: list[ChemLoadingError]
109
106
 
110
107
 
111
108
  @dataclass