sapiopycommons 2025.7.1a576__py3-none-any.whl → 2025.7.2a579__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 (60) hide show
  1. sapiopycommons/callbacks/callback_util.py +665 -332
  2. sapiopycommons/callbacks/field_builder.py +2 -0
  3. sapiopycommons/chem/IndigoMolecules.py +31 -1
  4. sapiopycommons/chem/Molecules.py +3 -3
  5. sapiopycommons/chem/ps_commons.py +381 -0
  6. sapiopycommons/customreport/auto_pagers.py +26 -1
  7. sapiopycommons/customreport/term_builder.py +1 -1
  8. sapiopycommons/datatype/pseudo_data_types.py +349 -326
  9. sapiopycommons/eln/experiment_cache.py +188 -0
  10. sapiopycommons/eln/experiment_handler.py +408 -767
  11. sapiopycommons/eln/experiment_report_util.py +11 -6
  12. sapiopycommons/eln/experiment_step_factory.py +476 -0
  13. sapiopycommons/eln/plate_designer.py +7 -2
  14. sapiopycommons/eln/step_creation.py +236 -0
  15. sapiopycommons/files/file_util.py +7 -5
  16. sapiopycommons/general/accession_service.py +2 -2
  17. sapiopycommons/general/aliases.py +3 -1
  18. sapiopycommons/general/audit_log.py +7 -0
  19. sapiopycommons/general/custom_report_util.py +12 -0
  20. sapiopycommons/general/data_structure_util.py +115 -0
  21. sapiopycommons/processtracking/custom_workflow_handler.py +11 -1
  22. sapiopycommons/processtracking/endpoints.py +27 -0
  23. sapiopycommons/recordmodel/record_handler.py +657 -317
  24. sapiopycommons/rules/eln_rule_handler.py +8 -1
  25. sapiopycommons/rules/on_save_rule_handler.py +8 -1
  26. sapiopycommons/webhook/webhook_handlers.py +3 -0
  27. sapiopycommons/webhook/webservice_handlers.py +2 -2
  28. {sapiopycommons-2025.7.1a576.dist-info → sapiopycommons-2025.7.2a579.dist-info}/METADATA +2 -2
  29. sapiopycommons-2025.7.2a579.dist-info/RECORD +69 -0
  30. sapiopycommons/ai/__init__.py +0 -0
  31. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.py +0 -43
  32. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2.pyi +0 -31
  33. sapiopycommons/ai/api/fielddefinitions/proto/fields_pb2_grpc.py +0 -24
  34. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.py +0 -123
  35. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2.pyi +0 -598
  36. sapiopycommons/ai/api/fielddefinitions/proto/velox_field_def_pb2_grpc.py +0 -24
  37. sapiopycommons/ai/api/plan/proto/step_output_pb2.py +0 -45
  38. sapiopycommons/ai/api/plan/proto/step_output_pb2.pyi +0 -42
  39. sapiopycommons/ai/api/plan/proto/step_output_pb2_grpc.py +0 -24
  40. sapiopycommons/ai/api/plan/proto/step_pb2.py +0 -43
  41. sapiopycommons/ai/api/plan/proto/step_pb2.pyi +0 -43
  42. sapiopycommons/ai/api/plan/proto/step_pb2_grpc.py +0 -24
  43. sapiopycommons/ai/api/plan/script/proto/script_pb2.py +0 -53
  44. sapiopycommons/ai/api/plan/script/proto/script_pb2.pyi +0 -99
  45. sapiopycommons/ai/api/plan/script/proto/script_pb2_grpc.py +0 -153
  46. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.py +0 -57
  47. sapiopycommons/ai/api/plan/tool/proto/entry_pb2.pyi +0 -96
  48. sapiopycommons/ai/api/plan/tool/proto/entry_pb2_grpc.py +0 -24
  49. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.py +0 -67
  50. sapiopycommons/ai/api/plan/tool/proto/tool_pb2.pyi +0 -220
  51. sapiopycommons/ai/api/plan/tool/proto/tool_pb2_grpc.py +0 -154
  52. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.py +0 -39
  53. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2.pyi +0 -32
  54. sapiopycommons/ai/api/session/proto/sapio_conn_info_pb2_grpc.py +0 -24
  55. sapiopycommons/ai/protobuf_utils.py +0 -508
  56. sapiopycommons/ai/test_client.py +0 -216
  57. sapiopycommons/ai/tool_service_base.py +0 -798
  58. sapiopycommons-2025.7.1a576.dist-info/RECORD +0 -92
  59. {sapiopycommons-2025.7.1a576.dist-info → sapiopycommons-2025.7.2a579.dist-info}/WHEEL +0 -0
  60. {sapiopycommons-2025.7.1a576.dist-info → sapiopycommons-2025.7.2a579.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,236 @@
1
+ from typing import Iterable
2
+
3
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, ExperimentEntryIdentifier
4
+ from sapiopylib.rest.pojo.TableColumn import TableColumn
5
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition
6
+ from sapiopylib.rest.pojo.eln.SapioELNEnums import ExperimentEntryStatus, ElnEntryType
7
+ from sapiopylib.rest.pojo.eln.field_set import ElnFieldSetInfo
8
+
9
+
10
+ # CR-47564: Created these classes to streamline entry creation using the ExperimentEntryFactory.
11
+ class StepCreation:
12
+ """
13
+ An object that contains the criteria for creating a new entry in the experiment.
14
+ """
15
+ _entry_type: ElnEntryType
16
+ """The type of the entry to be created."""
17
+ is_shown_in_template: bool | None
18
+ """Whether the entry will appear in the template if the experiment this entry is in is saved to a new template."""
19
+ is_removable: bool | None
20
+ """Whether the entry can be removed by users."""
21
+ is_renamable: bool | None
22
+ """Whether the entry can be renamed by users."""
23
+ is_static_view: bool | None
24
+ """Whether the entry's attachment is static. For attachment entries only. Static attachment entries will store
25
+ their attachment data in the template."""
26
+ related_entry_set: Iterable[ExperimentEntryIdentifier | str] | None
27
+ """The IDs of the entries this entry is implicitly dependent on. If any of the entries are deleted then this entry
28
+ is also deleted."""
29
+ dependency_set: Iterable[ExperimentEntryIdentifier | str] | None
30
+ """The IDs of the entries this entry is dependent on. Requires the entries to be completed before this entry will
31
+ be enabled."""
32
+ requires_grabber_plugin: bool
33
+ """Whether to run a grabber plugin when this entry is initialized."""
34
+ entry_singleton_id: str | None
35
+ """When this field is present (i.e. not null or blank) it will enforce that only one entry with this singleton
36
+ value is present in the experiment. If you attempt to create an entry with the singletonId of an entry already
37
+ present in the experiment it will return the existing entry instead of creating a new one. If an entry isn't
38
+ present in the Notebook Experiment with a matching singletonId it will create a new entry like normal."""
39
+ is_hidden: bool | None
40
+ """Whether the user is able to visibly see this entry within the experiment."""
41
+ entry_height: int | None
42
+ """The height of this entry in pixels. Setting the height to 0 will cause the entry to auto-size to its contents."""
43
+ description: str | None
44
+ """The description of the entry."""
45
+ is_initialization_required: bool | None
46
+ """Whether the user must manually initialize this entry by clicking on it."""
47
+ collapse_entry: bool | None
48
+ """Whether the entry should be collapsed by default."""
49
+ entry_status: ExperimentEntryStatus | None
50
+ """The current status of the entry."""
51
+ template_item_fulfilled_timestamp: int | None
52
+ """The time in milliseconds since the epoch that this entry became initialized."""
53
+ entry_options: dict[str, str] | None
54
+ """The entry options of the entry."""
55
+
56
+ def __init__(self, entry_type: ElnEntryType):
57
+ self._entry_type = entry_type
58
+ self.is_shown_in_template = None
59
+ self.is_removable = None
60
+ self.is_renamable = None
61
+ self.is_static_view = None
62
+ self.related_entry_set = None
63
+ self.dependency_set = None
64
+ self.requires_grabber_plugin = False
65
+ self.entry_singleton_id = None
66
+ self.is_hidden = None
67
+ self.entry_height = None
68
+ self.description = None
69
+ self.is_initialization_required = None
70
+ self.collapse_entry = None
71
+ self.entry_status = None
72
+ self.template_item_fulfilled_timestamp = None
73
+ self.entry_options = None
74
+
75
+ @property
76
+ def entry_type(self) -> ElnEntryType:
77
+ return self._entry_type
78
+
79
+ class AttachmentStepCreation(StepCreation):
80
+ """
81
+ An object that contains criteria for creating a new attachment entry in an experiment.
82
+ """
83
+ def __init__(self):
84
+ super().__init__(ElnEntryType.Attachment)
85
+
86
+
87
+ class DashboardStepCreation(StepCreation):
88
+ """
89
+ An object that contains criteria for creating a new dashboard entry in an experiment.
90
+ """
91
+ source_entry: ExperimentEntryIdentifier | str | None
92
+ """The entry that contains the source data for this entry's dashboard(s)."""
93
+ dashboard_guids: Iterable[str] | None
94
+ """The GUIDs of the dashboards to display in this entry."""
95
+
96
+ def __init__(self):
97
+ super().__init__(ElnEntryType.Dashboard)
98
+ self.source_entry = None
99
+ self.dashboard_guids = None
100
+
101
+
102
+ class GlobalDtFormStepCreation(StepCreation):
103
+ """
104
+ An object that contains criteria for creating a new global data type form entry in an experiment.
105
+ """
106
+ layout_name: str | None
107
+ """The name of the data type layout to be displayed in this form. The layout must be for the data type for this
108
+ entry."""
109
+ form_names: Iterable[str] | None
110
+ """The names of the components in the chosen data type layout to display in this form."""
111
+ extension_types: Iterable[DataTypeIdentifier] | None
112
+ """The names of the extension data types to display fields from within the form."""
113
+ field_names: Iterable[FieldIdentifier] | None
114
+ """A list of data field names for the fields to be displayed in the form."""
115
+
116
+ def __init__(self):
117
+ super().__init__(ElnEntryType.Form)
118
+ self.form_names = None
119
+ self.layout_name = None
120
+ self.extension_types = None
121
+ self.field_names = None
122
+
123
+
124
+ class ELnDtFormStepCreation(StepCreation):
125
+ """
126
+ An object that contains criteria for creating a new ELN data type form entry in an experiment.
127
+ """
128
+ is_field_addable: bool | None
129
+ """Whether new fields can be added to the entry by users."""
130
+ is_existing_field_removable: bool | None
131
+ """Whether existing fields on the entry can be removed by users."""
132
+ field_sets: Iterable[int | str | ElnFieldSetInfo] | None
133
+ """The predefined field sets to display in this form."""
134
+ field_definitions: Iterable[AbstractVeloxFieldDefinition] | None
135
+ """New field definitions to be created for this entry."""
136
+ predefined_field_names: Iterable[str] | None
137
+ """The names of the predefined fields to display in this form."""
138
+
139
+ def __init__(self):
140
+ super().__init__(ElnEntryType.Form)
141
+ self.is_field_addable = None
142
+ self.is_existing_field_removable = None
143
+ self.field_sets = None
144
+ self.field_definitions = None
145
+ self.predefined_field_names = None
146
+ self.table_columns = None
147
+
148
+
149
+ class PluginStepCreation(StepCreation):
150
+ """
151
+ An object that contains criteria for creating a new plugin entry in an experiment.
152
+ """
153
+ plugin_name: str | None
154
+ """The client side plugin name to render this entry with."""
155
+ using_template_data: bool | None
156
+ """Whether this entry will use the data from the template."""
157
+ provides_template_data: bool | None
158
+ """Whether this entry can provide data to copy into a new template."""
159
+
160
+ def __init__(self):
161
+ super().__init__(ElnEntryType.Plugin)
162
+ self.plugin_name = None
163
+ self.using_template_data = None
164
+ self.provides_template_data = None
165
+
166
+
167
+ class GlobalDtTableStepCreation(StepCreation):
168
+ """
169
+ An object that contains criteria for creating a new global data type table entry in an experiment.
170
+ """
171
+ layout_name: str | None
172
+ """The name of the data type layout to display in this table."""
173
+ extension_types: Iterable[str] | None
174
+ """The names of the extension data types to display fields from within the table."""
175
+ table_columns: Iterable[TableColumn] | None
176
+ """The columns to display in the table. This can be used to change the sort order and direction of columns."""
177
+ field_names: Iterable[FieldIdentifier] | None
178
+ """A list of data field names for the fields to be displayed in the table. These will be added as TableColumns and
179
+ placed after any of the existing columns specified in the table_columns parameter without any sorting."""
180
+ show_key_fields: bool | None
181
+ """Whether the key fields of the data type should be shown in the entry."""
182
+
183
+ def __init__(self):
184
+ super().__init__(ElnEntryType.Table)
185
+ self.layout_name = None
186
+ self.extension_types = None
187
+ self.table_columns = None
188
+ self.field_names = None
189
+ self.show_key_fields = None
190
+
191
+
192
+ class ELnDtTableStepCreation(StepCreation):
193
+ """
194
+ An object that contains criteria for creating a new ELN data type table entry in an experiment.
195
+ """
196
+ is_field_addable: bool | None
197
+ """Whether new fields can be added to the entry by users."""
198
+ is_existing_field_removable: bool | None
199
+ """Whether existing fields on the entry can be removed by users."""
200
+ field_sets: Iterable[int | str | ElnFieldSetInfo] | None
201
+ """The predefined field sets to display in this form."""
202
+ field_definitions: Iterable[AbstractVeloxFieldDefinition] | None
203
+ """New field definitions to be created for this entry."""
204
+ predefined_field_names: Iterable[str] | None
205
+ """The names of the predefined fields to display in this form."""
206
+ table_columns: Iterable[TableColumn] | None
207
+ """The columns to display in the table."""
208
+
209
+ def __init__(self):
210
+ super().__init__(ElnEntryType.Table)
211
+ self.is_field_addable = None
212
+ self.is_existing_field_removable = None
213
+ self.field_sets = None
214
+ self.field_definitions = None
215
+ self.predefined_field_names = None
216
+ self.table_columns = None
217
+
218
+
219
+ class TempDataStepCreation(StepCreation):
220
+ """
221
+ An object that contains criteria for creating a new temp data entry in an experiment.
222
+ """
223
+ plugin_path: str | None
224
+ """The temp data plugin path to run to populate the entry."""
225
+
226
+ def __init__(self):
227
+ super().__init__(ElnEntryType.TempData)
228
+ self.plugin_path = None
229
+
230
+
231
+ class TextStepCreation(StepCreation):
232
+ """
233
+ An object that contains criteria for creating a new text entry in an experiment.
234
+ """
235
+ def __init__(self):
236
+ super().__init__(ElnEntryType.Text)
@@ -327,11 +327,13 @@ class FileUtil:
327
327
  :param files: A dictionary of file name to file data as a string or bytes.
328
328
  :return: The bytes for a zip file containing the input files.
329
329
  """
330
- zip_buffer: io.BytesIO = io.BytesIO()
331
- with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
332
- for file_name, file_data in files.items():
333
- zip_file.writestr(file_name, file_data)
334
- return zip_buffer.getvalue()
330
+ with io.BytesIO() as zip_buffer:
331
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
332
+ for file_name, file_data in files.items():
333
+ zip_file.writestr(file_name, file_data)
334
+ # PR-47697: Indent the getvalue call into the outer with block. Making the call outside of the with block
335
+ # throws an I/O exception.
336
+ return zip_buffer.getvalue()
335
337
 
336
338
  # Deprecated functions:
337
339
 
@@ -199,7 +199,7 @@ class AccessionRequestId(AbstractAccessionServiceOperator):
199
199
 
200
200
  Properties:
201
201
  numberOfCharacters: Number of characters maximum in the request ID.
202
- accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compability patch for converting these to the new preference format.
202
+ accessorName: This is a legacy variable from drum.getNextIdListByMapName(), which allows setting different "accessorName" from old system. We need this for compatibility patch for converting these to the new preference format.
203
203
  """
204
204
  _num_of_characters: int
205
205
  _accessor_name: str
@@ -341,7 +341,7 @@ class AccessionService:
341
341
  def get_affixed_id_in_batch(self, data_type_name: str, data_field_name: str, num_ids: int, prefix: str | None,
342
342
  suffix: str | None, num_digits: int | None, start_num: int = 1) -> list[str]:
343
343
  """
344
- Get the batch affixed IDs that are maximal in cache and contiguious for a particular datatype.datafield under a given format.
344
+ Get the batch affixed IDs that are maximal in cache and contiguous for a particular datatype.datafield under a given format.
345
345
  :param data_type_name: The datatype name to look for max ID
346
346
  :param data_field_name: The datafield name to look for max ID
347
347
  :param num_ids: The number of IDs to accession.
@@ -219,7 +219,9 @@ class AliasUtil:
219
219
  # noinspection PyTypeChecker
220
220
  fields: FieldMap = record.get_fields()
221
221
  else:
222
- fields: FieldMap = record.fields.copy_to_dict()
222
+ # TI-47593: Copy the record's fields by using the get() method instead of copy_to_dict() so that date
223
+ # macros get translated to valid field values.
224
+ fields: FieldMap = {f: record.fields.get(f) for f in record.fields}
223
225
  # PR-47457: Only include the record ID if the caller requests it, since including the record ID can break
224
226
  # callbacks in certain circumstances if the record ID is negative.
225
227
  if include_record_id:
@@ -8,6 +8,7 @@ from sapiopycommons.customreport.column_builder import ColumnBuilder
8
8
  from sapiopycommons.customreport.term_builder import TermBuilder
9
9
  from sapiopycommons.datatype.pseudo_data_types import AuditLogPseudoDef
10
10
  from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
11
+ from sapiopycommons.general.exceptions import SapioException
11
12
 
12
13
 
13
14
  class EventType(Enum):
@@ -134,6 +135,9 @@ class AuditLogUtil:
134
135
  :param fields: The data field names to include changes for.
135
136
  :return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
136
137
  """
138
+ # PR-47701: Throw an exception if no records are provided.
139
+ if not records:
140
+ raise SapioException("No records provided. Please provide at least one record to build a report for.")
137
141
  # Build the raw report term querying for any entry with a matching record ID value to the record ID's
138
142
  # passed in.
139
143
  record_ids = AliasUtil.to_record_ids(records)
@@ -160,6 +164,9 @@ class AuditLogUtil:
160
164
  :return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
161
165
  AuditLogEntry objects which match the record id value of those records.
162
166
  """
167
+ # PR-47701: Return an empty dictionary if no records are provided.
168
+ if not records:
169
+ return {}
163
170
  # First, we must build our report criteria for running the Custom Report.
164
171
  criteria = AuditLogUtil.create_data_record_audit_log_report(records, fields)
165
172
 
@@ -24,6 +24,12 @@ class CustomReportUtil:
24
24
  System reports are also known as predefined searches in the system and must be defined in the data designer for
25
25
  a specific data type. That is, saved searches created by users cannot be run using this function.
26
26
 
27
+ IMPORTANT NOTICE: Custom reports that are not single data type (i.e. they have terms or columns from multiple
28
+ data types) may not be 100% time accurate. Such reports use the system's ancestor table to retrieve the
29
+ relationships, and this table takes some time to update after relationships are updated, especially for more
30
+ populous data types. If you need 100% time accurate results to the current state of the records and
31
+ relationships in the database, you should query for the records directly instead of using a custom report.
32
+
27
33
  :param context: The current webhook context or a user object to send requests from.
28
34
  :param report_name: The name of the system report to run.
29
35
  :param filters: If provided, filter the results of the report using the given mapping of headers to values to
@@ -63,6 +69,12 @@ class CustomReportUtil:
63
69
  results. They are like advanced or predefined searches from the system, except they are constructed from
64
70
  within the webhook instead of from within the system.
65
71
 
72
+ IMPORTANT NOTICE: Custom reports that are not single data type (i.e. they have terms or columns from multiple
73
+ data types) may not be 100% time accurate. Such reports use the system's ancestor table to retrieve the
74
+ relationships, and this table takes some time to update after relationships are updated, especially for more
75
+ populous data types. If you need 100% time accurate results to the current state of the records and
76
+ relationships in the database, you should query for the records directly instead of using a custom report.
77
+
66
78
  :param context: The current webhook context or a user object to send requests from.
67
79
  :param report_criteria: The custom report criteria to run.
68
80
  :param filters: If provided, filter the results of the report using the given mapping of headers to values to
@@ -0,0 +1,115 @@
1
+ from enum import Enum
2
+ from typing import Iterable, Any, Collection
3
+
4
+ from sapiopycommons.general.exceptions import SapioException
5
+
6
+
7
+ class ArrayTransformation(Enum):
8
+ """
9
+ An enumeration of the different transformations that can be applied to a 2D array.
10
+ """
11
+ ROTATE_CLOCKWISE = 0
12
+ ROTATE_COUNTER_CLOCKWISE = 1
13
+ ROTATE_180_DEGREES = 2
14
+ MIRROR_HORIZONTAL = 3
15
+ MIRROR_VERTICAL = 4
16
+
17
+
18
+ # FR-47524: Create a DataStructureUtils class that implements various collection utility functions from our Java
19
+ # libraries.
20
+ class DataStructureUtil:
21
+ """
22
+ Utility class for working with data structures. Copies from ListUtil, SetUtil, and various other classes in
23
+ our Java library.
24
+ """
25
+ @staticmethod
26
+ def find_first_or_none(values: Iterable[Any]) -> Any | None:
27
+ """
28
+ Get the first value from an iterable, or None if the iterable is empty.
29
+
30
+ :param values: An iterable of values.
31
+ :return: The first value from the input, or None if the input is empty.
32
+ """
33
+ return next(iter(values), None)
34
+
35
+ @staticmethod
36
+ def remove_null_values(values: Iterable[Any]) -> list[Any]:
37
+ """
38
+ Remove null values from a list.
39
+
40
+ :param values: An iterable of values.
41
+ :return: A list containing all the non-null values from the input.
42
+ """
43
+ return [value for value in values if value is not None]
44
+
45
+ @staticmethod
46
+ def transform_2d_array(values: Collection[Collection[Any]], transformation: ArrayTransformation) \
47
+ -> Collection[Collection[Any]]:
48
+ """
49
+ Perform a transformation on a 2D list.
50
+
51
+ :param values: An iterable of iterables. The iterables should all be of the same size.
52
+ :param transformation: The transformation to apply to the input.
53
+ :return: A new 2D list containing the input transformed according to the specified transformation.
54
+ """
55
+ x: int = len(values)
56
+ for row in values:
57
+ y = len(row)
58
+ if y != x:
59
+ raise SapioException(f"Input must be a square 2D array. The provided array has a length of {x} but "
60
+ f"at least one row has a length of {y}.")
61
+
62
+ match transformation:
63
+ case ArrayTransformation.ROTATE_CLOCKWISE:
64
+ return [list(row) for row in zip(*values[::-1])]
65
+ case ArrayTransformation.ROTATE_COUNTER_CLOCKWISE:
66
+ return [list(row) for row in zip(*values)][::-1]
67
+ case ArrayTransformation.ROTATE_180_DEGREES:
68
+ return [row[::-1] for row in values[::-1]]
69
+ case ArrayTransformation.MIRROR_HORIZONTAL:
70
+ return [list(row[::-1]) for row in values]
71
+ case ArrayTransformation.MIRROR_VERTICAL:
72
+ return values[::-1]
73
+
74
+ raise SapioException(f"Invalid transformation: {transformation}")
75
+
76
+ @staticmethod
77
+ def flatten_to_list(values: Iterable[Iterable[Any]]) -> list[Any]:
78
+ """
79
+ Flatten a list of lists into a single list.
80
+
81
+ :param values: An iterable of iterables.
82
+ :return: A single list containing all the values from the input. Elements are in the order they appear in the
83
+ input.
84
+ """
85
+ return [item for sublist in values for item in sublist]
86
+
87
+ @staticmethod
88
+ def flatten_to_set(values: Iterable[Iterable[Any]]) -> set[Any]:
89
+ """
90
+ Flatten a list of lists into a single set.
91
+
92
+ :param values: An iterable of iterables.
93
+ :return: A single set containing all the values from the input. Elements are in the order they appear in the
94
+ input.
95
+ """
96
+ return {item for subset in values for item in subset}
97
+
98
+ @staticmethod
99
+ def invert_dictionary(dictionary: dict[Any, Any], list_values: bool = False) \
100
+ -> dict[Any, Any] | dict[Any, list[Any]]:
101
+ """
102
+ Invert a dictionary, swapping keys and values. Note that the values of the input dictionary must be hashable.
103
+
104
+ :param dictionary: A dictionary to invert.
105
+ :param list_values: If false, keys that share the same value in the input dictionary will be overwritten in
106
+ the output dictionary so that only the last key remains. If true, the values of the output dictionary will
107
+ be lists where input keys that share the same value will be stored together.
108
+ :return: A new dictionary with the keys and values swapped.
109
+ """
110
+ if list_values:
111
+ inverted = {}
112
+ for key, value in dictionary.items():
113
+ inverted.setdefault(value, []).append(key)
114
+ return inverted
115
+ return {value: key for key, value in dictionary.items()}
@@ -94,6 +94,9 @@ class QueueItemHandler:
94
94
  """
95
95
  A class used for handling the display of records in custom process queues, which are controlled in the system by
96
96
  ProcessQueueItem data types.
97
+
98
+ IMPORTANT NOTICE: This is only for custom processes that make use of ProcessQueueItem records. For experiment
99
+ processes that use AssignedProcess records, see the ProcessTracking class.
97
100
  """
98
101
  user: SapioUser
99
102
  context: ProcessQueueContext | None
@@ -105,7 +108,8 @@ class QueueItemHandler:
105
108
  """
106
109
  self.user = AliasUtil.to_sapio_user(context)
107
110
  self.rec_handler = RecordHandler(self.user)
108
- if isinstance(context, SapioWebhookContext):
111
+ # PR-47565: Only initialize a ProcessQueueContext if the given context object has context_data.
112
+ if isinstance(context, SapioWebhookContext) and context.context_data:
109
113
  self.context = ProcessQueueContext(context)
110
114
  else:
111
115
  self.context = None
@@ -260,6 +264,9 @@ class QueueItemHandler:
260
264
  Given a list of records, create process queue item records for them at the provided process and step names.
261
265
  You must store and commit using the record model manager in order for these changes to take effect.
262
266
 
267
+ IMPORTANT NOTICE: This is only for custom processes that make use of ProcessQueueItem records. For experiment
268
+ processes that use AssignedProcess records, see the ProcessTracking class.
269
+
263
270
  :param records: The records to create process queue items for.
264
271
  :param process: The name of the process to queue for.
265
272
  :param step: The name of the step in the above process to queue for. This is the "Workflow Name" field of the
@@ -292,6 +299,9 @@ class QueueItemHandler:
292
299
  criteria and remove them from the queue by setting the ShowInQueue field on the process queue items to false.
293
300
  You must store and commit using the record model manager in order for these changes to take effect.
294
301
 
302
+ IMPORTANT NOTICE: This is only for custom processes that make use of ProcessQueueItem records. For experiment
303
+ processes that use AssignedProcess records, see the ProcessTracking class.
304
+
295
305
  :param records: The records to remove from the queue.
296
306
  :param wrapper: The record model wrapper for the process queue items being updated. If not provided, the
297
307
  returned records will be PyRecordModels instead of WrappedRecordModels.
@@ -5,6 +5,12 @@ from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, Experime
5
5
 
6
6
 
7
7
  class ProcessTracking:
8
+ """
9
+ A class for calling the Foundations process tracking endpoints.
10
+
11
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
12
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
13
+ """
8
14
  @staticmethod
9
15
  def assign_to_process(context: UserIdentifier, data_type: DataTypeIdentifier, records: list[RecordIdentifier],
10
16
  process_name: str, step_number: int | None = None, branch_id: int | None = None,
@@ -14,6 +20,9 @@ class ProcessTracking:
14
20
  at that step.
15
21
  Synonymous with what occurs during request creation or when using the assign to process button.
16
22
 
23
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
24
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
25
+
17
26
  :param context: The current webhook context or a user object to send requests from.
18
27
  :param data_type: The data type of the tracked records.
19
28
  :param records: A list of the tracked records.
@@ -46,6 +55,9 @@ class ProcessTracking:
46
55
  tracked records from "Ready for -" to "In Process -" for their current step.
47
56
  Synonymous with what occurs when starting a process step in the system.
48
57
 
58
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
59
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
60
+
49
61
  :param context: The current webhook context or a user object to send requests from.
50
62
  :param data_type: The data type of the tracked records.
51
63
  :param records: A list of the tracked records.
@@ -73,6 +85,9 @@ class ProcessTracking:
73
85
  Moves the assigned process down to the descendant sample(s) if both samples are provided in the records list.
74
86
  Synonymous with what occurs when completing an experiment in the system.
75
87
 
88
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
89
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
90
+
76
91
  :param context: The current webhook context or a user object to send requests from.
77
92
  :param data_type: The data type of the tracked records.
78
93
  :param records: A list of the tracked records.
@@ -96,6 +111,9 @@ class ProcessTracking:
96
111
  records must be "In Process -" for the given step.
97
112
  Synonymous with what occurs when failing a sample due to a QC failure in a process in the system.
98
113
 
114
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
115
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
116
+
99
117
  :param context: The current webhook context or a user object to send requests from.
100
118
  :param data_type: The data type of the tracked records.
101
119
  :param records: A list of the tracked records.
@@ -122,6 +140,9 @@ class ProcessTracking:
122
140
  will move their status to "Ready for -" for the next step in the process, or "Completed -" if this was the
123
141
  last step.
124
142
 
143
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
144
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
145
+
125
146
  :param context: The current webhook context or a user object to send requests from.
126
147
  :param data_type: The data type of the tracked records.
127
148
  :param records: A list of the tracked records.
@@ -149,6 +170,9 @@ class ProcessTracking:
149
170
  step will move their status to "Ready for -" for the next step in the process, or "Completed -" if this was the
150
171
  last step.
151
172
 
173
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
174
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
175
+
152
176
  :param context: The current webhook context or a user object to send requests from.
153
177
  :param data_type: The data type of the tracked records.
154
178
  :param records: A list of the tracked records.
@@ -180,6 +204,9 @@ class ProcessTracking:
180
204
  Synonymous with what occurs when reprocessing records to a previous step due to QC failures in a process in the
181
205
  system.
182
206
 
207
+ IMPORTANT NOTICE: This is only for experiment processes that make use of AssignedProcess records. For custom
208
+ processes that use ProcessQueueItem records, see the QueueItemHandler class.
209
+
183
210
  :param context: The current webhook context or a user object to send requests from.
184
211
  :param records: A list of ReturnPoint records to reprocess to.
185
212
  """