sapiopycommons 2024.8.15a304__py3-none-any.whl → 2024.8.19a305__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 (28) hide show
  1. sapiopycommons/callbacks/callback_util.py +122 -25
  2. sapiopycommons/customreport/__init__.py +0 -0
  3. sapiopycommons/customreport/column_builder.py +60 -0
  4. sapiopycommons/customreport/custom_report_builder.py +125 -0
  5. sapiopycommons/customreport/term_builder.py +296 -0
  6. sapiopycommons/datatype/attachment_util.py +15 -6
  7. sapiopycommons/eln/experiment_handler.py +193 -39
  8. sapiopycommons/files/complex_data_loader.py +1 -1
  9. sapiopycommons/files/file_bridge.py +1 -1
  10. sapiopycommons/files/file_bridge_handler.py +21 -0
  11. sapiopycommons/files/file_util.py +38 -5
  12. sapiopycommons/files/file_validator.py +21 -6
  13. sapiopycommons/files/file_writer.py +44 -15
  14. sapiopycommons/general/aliases.py +93 -2
  15. sapiopycommons/general/audit_log.py +200 -0
  16. sapiopycommons/general/popup_util.py +17 -0
  17. sapiopycommons/general/sapio_links.py +48 -0
  18. sapiopycommons/general/time_util.py +40 -0
  19. sapiopycommons/recordmodel/record_handler.py +114 -17
  20. sapiopycommons/rules/eln_rule_handler.py +29 -22
  21. sapiopycommons/rules/on_save_rule_handler.py +29 -28
  22. sapiopycommons/webhook/webhook_handlers.py +90 -26
  23. sapiopycommons/webhook/webservice_handlers.py +67 -0
  24. {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.19a305.dist-info}/METADATA +1 -1
  25. sapiopycommons-2024.8.19a305.dist-info/RECORD +50 -0
  26. sapiopycommons-2024.8.15a304.dist-info/RECORD +0 -43
  27. {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.19a305.dist-info}/WHEEL +0 -0
  28. {sapiopycommons-2024.8.15a304.dist-info → sapiopycommons-2024.8.19a305.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import warnings
3
4
  from abc import abstractmethod
4
5
  from enum import Enum
5
6
  from typing import Any
@@ -18,7 +19,7 @@ class FileWriter:
18
19
  body: list[list[Any]]
19
20
  delimiter: str
20
21
  line_break: str
21
- column_definitions: list[ColumnDef]
22
+ column_definitions: dict[str, ColumnDef]
22
23
 
23
24
  def __init__(self, headers: list[str], delimiter: str = ",", line_break: str = "\r\n"):
24
25
  """
@@ -30,7 +31,7 @@ class FileWriter:
30
31
  self.delimiter = delimiter
31
32
  self.line_break = line_break
32
33
  self.body = []
33
- self.column_definitions = []
34
+ self.column_definitions = {}
34
35
 
35
36
  def add_row_list(self, row: list[Any]) -> None:
36
37
  """
@@ -65,21 +66,49 @@ class FileWriter:
65
66
  new_row.append(row.get(header, ""))
66
67
  self.body.append(new_row)
67
68
 
68
- def add_column_definitions(self, column_defs: list[ColumnDef]) -> None:
69
+ def add_column_definition(self, header: str, column_def: ColumnDef) -> None:
69
70
  """
70
- Add new column definitions to this FileWriter. Column definitions are evaluated in the order they are added,
71
- meaning that they map to the header with the equivalent index. Before the file is built, the number of column
72
- definitions must equal the number of headers if any column definition is provided.
71
+ Add a new column definition to this FileWriter for a specific header.
73
72
 
74
- ColumnDefs are only used if the build_file function is provided with a list of RowBundles.
73
+ ColumnDefs are only used if the build_file function is provided with a list of RowBundles. Every header must
74
+ have a column definition if this is the case.
75
75
 
76
76
  Custom column definitions can be created by defining a class that extends ColumnDef and implements the print
77
77
  method.
78
78
 
79
- :param column_defs: A list of column definitions to be used to construct the file when build_file is
79
+ :param column_def: A column definitions to be used to construct the file when build_file is
80
80
  called.
81
+ :param header: The header that this column definition is for. If a header is provided that isn't in the headers
82
+ list, the header is appended to the end of the list.
81
83
  """
82
- self.column_definitions.extend(column_defs)
84
+ if header not in self.headers:
85
+ self.headers.append(header)
86
+ self.column_definitions[header] = column_def
87
+
88
+ def add_column_definitions(self, column_defs: dict[str, ColumnDef]) -> None:
89
+ """
90
+ Add new column definitions to this FileWriter.
91
+
92
+ ColumnDefs are only used if the build_file function is provided with a list of RowBundles. Every header must
93
+ have a column definition if this is the case.
94
+
95
+ Custom column definitions can be created by defining a class that extends ColumnDef and implements the print
96
+ method.
97
+
98
+ :param column_defs: A dictionary of header names to column definitions to be used to construct the file when
99
+ build_file is called.
100
+ """
101
+ # For backwards compatibility purposes, if column definitions are provided as a list,
102
+ # add them in order of appearance of the headers. This will only work if the headers are defined first, though.
103
+ if isinstance(column_defs, list):
104
+ warnings.warn("Adding column definitions is no longer expected as a list. Continuing to provide a list to "
105
+ "this function may result in undesirable behavior.", UserWarning)
106
+ if not self.headers:
107
+ raise SapioException("No headers provided to FileWriter before the column definitions were added.")
108
+ for header, column_def in zip(self.headers, column_defs):
109
+ self.column_definitions[header] = column_def
110
+ for header, column_def in column_defs.items():
111
+ self.add_column_definition(header, column_def)
83
112
 
84
113
  def build_file(self, rows: list[RowBundle] | None = None, sorter=None, reverse: bool = False) -> str:
85
114
  """
@@ -100,11 +129,10 @@ class FileWriter:
100
129
  """
101
130
  # If any column definitions have been provided, the number of column definitions and headers must be equal.
102
131
  if self.column_definitions:
103
- def_count: int = len(self.column_definitions)
104
- header_count: int = len(self.headers)
105
- if def_count != header_count:
106
- raise SapioException(f"FileWriter has {def_count} column definitions defined but {header_count} "
107
- f"headers. The number of column definitions must equal the number of headers.")
132
+ for header in self.headers:
133
+ if header not in self.column_definitions:
134
+ raise SapioException(f"FileWriter has no column definition for the header {header}. If any column "
135
+ f"definitions are provided, then all headers must have a column definition.")
108
136
  # If any RowBundles have been provided, there must be column definitions for mapping them to the file.
109
137
  elif rows:
110
138
  raise SapioException(f"FileWriter was given RowBundles but contains no column definitions for mapping "
@@ -130,7 +158,8 @@ class FileWriter:
130
158
  rows.sort(key=lambda x: x.index)
131
159
  for row in rows:
132
160
  new_row: list[Any] = []
133
- for column in self.column_definitions:
161
+ for header in self.headers:
162
+ column = self.column_definitions[header]
134
163
  if column.may_skip and row.may_skip:
135
164
  new_row.append("")
136
165
  else:
@@ -2,10 +2,13 @@ from collections.abc import Iterable
2
2
  from typing import Any
3
3
 
4
4
  from sapiopylib.rest.pojo.DataRecord import DataRecord
5
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
5
6
  from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment
6
7
  from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
7
8
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
8
- from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType
9
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
10
+
11
+ from sapiopycommons.general.exceptions import SapioException
9
12
 
10
13
  RecordModel = PyRecordModel | WrappedRecordModel | WrappedType
11
14
  """Different forms that a record model could take."""
@@ -13,6 +16,13 @@ SapioRecord = DataRecord | RecordModel
13
16
  """A record could be provided as either a DataRecord, PyRecordModel, or WrappedRecordModel (WrappedType)."""
14
17
  RecordIdentifier = SapioRecord | int
15
18
  """A RecordIdentifier is either a record type or an integer for the record's record ID."""
19
+ DataTypeIdentifier = SapioRecord | type[WrappedType] | str
20
+ """A DataTypeIdentifier is either a SapioRecord, a record model wrapper type, or a string."""
21
+ FieldIdentifier = WrapperField | str | tuple[str, FieldType]
22
+ """A FieldIdentifier is either wrapper field from a record model wrapper, a string, or a tuple of string
23
+ and field type."""
24
+ HasFieldWrappers = type[WrappedType] | WrappedRecordModel
25
+ """An identifier for classes that have wrapper fields."""
16
26
  ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
17
27
  """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
18
28
  ID."""
@@ -50,7 +60,88 @@ class AliasUtil:
50
60
 
51
61
  :return: A list of record IDs for the input records.
52
62
  """
53
- return [(x if isinstance(x, int) else x.record_id) for x in records]
63
+ return [(AliasUtil.to_record_id(x)) for x in records]
64
+
65
+ @staticmethod
66
+ def to_record_id(record: RecordIdentifier):
67
+ """
68
+ Convert a single variable that could be either an integer, DataRecord, PyRecordModel,
69
+ or WrappedRecordModel to just an integer (taking the record ID from the record).
70
+
71
+ :return: A record ID for the input record.
72
+ """
73
+ return record if isinstance(record, int) else record.record_id
74
+
75
+ @staticmethod
76
+ def to_data_type_name(value: DataTypeIdentifier) -> str:
77
+ """
78
+ Convert a given value to a data type name.
79
+
80
+ :param value: A value which is a string, record, or record model type.
81
+ :return: A string of the data type name of the input value.
82
+ """
83
+ if isinstance(value, str):
84
+ return value
85
+ if isinstance(value, SapioRecord):
86
+ return value.data_type_name
87
+ return value.get_wrapper_data_type_name()
88
+
89
+ @staticmethod
90
+ def to_data_type_names(values: Iterable[DataTypeIdentifier], return_set: bool = False) -> list[str] | set[str]:
91
+ """
92
+ Convert a given iterable of values to a list or set of data type names.
93
+
94
+ :param values: An iterable of values which are strings, records, or record model types.
95
+ :param return_set: If true, return a set instead of a list.
96
+ :return: A list or set of strings of the data type name of the input value.
97
+ """
98
+ values = [AliasUtil.to_data_type_name(x) for x in values]
99
+ return set(values) if return_set else values
100
+
101
+ @staticmethod
102
+ def to_data_field_name(value: FieldIdentifier) -> str:
103
+ """
104
+ Convert a string or WrapperField to a data field name string.
105
+
106
+ :param value: A string or WrapperField.
107
+ :return: A string of the data field name of the input value.
108
+ """
109
+ if isinstance(value, tuple):
110
+ return value[0]
111
+ if isinstance(value, WrapperField):
112
+ return value.field_name
113
+ return value
114
+
115
+ @staticmethod
116
+ def to_data_field_names(values: Iterable[FieldIdentifier]) -> list[str]:
117
+ """
118
+ Convert an iterable of strings or WrapperFields to a list of data field name strings.
119
+
120
+ :param values: An iterable of strings or WrapperFields.
121
+ :return: A list of strings of the data field names of the input values.
122
+ """
123
+ return [AliasUtil.to_data_field_name(x) for x in values]
124
+
125
+ @staticmethod
126
+ def to_field_type(field: FieldIdentifier, data_type: HasFieldWrappers | None = None) -> FieldType:
127
+ """
128
+ Convert a given field identifier to the field type for that field.
129
+
130
+ :param field: A string or WrapperField.
131
+ :param data_type: If the field is provided as a string, then a record model wrapper or wrapped record model
132
+ must be provided to determine the field type.
133
+ :return: The field type of the given field.
134
+ """
135
+ if isinstance(field, tuple):
136
+ return field[1]
137
+ if isinstance(field, WrapperField):
138
+ return field.field_type
139
+ for var in dir(data_type):
140
+ attr = getattr(data_type, var)
141
+ if isinstance(attr, WrapperField) and attr.field_name == field:
142
+ return attr.field_type
143
+ raise SapioException(f"The wrapper of data type \"{data_type.get_wrapper_data_type_name()}\" doesn't have a "
144
+ f"field with the name \"{field}\",")
54
145
 
55
146
  @staticmethod
56
147
  def to_field_map_lists(records: Iterable[SapioRecord]) -> list[FieldMap]:
@@ -0,0 +1,200 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from sapiopylib.rest.DataRecordManagerService import DataRecordManager
5
+ from sapiopylib.rest.User import SapioUser
6
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, RawReportTerm, CustomReportCriteria, RawTermOperation, \
7
+ CompositeReportTerm, CompositeTermOperation
8
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
9
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
10
+
11
+ from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil
12
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
13
+
14
+ EVENTTYPE_COLUMN = "EVENTTYPE"
15
+ TIMESTAMP_COLUMN = "TIMESTAMP"
16
+ DATATYPENAME_COLUMN = "DATATYPENAME"
17
+ RECORDID_COLUMN = "RECORDID"
18
+ DESCRIPTION_COLUMN = "DESCRIPTION"
19
+ USERNAME_COLUMN = "USERNAME"
20
+ USERCOMMENT_COLUMN = "USERCOMMENT"
21
+ RECORDNAME_COLUMN = "RECORDNAME"
22
+ DATAFIELDNAME_COLUMN = "DATAFIELDNAME"
23
+ ORIGINALVALUE_COLUMN = "ORIGINALVALUE"
24
+ NEWVALUE_COLUMN = "NEWVALUE"
25
+
26
+
27
+ class EventType(Enum):
28
+ """An enum to represent the possible event type values with the event type column in the audit log table."""
29
+ ADD = 0
30
+ DELETE = 1
31
+ MODIFY = 2
32
+ INFO = 3
33
+ ERROR = 4
34
+ WARNING = 5
35
+ IMPORT = 6
36
+ GENERATE = 7
37
+ EXPORT = 8
38
+ ADDREF = 9
39
+ REMOVEREF = 10
40
+ ESIGNATURE = 11
41
+ ROLEASSIGNMENT = 12
42
+
43
+
44
+ class AuditLogEntry:
45
+
46
+ __event_type: EventType
47
+ __date: int
48
+ __data_type_name: str
49
+ __record_id: int
50
+ __description: str
51
+ __users_login_name: str
52
+ __comment: str
53
+ __data_record_name: str
54
+ __data_field_name: str
55
+ __original_value: str
56
+ __new_value: str
57
+
58
+ @property
59
+ def event_type(self) -> EventType:
60
+ return self.__event_type
61
+
62
+ @property
63
+ def date(self) -> int:
64
+ return self.__date
65
+
66
+ @property
67
+ def data_type_name(self) -> str:
68
+ return self.__data_type_name
69
+
70
+ @property
71
+ def record_id(self) -> int:
72
+ return self.__record_id
73
+
74
+ @property
75
+ def description(self) -> str:
76
+ return self.__description
77
+
78
+ @property
79
+ def users_login_name(self) -> str:
80
+ return self.__users_login_name
81
+
82
+ @property
83
+ def comment(self) -> str:
84
+ return self.__comment
85
+
86
+ @property
87
+ def data_record_name(self) -> str:
88
+ return self.__data_record_name
89
+
90
+ @property
91
+ def data_field_name(self) -> str:
92
+ return self.__data_field_name
93
+
94
+ @property
95
+ def original_value(self) -> str:
96
+ return self.__original_value
97
+
98
+ @property
99
+ def new_value(self) -> str:
100
+ return self.__new_value
101
+
102
+ def __init__(self, report_row: dict[str, Any]):
103
+ self.__event_type = EventType((report_row[EVENTTYPE_COLUMN]))
104
+ self.__date = report_row[TIMESTAMP_COLUMN]
105
+ self.__data_type_name = report_row[DATATYPENAME_COLUMN]
106
+ self.__record_id = report_row[RECORDID_COLUMN]
107
+ self.__description = report_row[DESCRIPTION_COLUMN]
108
+ self.__users_login_name = report_row[USERNAME_COLUMN]
109
+ self.__comment = report_row[USERCOMMENT_COLUMN]
110
+ self.__data_record_name = report_row[RECORDNAME_COLUMN]
111
+ self.__data_field_name = report_row[DATAFIELDNAME_COLUMN]
112
+ self.__original_value = report_row[ORIGINALVALUE_COLUMN]
113
+ self.__new_value = report_row[NEWVALUE_COLUMN]
114
+
115
+
116
+ class AuditLog:
117
+ AUDIT_LOG_PSEUDO_DATATYPE: str = "AUDITLOG"
118
+ EVENT_TYPE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, EVENTTYPE_COLUMN, FieldType.ENUM)
119
+ DATE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, TIMESTAMP_COLUMN, FieldType.DATE)
120
+ DATA_TYPE_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATATYPENAME_COLUMN, FieldType.STRING)
121
+ RECORD_ID: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, FieldType.LONG)
122
+ DESCRIPTION: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DESCRIPTION_COLUMN, FieldType.STRING)
123
+ USERS_LOGIN_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERNAME_COLUMN, FieldType.STRING)
124
+ COMMENT: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERCOMMENT_COLUMN, FieldType.STRING)
125
+ DATA_RECORD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDNAME_COLUMN, FieldType.STRING)
126
+ DATA_FIELD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, FieldType.STRING)
127
+ ORIGINAL_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, ORIGINALVALUE_COLUMN, FieldType.STRING)
128
+ NEW_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, NEWVALUE_COLUMN, FieldType.STRING)
129
+
130
+ AUDIT_LOG_COLUMNS = [EVENT_TYPE, DATE, DATA_TYPE_NAME, RECORD_ID, DESCRIPTION, USERS_LOGIN_NAME, COMMENT,
131
+ DATA_RECORD_NAME, DATA_FIELD_NAME, ORIGINAL_VALUE, NEW_VALUE]
132
+ user: SapioUser
133
+
134
+ def __init__(self, context: SapioWebhookContext | SapioUser):
135
+ self.user = context if isinstance(context, SapioUser) else context.user
136
+
137
+ @staticmethod
138
+ def create_data_record_audit_log_report(records: list[RecordIdentifier], fields: list[str] | None = None) -> CustomReportCriteria:
139
+ """
140
+ This method creates a CustomReportCriteria object for running an audit log query based on data records.
141
+
142
+ Creates a CustomReportCriteria object with a query term based on the record ids/records passed into the method.
143
+ Optionally, the fields parameter can be populated to limit the search to particular fields. If the fields
144
+ parameter is not populated, the search will include results for all field changes.
145
+
146
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
147
+ :param fields: The data field names to include changes for.
148
+ :return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
149
+ """
150
+ # We need to compile the record ids from these record identifiers as "str" variables, so they can be
151
+ # concatenated.
152
+ record_ids = AliasUtil.to_record_ids(records)
153
+ id_strs = [str(id_int) for id_int in record_ids]
154
+
155
+ # Next we'll build the raw report term querying for any entry with a matching record id value to the record ID's
156
+ # passed in
157
+ root_term = RawReportTerm(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN,
158
+ RawTermOperation.EQUAL_TO_OPERATOR, "{" + ",".join(id_strs) + "}")
159
+
160
+ # If the user passed in any specific fields, then we should limit the query to those fields.
161
+ if fields:
162
+ field_term = RawReportTerm(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN,
163
+ RawTermOperation.EQUAL_TO_OPERATOR, "{" + ",".join(fields) + "}")
164
+ root_term = CompositeReportTerm(root_term, CompositeTermOperation.AND_OPERATOR, field_term)
165
+
166
+ return CustomReportCriteria(AuditLog.AUDIT_LOG_COLUMNS, root_term)
167
+
168
+ def run_data_record_audit_log_report(self, records: list[RecordIdentifier], fields: list[str] | None = None) -> dict[RecordIdentifier, list[AuditLogEntry]]:
169
+ """
170
+ This method runs a custom report for changes made to the given data records using the audit log.
171
+ See "create_data_record_audit_log_report" for more details about the data record audit log report.
172
+
173
+ :param records: The DataRecords, RecordModels, or record ids to base the search on.
174
+ :param fields: The data field names to include changes for.
175
+ :return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
176
+ AuditLogEntry objects which match the record id value of those records.
177
+ """
178
+ # First, we must build our report criteria for running the Custom Report.
179
+ criteria = AuditLog.create_data_record_audit_log_report(records, fields)
180
+
181
+ # Then we must run the custom report using that criteria.
182
+ raw_report_data: list[dict[str, Any]] = CustomReportUtil.run_custom_report(self.user, criteria)
183
+
184
+ # This section will prepare a map matching the original RecordIdentifier by record id.
185
+ # This is because the audit log entries will have record ids, but we want the keys in our result map
186
+ # to match the record identifiers that the user passed in, for convenience.
187
+ record_identifier_mapping: dict[int, RecordIdentifier] = dict()
188
+ for record in records:
189
+ record_id = AliasUtil.to_record_id(record)
190
+ record_identifier_mapping[record_id] = record
191
+
192
+ # Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
193
+ # and the value is a list of applicable audit log entries.
194
+ final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
195
+ for audit_entry_data in raw_report_data:
196
+ audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
197
+ identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
198
+ final_audit_data.setdefault(identifier, []).append(audit_entry)
199
+
200
+ return final_audit_data
@@ -1,3 +1,5 @@
1
+ import warnings
2
+
1
3
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
2
4
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
3
5
  from sapiopylib.rest.pojo.datatype.FieldDefinition import VeloxStringFieldDefinition, AbstractVeloxFieldDefinition, \
@@ -51,6 +53,7 @@ class PopupUtil:
51
53
  :param request_context: Context that will be returned to the webhook server in the client callback result.
52
54
  :return: A SapioWebhookResult with the popup as its client callback request.
53
55
  """
56
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
54
57
  if display_name is None:
55
58
  display_name = data_type
56
59
  if plural_display_name is None:
@@ -97,6 +100,7 @@ class PopupUtil:
97
100
  :param request_context: Context that will be returned to the webhook server in the client callback result.
98
101
  :return: A SapioWebhookResult with the popup as its client callback request.
99
102
  """
103
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
100
104
  # Get the field definitions of the data type.
101
105
  data_type: str = record.data_type_name
102
106
  type_man = DataMgmtServer.get_data_type_manager(context.user)
@@ -155,6 +159,7 @@ class PopupUtil:
155
159
  :param request_context: Context that will be returned to the webhook server in the client callback result.
156
160
  :return: A SapioWebhookResult with the popup as its client callback request.
157
161
  """
162
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
158
163
  if max_length is None:
159
164
  max_length = len(default_value) if default_value else 100
160
165
  string_field = VeloxStringFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -191,6 +196,7 @@ class PopupUtil:
191
196
  :param request_context: Context that will be returned to the webhook server in the client callback result.
192
197
  :return: A SapioWebhookResult with the popup as its client callback request.
193
198
  """
199
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
194
200
  if default_value is None:
195
201
  default_value = max(0, min_value)
196
202
  integer_field = VeloxIntegerFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -229,6 +235,7 @@ class PopupUtil:
229
235
  :param request_context: Context that will be returned to the webhook server in the client callback result.
230
236
  :return: A SapioWebhookResult with the popup as its client callback request.
231
237
  """
238
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
232
239
  if default_value is None:
233
240
  default_value = min_value
234
241
  double_field = VeloxDoubleFieldDefinition(data_type, field_name, field_name, default_value=default_value,
@@ -260,6 +267,7 @@ class PopupUtil:
260
267
  :param request_context: Context that will be returned to the webhook server in the client callback result.
261
268
  :return: A SapioWebhookResult with the popup as its client callback request.
262
269
  """
270
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
263
271
  if display_name is None:
264
272
  display_name = data_type
265
273
  if plural_display_name is None:
@@ -295,6 +303,7 @@ class PopupUtil:
295
303
  :param request_context: Context that will be returned to the webhook server in the client callback result.
296
304
  :return: A SapioWebhookResult with the popup as its client callback request.
297
305
  """
306
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
298
307
  data_types: set[str] = {x.data_type_name for x in records}
299
308
  if len(data_types) > 1:
300
309
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -347,6 +356,7 @@ class PopupUtil:
347
356
  :param request_context: Context that will be returned to the webhook server in the client callback result.
348
357
  :return: A SapioWebhookResult with the popup as its client callback request.
349
358
  """
359
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
350
360
  data_types: set[str] = {x.data_type_name for x in records}
351
361
  if len(data_types) > 1:
352
362
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -391,6 +401,7 @@ class PopupUtil:
391
401
  :param request_context: Context that will be returned to the webhook server in the client callback result.
392
402
  :return: A SapioWebhookResult with the popup as its client callback request.
393
403
  """
404
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
394
405
  callback = ListDialogRequest(title, multi_select, options,
395
406
  callback_context_data=request_context)
396
407
  return SapioWebhookResult(True, client_callback_request=callback)
@@ -415,6 +426,7 @@ class PopupUtil:
415
426
  :param request_context: Context that will be returned to the webhook server in the client callback result.
416
427
  :return: A SapioWebhookResult with the popup as its client callback request.
417
428
  """
429
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
418
430
  callback = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
419
431
  callback_context_data=request_context)
420
432
  return SapioWebhookResult(True, client_callback_request=callback)
@@ -437,6 +449,7 @@ class PopupUtil:
437
449
  :param request_context: Context that will be returned to the webhook server in the client callback result.
438
450
  :return: A SapioWebhookResult with the popup as its client callback request.
439
451
  """
452
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
440
453
  return PopupUtil.option_popup(title, msg, ["OK"], 0, user_can_cancel, request_context=request_context)
441
454
 
442
455
  @staticmethod
@@ -458,6 +471,7 @@ class PopupUtil:
458
471
  :param request_context: Context that will be returned to the webhook server in the client callback result.
459
472
  :return: A SapioWebhookResult with the popup as its client callback request.
460
473
  """
474
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
461
475
  return PopupUtil.option_popup(title, msg, ["Yes", "No"], 0 if default_yes else 1, user_can_cancel,
462
476
  request_context=request_context)
463
477
 
@@ -470,6 +484,7 @@ class PopupUtil:
470
484
 
471
485
  Deprecated for PopupUtil.text_field_popup.
472
486
  """
487
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
473
488
  return PopupUtil.string_field_popup(title, "", field_name, msg, len(msg), False, data_type,
474
489
  request_context=request_context, auto_size=True)
475
490
 
@@ -481,6 +496,7 @@ class PopupUtil:
481
496
 
482
497
  Deprecated for PopupUtil.option_popup.
483
498
  """
499
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
484
500
  return PopupUtil.option_popup(title, msg, options, 0, user_can_cancel, request_context=request_context)
485
501
 
486
502
  @staticmethod
@@ -490,4 +506,5 @@ class PopupUtil:
490
506
 
491
507
  Deprecated for PopupUtil.ok_popup.
492
508
  """
509
+ warnings.warn("PopupUtil is deprecated as of 24.5+. Use CallbackUtil instead.", DeprecationWarning)
493
510
  return PopupUtil.ok_popup(title, msg, False, request_context=request_context)
@@ -0,0 +1,48 @@
1
+ from sapiopylib.rest.User import SapioUser
2
+ from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
3
+
4
+ from sapiopycommons.general.aliases import RecordIdentifier, ExperimentIdentifier, AliasUtil
5
+ from sapiopycommons.general.exceptions import SapioException
6
+
7
+
8
+ class SapioNavigationLinker:
9
+ """
10
+ Given a URL to a system's webservice API (example: https://company.exemplareln.com/webservice/api), construct
11
+ URLs for navigation links to various locations in the system.
12
+ """
13
+ base_url: str
14
+
15
+ def __init__(self, url: str | SapioUser | SapioWebhookContext):
16
+ """
17
+ :param url: A user or context object that is being used to send requests to a Sapio system, or a URL to a
18
+ system's webservice API.
19
+ """
20
+ if isinstance(url, SapioWebhookContext):
21
+ url = url.user.url
22
+ elif isinstance(url, SapioUser):
23
+ url = url.url
24
+ self.base_url = url.rstrip("/").replace('webservice/api', 'veloxClient')
25
+
26
+ def data_record(self, record_identifier: RecordIdentifier, data_type_name: str | None = None) -> str:
27
+ """
28
+ :param record_identifier: An object that can be used to identify a record in the system, be that a record ID,
29
+ a data record, or a record model.
30
+ :param data_type_name: If the provided record identifier is a record ID, then the data type name of the record
31
+ must be provided in this parameter. Otherwise, this parameter is ignored.
32
+ :return: A URL for navigating to the input record.
33
+ """
34
+ record_id: int = AliasUtil.to_record_id(record_identifier)
35
+ if not isinstance(record_identifier, int):
36
+ data_type_name = AliasUtil.to_data_type_name(record_identifier)
37
+ if not data_type_name:
38
+ raise SapioException("Unable to create a data record link without a data type name. "
39
+ "Only a record ID was provided.")
40
+ return self.base_url + f"/#dataType={data_type_name};recordId={record_id};view=dataRecord"
41
+
42
+ def experiment(self, experiment: ExperimentIdentifier) -> str:
43
+ """
44
+ :param experiment: An object that can be used to identify an experiment in the system, be that an experiment
45
+ object, experiment protocol, or a notebook ID.
46
+ :return: A URL for navigating to the input experiment.
47
+ """
48
+ return self.base_url + f"/#notebookExperimentId={AliasUtil.to_notebook_id(experiment)};view=eln"
@@ -1,8 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import time
2
4
  from datetime import datetime
3
5
 
4
6
  import pytz
5
7
 
8
+ from sapiopycommons.general.exceptions import SapioException
9
+
6
10
  __timezone = None
7
11
  """The default timezone. Use TimeUtil.set_default_timezone in a global context before making use of TimeUtil."""
8
12
 
@@ -137,3 +141,39 @@ class TimeUtil:
137
141
  return True
138
142
  except Exception:
139
143
  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}"