sapiopycommons 2024.10.14a341__py3-none-any.whl → 2024.10.25a345__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.

@@ -231,7 +231,7 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers=False,
231
231
  indigo_mol.aromatize()
232
232
  if enhanced_stereo:
233
233
  # Remove enhanced stereo layer when generating InChI as the stereo hash is generated separately for reg.
234
- mol_copy: Mol = Chem.MolFromMolBlock(Chem.MolToMolBlock(mol))
234
+ mol_copy: Mol = Chem.Mol(mol)
235
235
  Chem.CanonicalizeEnhancedStereo(mol_copy)
236
236
  molecule["inchi"] = Chem.MolToInchi(mol_copy)
237
237
  molecule["inchiKey"] = Chem.MolToInchiKey(mol_copy)
@@ -52,9 +52,9 @@ class ColumnBuilder:
52
52
  :param field: The field name to return the type of.
53
53
  :return: The field type of the given field name.
54
54
  """
55
- # Check if the field name is a system field. If it us, use the field type defined in this file.
55
+ # Check if the field name is a system field. If it is, use the field type defined in this file.
56
56
  field_name: str = AliasUtil.to_data_field_name(field)
57
57
  if field_name in SYSTEM_FIELDS:
58
58
  return SYSTEM_FIELDS.get(field_name)
59
59
  # Otherwise, check if the field type can be found from the wrapper.
60
- return AliasUtil.to_field_type(field_name, data_type)
60
+ return AliasUtil.to_field_type(field, data_type)
@@ -0,0 +1,440 @@
1
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
2
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrapperField
3
+
4
+
5
+ class ActiveTaskPseudoDef:
6
+ DATA_TYPE_NAME: str = "ActiveTask"
7
+ ACTIVE_TASK_ID__FIELD_NAME = WrapperField("ActiveTaskId", FieldType.LONG)
8
+ ACTIVE_WORKFLOW_ID__FIELD_NAME = WrapperField("ActiveWorkflowId", FieldType.LONG)
9
+ DATE_EDITED__FIELD_NAME = WrapperField("DateEdited", FieldType.DATE)
10
+ EDITED_BY__FIELD_NAME = WrapperField("EditedBy", FieldType.STRING)
11
+ STATUS__FIELD_NAME = WrapperField("Status", FieldType.ENUM)
12
+ TASK_USAGE_ID__FIELD_NAME = WrapperField("TaskUsageId", FieldType.LONG)
13
+
14
+
15
+ class ActiveWorkflowPseudoDef:
16
+ DATA_TYPE_NAME: str = "ActiveWorkflow"
17
+ ACTIVE_WORKFLOW_ID__FIELD_NAME = WrapperField("ActiveWorkflowId", FieldType.LONG)
18
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
19
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.DATE)
20
+ DATE_EDITED__FIELD_NAME = WrapperField("DateEdited", FieldType.STRING)
21
+ EDITED_BY__FIELD_NAME = WrapperField("EditedBy", FieldType.STRING)
22
+ ESTIMATED_ATTACHMENTS__FIELD_NAME = WrapperField("EstimatedAttachments", FieldType.LONG)
23
+ NAME__FIELD_NAME = WrapperField("Name", FieldType.STRING)
24
+ RELATED_RECORD_ID__FIELD_NAME = WrapperField("RelatedRecordId", FieldType.LONG)
25
+ STATUS__FIELD_NAME = WrapperField("Status", FieldType.ENUM)
26
+ WORKFLOW_ID__FIELD_NAME = WrapperField("WorkflowId", FieldType.LONG)
27
+
28
+
29
+ class AuditLogPseudoDef:
30
+ DATA_TYPE_NAME: str = "AuditLog"
31
+ DATA_FIELD_NAME__FIELD_NAME = WrapperField("DataFieldName", FieldType.STRING)
32
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("DataTypeName", FieldType.STRING)
33
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
34
+ EVENT_TYPE__FIELD_NAME = WrapperField("EventType", FieldType.ENUM)
35
+ FULL_NAME__FIELD_NAME = WrapperField("FullName", FieldType.STRING)
36
+ NEW_VALUE__FIELD_NAME = WrapperField("NewValue", FieldType.STRING)
37
+ ORIGINAL_VALUE__FIELD_NAME = WrapperField("OriginalValue", FieldType.STRING)
38
+ RECORD_ID__FIELD_NAME = WrapperField("RecordId", FieldType.LONG)
39
+ RECORD_NAME__FIELD_NAME = WrapperField("RecordName", FieldType.STRING)
40
+ TIME_STAMP__FIELD_NAME = WrapperField("TimeStamp", FieldType.DATE)
41
+ USER_COMMENT__FIELD_NAME = WrapperField("UserComment", FieldType.STRING)
42
+ USER_NAME__FIELD_NAME = WrapperField("UserName", FieldType.STRING)
43
+
44
+
45
+ class DataFieldDefinitionPseudoDef:
46
+ DATA_TYPE_NAME: str = "DataFieldDefinition"
47
+ APPROVE_EDIT__FIELD_NAME = WrapperField("ApproveEdit", FieldType.BOOLEAN)
48
+ AUTO_CLEAR_FIELD_LIST__FIELD_NAME = WrapperField("AutoClearFieldList", FieldType.STRING)
49
+ AUTO_SORT__FIELD_NAME = WrapperField("AutoSort", FieldType.BOOLEAN)
50
+ COLOR_MAPPING_ID__FIELD_NAME = WrapperField("ColorMappingId", FieldType.LONG)
51
+ DATA_FIELD_NAME__FIELD_NAME = WrapperField("DataFieldName", FieldType.STRING)
52
+ DATA_FIELD_TAG__FIELD_NAME = WrapperField("DataFieldTag", FieldType.STRING)
53
+ DATA_FIELD_TYPE__FIELD_NAME = WrapperField("DataFieldType", FieldType.STRING)
54
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("DataTypeName", FieldType.STRING)
55
+ DECIMAL_DIGITS__FIELD_NAME = WrapperField("DecimalDigits", FieldType.INTEGER)
56
+ DEFAULT_VALUE__FIELD_NAME = WrapperField("DefaultValue", FieldType.STRING)
57
+ DEPENDENT_FIELDS__FIELD_NAME = WrapperField("Dependent_Fields", FieldType.STRING)
58
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
59
+ DIRECT_EDIT__FIELD_NAME = WrapperField("DirectEdit", FieldType.BOOLEAN)
60
+ DISPLAY_NAME__FIELD_NAME = WrapperField("DisplayName", FieldType.STRING)
61
+ EDITABLE__FIELD_NAME = WrapperField("Editable", FieldType.BOOLEAN)
62
+ ENUM_VALUES__FIELD_NAME = WrapperField("EnumValues", FieldType.STRING)
63
+ FORM_COL__FIELD_NAME = WrapperField("FormCol", FieldType.SHORT)
64
+ FORM_COL_SPAN__FIELD_NAME = WrapperField("FormColSpan", FieldType.SHORT)
65
+ FORM_NAME__FIELD_NAME = WrapperField("FormName", FieldType.STRING)
66
+ HTML_EDITOR__FIELD_NAME = WrapperField("HtmlEditor", FieldType.BOOLEAN)
67
+ IDENTIFIER__FIELD_NAME = WrapperField("Identifier", FieldType.BOOLEAN)
68
+ INDEX_FOR_SEARCH__FIELD_NAME = WrapperField("IndexForSearch", FieldType.BOOLEAN)
69
+ LINK_OUT__FIELD_NAME = WrapperField("LinkOut", FieldType.BOOLEAN)
70
+ LINK_OUT_URL__FIELD_NAME = WrapperField("LinkOutUrl", FieldType.BOOLEAN)
71
+ MAX_LENGTH__FIELD_NAME = WrapperField("MaxLength", FieldType.INTEGER)
72
+ MAXIMUM_VALUE__FIELD_NAME = WrapperField("MaximumValue", FieldType.DOUBLE)
73
+ MINIMUM_VALUE__FIELD_NAME = WrapperField("MinimumValue", FieldType.DOUBLE)
74
+ MULT_SELECT__FIELD_NAME = WrapperField("MultSelect", FieldType.BOOLEAN)
75
+ NUM_LINES__FIELD_NAME = WrapperField("NumLines", FieldType.INTEGER)
76
+ REQUIRED__FIELD_NAME = WrapperField("Required", FieldType.BOOLEAN)
77
+ SORT_KEY__FIELD_NAME = WrapperField("SortKey", FieldType.BOOLEAN)
78
+ STATIC_DATE__FIELD_NAME = WrapperField("StaticDate", FieldType.BOOLEAN)
79
+ UNIQUE_VALUE__FIELD_NAME = WrapperField("UniqueValue", FieldType.BOOLEAN)
80
+ VISIBLE__FIELD_NAME = WrapperField("Visible", FieldType.BOOLEAN)
81
+ WORKFLOW_ONLY_EDITING__FIELD_NAME = WrapperField("WorkflowOnlyEditing", FieldType.BOOLEAN)
82
+
83
+
84
+ class DataTypeDefinitionPseudoDef:
85
+ DATA_TYPE_NAME: str = "DataTypeDefinition"
86
+ ADDABLE__FIELD_NAME = WrapperField("Addable", FieldType.BOOLEAN)
87
+ ATTACHMENT__FIELD_NAME = WrapperField("Attachment", FieldType.BOOLEAN)
88
+ ATTACHMENT_TYPE__FIELD_NAME = WrapperField("AttachmentType", FieldType.STRING)
89
+ DATA_TYPE_TAG__FIELD_NAME = WrapperField("DATA_TYPE_TAG", FieldType.STRING)
90
+ DATA_TYPE_ID__FIELD_NAME = WrapperField("DataTypeId", FieldType.LONG)
91
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("DataTypeName", FieldType.STRING)
92
+ DELETABLE__FIELD_NAME = WrapperField("Deletable", FieldType.BOOLEAN)
93
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
94
+ DISPLAY_NAME__FIELD_NAME = WrapperField("DisplayName", FieldType.STRING)
95
+ EXTENSION_TYPE__FIELD_NAME = WrapperField("ExtensionType", FieldType.BOOLEAN)
96
+ GROUP_ADDABLE__FIELD_NAME = WrapperField("GroupAddable", FieldType.BOOLEAN)
97
+ HIDE_DATA_RECORDS__FIELD_NAME = WrapperField("HideDataRecords", FieldType.BOOLEAN)
98
+ HIGH_VOLUME__FIELD_NAME = WrapperField("HighVolume", FieldType.BOOLEAN)
99
+ IS_HIDE_IN_MOBILE__FIELD_NAME = WrapperField("IS_HIDE_IN_MOBILE", FieldType.BOOLEAN)
100
+ IS_HVDT_ON_SAVE_ENABLED__FIELD_NAME = WrapperField("IS_HVDT_ON_SAVE_ENABLED", FieldType.BOOLEAN)
101
+ IS_PUBLIC_ATTACHMENT__FIELD_NAME = WrapperField("IS_PUBLIC_ATTACHMENT", FieldType.BOOLEAN)
102
+ ICON_COLOR__FIELD_NAME = WrapperField("IconColor", FieldType.STRING)
103
+ ICON_NAME__FIELD_NAME = WrapperField("IconName", FieldType.STRING)
104
+ IMPORTABLE__FIELD_NAME = WrapperField("Importable", FieldType.BOOLEAN)
105
+ IS_ACTIVE__FIELD_NAME = WrapperField("IsActive", FieldType.BOOLEAN)
106
+ IS_HIDDEN__FIELD_NAME = WrapperField("IsHidden", FieldType.BOOLEAN)
107
+ MAX_TABLE_ROW_COUNT__FIELD_NAME = WrapperField("MaxTableRowCount", FieldType.LONG)
108
+ PLURAL_DISPLAY_NAME__FIELD_NAME = WrapperField("PluralDisplayName", FieldType.STRING)
109
+ RECORD_ASSIGNABLE__FIELD_NAME = WrapperField("RecordAssignable", FieldType.BOOLEAN)
110
+ RECORD_IMAGE_ASSIGNABLE__FIELD_NAME = WrapperField("RecordImageAssignable", FieldType.BOOLEAN)
111
+ RECORD_IMAGE_MANUALLY_ADDABLE__FIELD_NAME = WrapperField("RecordImageManuallyAddable", FieldType.BOOLEAN)
112
+ REMOVABLE__FIELD_NAME = WrapperField("Removable", FieldType.BOOLEAN)
113
+ RESTRICTED__FIELD_NAME = WrapperField("Restricted", FieldType.BOOLEAN)
114
+ SHOW_ON_HOME_SCREEN__FIELD_NAME = WrapperField("ShowOnHomeScreen", FieldType.BOOLEAN)
115
+ SHOW_SUB_TABLES__FIELD_NAME = WrapperField("ShowSubTables", FieldType.BOOLEAN)
116
+ SHOW_TABS__FIELD_NAME = WrapperField("ShowTabs", FieldType.BOOLEAN)
117
+ SINGLE_PARENT__FIELD_NAME = WrapperField("SingleParent", FieldType.BOOLEAN)
118
+ UNDER_CONTAINER__FIELD_NAME = WrapperField("UnderContainer", FieldType.BOOLEAN)
119
+
120
+
121
+ class EnbDataTypeDefinitionPseudoDef:
122
+ DATA_TYPE_NAME: str = "EnbDataTypeDefinition"
123
+ DATA_TYPE_ID__FIELD_NAME = WrapperField("DataTypeId", FieldType.LONG)
124
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("DataTypeName", FieldType.STRING)
125
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
126
+ DISPLAY_NAME__FIELD_NAME = WrapperField("DisplayName", FieldType.STRING)
127
+ ENB_DATA_TYPE_NAME__FIELD_NAME = WrapperField("EnbDataTypeName", FieldType.STRING)
128
+ ICON_COLOR__FIELD_NAME = WrapperField("IconColor", FieldType.STRING)
129
+ ICON_NAME__FIELD_NAME = WrapperField("IconName", FieldType.STRING)
130
+ NOTEBOOK_EXPERIMENT_ID__FIELD_NAME = WrapperField("Notebook_Experiment_ID", FieldType.LONG)
131
+ PLURAL_DISPLAY_NAME__FIELD_NAME = WrapperField("PluralDisplayName", FieldType.STRING)
132
+
133
+
134
+ class EnbEntryPseudoDef:
135
+ DATA_TYPE_NAME: str = "EnbEntry"
136
+ APPROVAL_DUE_DATE__FIELD_NAME = WrapperField("ApprovalDueDate", FieldType.DATE)
137
+ COLUMN_ORDER__FIELD_NAME = WrapperField("ColumnOrder", FieldType.INTEGER)
138
+ COLUMN_SPAN__FIELD_NAME = WrapperField("ColumnSpan", FieldType.INTEGER)
139
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
140
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("DataTypeName", FieldType.STRING)
141
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.DATE)
142
+ DEPENDENT_ENTRY_ID_LIST__FIELD_NAME = WrapperField("DependentEntryIdList", FieldType.STRING)
143
+ ENTRY_DESCRIPTION__FIELD_NAME = WrapperField("EntryDescription", FieldType.STRING)
144
+ ENTRY_HEIGHT__FIELD_NAME = WrapperField("EntryHeight", FieldType.INTEGER)
145
+ ENTRY_ID__FIELD_NAME = WrapperField("EntryId", FieldType.LONG)
146
+ ENTRY_NAME__FIELD_NAME = WrapperField("EntryName", FieldType.STRING)
147
+ ENTRY_ORDER__FIELD_NAME = WrapperField("EntryOrder", FieldType.INTEGER)
148
+ ENTRY_REQUIRES_E_SIGN__FIELD_NAME = WrapperField("EntryRequiresESign", FieldType.BOOLEAN)
149
+ ENTRY_SINGLETON_ID__FIELD_NAME = WrapperField("EntrySingletonId", FieldType.STRING)
150
+ ENTRY_STATUS__FIELD_NAME = WrapperField("EntryStatus", FieldType.STRING)
151
+ ENTRY_TYPE__FIELD_NAME = WrapperField("EntryType", FieldType.STRING)
152
+ EXPERIMENT_ID__FIELD_NAME = WrapperField("ExperimentId", FieldType.LONG)
153
+ HAS_COMMENTS__FIELD_NAME = WrapperField("HasComments", FieldType.BOOLEAN)
154
+ IS_CREATED_FROM_TEMPLATE__FIELD_NAME = WrapperField("IsCreatedFromTemplate", FieldType.BOOLEAN)
155
+ IS_REQUIRED_ENTRY__FIELD_NAME = WrapperField("IsRequiredEntry", FieldType.BOOLEAN)
156
+ IS_SHOWN_IN_TEMPLATE__FIELD_NAME = WrapperField("IsShownInTemplate", FieldType.BOOLEAN)
157
+ LAST_MODIFIED_BY__FIELD_NAME = WrapperField("LastModifiedBy", FieldType.STRING)
158
+ LAST_MODIFIED_DATE__FIELD_NAME = WrapperField("LastModifiedDate", FieldType.DATE)
159
+ RELATED_ENTRY_ID_LIST__FIELD_NAME = WrapperField("RelatedEntryIdList", FieldType.STRING)
160
+ REQUIRES_GRABBER_PLUGIN__FIELD_NAME = WrapperField("RequriesGrabberPlugin", FieldType.BOOLEAN)
161
+ SOURCE_ENTRY_ID__FIELD_NAME = WrapperField("SourceEntryId", FieldType.LONG)
162
+ SUBMITTED_BY__FIELD_NAME = WrapperField("SubmittedBy", FieldType.STRING)
163
+ SUBMITTED_DATE__FIELD_NAME = WrapperField("SubmittedDate", FieldType.DATE)
164
+ TAB_ID__FIELD_NAME = WrapperField("TabId", FieldType.LONG)
165
+ TEMPLATE_ITEM_FULFILLED_TIME_STAMP__FIELD_NAME = WrapperField("TemplateItemFulfilledTimeStamp", FieldType.LONG)
166
+
167
+
168
+ class EnbEntryOptionsPseudoDef:
169
+ DATA_TYPE_NAME: str = "EnbEntryOptions"
170
+ ENTRY_ID__FIELD_NAME = WrapperField("EntryId", FieldType.LONG)
171
+ ENTRY_OPTION_VALUE__FIELD_NAME = WrapperField("EntryOptionValue", FieldType.STRING)
172
+ ENTRY_OPTION_KEY__FIELD_NAME = WrapperField("EntryOptionkey", FieldType.STRING)
173
+
174
+
175
+ class ExperimentEntryRecordPseudoDef:
176
+ DATA_TYPE_NAME: str = "ExperimentEntryRecord"
177
+ ENTRY_ID__FIELD_NAME = WrapperField("EntryId", FieldType.LONG)
178
+ RECORD_ID__FIELD_NAME = WrapperField("RecordId", FieldType.LONG)
179
+
180
+
181
+ class LimsUserPseudoDef:
182
+ DATA_TYPE_NAME: str = "LimsUser"
183
+ API_USER__FIELD_NAME = WrapperField("ApiUser", FieldType.BOOLEAN)
184
+ EMAIL_ADDRESS__FIELD_NAME = WrapperField("EmailAddress", FieldType.STRING)
185
+ FIRST_NAME__FIELD_NAME = WrapperField("FirstName", FieldType.STRING)
186
+ LAST_NAME__FIELD_NAME = WrapperField("LastName", FieldType.STRING)
187
+ MIDDLE_NAME__FIELD_NAME = WrapperField("MiddleName", FieldType.STRING)
188
+ PWD_EXPIRE_DATE__FIELD_NAME = WrapperField("PwdExpireDate", FieldType.DATE)
189
+ PWD_EXPIRE_INTERVAL__FIELD_NAME = WrapperField("PwdExpireInterval", FieldType.INTEGER)
190
+ USER_NAME__FIELD_NAME = WrapperField("UserName", FieldType.STRING)
191
+
192
+
193
+ class NotebookExperimentPseudoDef:
194
+ DATA_TYPE_NAME: str = "NotebookExperiment"
195
+ ACCESS_LEVEL__FIELD_NAME = WrapperField("AccessLevel", FieldType.STRING)
196
+ APPROVAL_DUE_DATE__FIELD_NAME = WrapperField("ApprovalDueDate", FieldType.DATE)
197
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
198
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.DATE)
199
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
200
+ EXPERIMENT_ID__FIELD_NAME = WrapperField("ExperimentId", FieldType.LONG)
201
+ EXPERIMENT_NAME__FIELD_NAME = WrapperField("ExperimentName", FieldType.STRING)
202
+ EXPERIMENT_OWNER__FIELD_NAME = WrapperField("ExperimentOwner", FieldType.STRING)
203
+ EXPERIMENT_RECORD_ID__FIELD_NAME = WrapperField("ExperimentRecordId", FieldType.LONG)
204
+ EXPERIMENT_TYPE_NAME__FIELD_NAME = WrapperField("ExperimentTypeName", FieldType.STRING)
205
+ IS_ACTIVE__FIELD_NAME = WrapperField("IsActive", FieldType.BOOLEAN)
206
+ IS_MODIFIABLE__FIELD_NAME = WrapperField("IsModifiable", FieldType.BOOLEAN)
207
+ IS_TEMPLATE__FIELD_NAME = WrapperField("IsTemplate", FieldType.BOOLEAN)
208
+ IS_PROTOCOL_TEMPLATE__FIELD_NAME = WrapperField("Is_Protocol_Template", FieldType.BOOLEAN)
209
+ LAST_MODIFIED_BY__FIELD_NAME = WrapperField("LastModifiedBy", FieldType.STRING)
210
+ LAST_MODIFIED_DATE__FIELD_NAME = WrapperField("LastModifiedDate", FieldType.DATE)
211
+ SOURCE_TEMPLATE_ID__FIELD_NAME = WrapperField("SourceTemplateId", FieldType.LONG)
212
+ STATUS__FIELD_NAME = WrapperField("Status", FieldType.STRING)
213
+
214
+
215
+ class NotebookExperimentOptionPseudoDef:
216
+ DATA_TYPE_NAME: str = "NotebookExperimentOption"
217
+ EXPERIMENT_ID__FIELD_NAME = WrapperField("ExperimentId", FieldType.LONG)
218
+ OPTION_KEY__FIELD_NAME = WrapperField("OptionKey", FieldType.STRING)
219
+ OPTION_VALUE__FIELD_NAME = WrapperField("OptionValue", FieldType.STRING)
220
+
221
+
222
+ class SystemLogPseudoDef:
223
+ DATA_TYPE_NAME: str = "SystemLog"
224
+ DATA_FIELD_NAME__FIELD_NAME = WrapperField("DataFieldName", FieldType.STRING)
225
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("DataTypeName", FieldType.STRING)
226
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
227
+ EVENT_ID__FIELD_NAME = WrapperField("EventId", FieldType.LONG)
228
+ EVENT_TYPE__FIELD_NAME = WrapperField("EventType", FieldType.STRING)
229
+ FULL_NAME__FIELD_NAME = WrapperField("FullName", FieldType.STRING)
230
+ NEW_VALUE__FIELD_NAME = WrapperField("NewValue", FieldType.STRING)
231
+ ORIGINAL_VALUE__FIELD_NAME = WrapperField("OriginalValue", FieldType.STRING)
232
+ RECORD_ID__FIELD_NAME = WrapperField("RecordId", FieldType.LONG)
233
+ RECORD_NAME__FIELD_NAME = WrapperField("RecordName", FieldType.STRING)
234
+ TIMESTAMP__FIELD_NAME = WrapperField("Timestamp", FieldType.DATE)
235
+ USER_COMMENT__FIELD_NAME = WrapperField("UserComment", FieldType.STRING)
236
+ USERNAME__FIELD_NAME = WrapperField("Username", FieldType.STRING)
237
+
238
+
239
+ class SystemObjectChangeLogPseudoDef:
240
+ DATA_TYPE_NAME: str = "System_Object_Change_Log"
241
+ ALT_ID__FIELD_NAME = WrapperField("Alt_Id", FieldType.STRING)
242
+ ATTRIBUTE_NAME__FIELD_NAME = WrapperField("Attribute_Name", FieldType.STRING)
243
+ CHANGE_TYPE__FIELD_NAME = WrapperField("Change_Type", FieldType.STRING)
244
+ DATA_FIELD_NAME__FIELD_NAME = WrapperField("Data_Field_Name", FieldType.STRING)
245
+ DATA_TYPE_NAME__FIELD_NAME = WrapperField("Data_Type_Name", FieldType.STRING)
246
+ EVENT_ID__FIELD_NAME = WrapperField("Event_Id", FieldType.STRING)
247
+ NEW_VALUE__FIELD_NAME = WrapperField("New_Value", FieldType.STRING)
248
+ OBJECT_ID__FIELD_NAME = WrapperField("Object_Id", FieldType.STRING)
249
+ OBJECT_TYPE__FIELD_NAME = WrapperField("Object_Type", FieldType.STRING)
250
+ OLD_VALUE__FIELD_NAME = WrapperField("Old_Value", FieldType.STRING)
251
+ TIMESTAMP__FIELD_NAME = WrapperField("Timestamp", FieldType.DATE)
252
+ USERNAME__FIELD_NAME = WrapperField("Username", FieldType.STRING)
253
+
254
+
255
+ class TaskPseudoDef:
256
+ DATA_TYPE_NAME: str = "Task"
257
+ ATTACHMENT_REQUIRED__FIELD_NAME = WrapperField("AttachmentRequired", FieldType.BOOLEAN)
258
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
259
+ CUSTOM_ACTION__FIELD_NAME = WrapperField("CustomAction", FieldType.STRING)
260
+ DATA_TYPE_NAME_LIST__FIELD_NAME = WrapperField("DataTypeNameList", FieldType.STRING)
261
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.DATE)
262
+ DATE_EDITED__FIELD_NAME = WrapperField("DateEdited", FieldType.DATE)
263
+ DISPLAY_TYPE__FIELD_NAME = WrapperField("DisplayType", FieldType.ENUM)
264
+ EDITED_BY__FIELD_NAME = WrapperField("EditedBy", FieldType.STRING)
265
+ INPUT_DATA_TYPE_NAME__FIELD_NAME = WrapperField("InputDataTypeName", FieldType.STRING)
266
+ IS_TEMPLATE__FIELD_NAME = WrapperField("IsTemplate", FieldType.BOOLEAN)
267
+ LONG_DESC__FIELD_NAME = WrapperField("LongDesc", FieldType.STRING)
268
+ MENU_TASK_ID__FIELD_NAME = WrapperField("MenuTaskId", FieldType.ENUM)
269
+ NAME__FIELD_NAME = WrapperField("Name", FieldType.STRING)
270
+ SHORT_DESC__FIELD_NAME = WrapperField("ShortDesc", FieldType.STRING)
271
+ TASK_ID__FIELD_NAME = WrapperField("TaskId", FieldType.LONG)
272
+ TASK_VERSION__FIELD_NAME = WrapperField("TaskVersion", FieldType.LONG)
273
+ TEMPLATE_TASK_ID__FIELD_NAME = WrapperField("TemplateTaskId", FieldType.LONG)
274
+ TYPE__FIELD_NAME = WrapperField("Type", FieldType.ENUM)
275
+
276
+
277
+ class TaskAttachmentPseudoDef:
278
+ DATA_TYPE_NAME: str = "TaskAttachment"
279
+ ACTIVE_TASK_ID__FIELD_NAME = WrapperField("ActiveTaskId", FieldType.LONG)
280
+ RECORD_ID__FIELD_NAME = WrapperField("RecordId", FieldType.LONG)
281
+
282
+
283
+ class TaskOptionPseudoDef:
284
+ DATA_TYPE_NAME: str = "TaskOption"
285
+ OPTION_KEY__FIELD_NAME = WrapperField("OptionKey", FieldType.STRING)
286
+ OPTION_VALUE__FIELD_NAME = WrapperField("OptionValue", FieldType.STRING)
287
+ TASK_ID__FIELD_NAME = WrapperField("TaskId", FieldType.LONG)
288
+
289
+
290
+ class TaskUsagePseudoDef:
291
+ DATA_TYPE_NAME: str = "TaskUsage"
292
+ FORCE_ATTACH__FIELD_NAME = WrapperField("ForceAttach", FieldType.BOOLEAN)
293
+ IS_TEMPLATE__FIELD_NAME = WrapperField("IsTemplate", FieldType.BOOLEAN)
294
+ TASK_ID__FIELD_NAME = WrapperField("TaskId", FieldType.LONG)
295
+ TASK_ORDER__FIELD_NAME = WrapperField("TaskOrder", FieldType.INTEGER)
296
+ TASK_USAGE_ID__FIELD_NAME = WrapperField("TaskUsageId", FieldType.LONG)
297
+ WORKFLOW_ID__FIELD_NAME = WrapperField("WorkflowId", FieldType.LONG)
298
+
299
+
300
+ class VeloxWebhookPseudoDef:
301
+ DATA_TYPE_NAME: str = "VELOXWEBHOOK"
302
+ CUSTOM_PLUGIN_POINT__FIELD_NAME = WrapperField("CUSTOM_PLUGIN_POINT", FieldType.STRING)
303
+ DATA_TYPE_NAME_SET__FIELD_NAME = WrapperField("DATA_TYPE_NAME_SET", FieldType.STRING)
304
+ DESCRIPTION__FIELD_NAME = WrapperField("DESCRIPTION", FieldType.STRING)
305
+ ENB_ENTRY_TYPE__FIELD_NAME = WrapperField("ENB_ENTRY_TYPE", FieldType.STRING)
306
+ EXPERIMENT_ENTRY_NAME__FIELD_NAME = WrapperField("EXPERIMENT_ENTRY_NAME", FieldType.STRING)
307
+ GUID__FIELD_NAME = WrapperField("GUID", FieldType.STRING)
308
+ ICON_COLOR__FIELD_NAME = WrapperField("ICON_COLOR", FieldType.STRING)
309
+ ICON_GUID__FIELD_NAME = WrapperField("ICON_GUID", FieldType.STRING)
310
+ IS_RETRY_ON_FAILURE__FIELD_NAME = WrapperField("IS_RETRY_ON_FAILURE", FieldType.BOOLEAN)
311
+ IS_TRANSACTIONAL__FIELD_NAME = WrapperField("IS_TRANSACTIONAL", FieldType.BOOLEAN)
312
+ LINE_1_TEXT__FIELD_NAME = WrapperField("LINE_1_TEXT", FieldType.STRING)
313
+ LINE_2_TEXT__FIELD_NAME = WrapperField("LINE_2_TEXT", FieldType.STRING)
314
+ PLUGIN_ORDER__FIELD_NAME = WrapperField("PLUGIN_ORDER", FieldType.INTEGER)
315
+ PLUGIN_POINT__FIELD_NAME = WrapperField("PLUGIN_POINT", FieldType.STRING)
316
+ SECTION_NAME_PATH__FIELD_NAME = WrapperField("SECTION_NAME_PATH", FieldType.STRING)
317
+ TEMPLATE_NAME__FIELD_NAME = WrapperField("TEMPLATE_NAME", FieldType.STRING)
318
+ WEBHOOK_URL__FIELD_NAME = WrapperField("WEBHOOK_URL", FieldType.STRING)
319
+
320
+
321
+ class VeloxWebhookExecutionPseudoDef:
322
+ DATA_TYPE_NAME: str = "VELOXWEBHOOK_EXECUTION"
323
+ EXECUTION_TIMESTAMP__FIELD_NAME = WrapperField("EXECUTION_TIMESTAMP", FieldType.DATE)
324
+ EXECUTION_USERNAME__FIELD_NAME = WrapperField("EXECUTION_USERNAME", FieldType.STRING)
325
+ GUID__FIELD_NAME = WrapperField("GUID", FieldType.STRING)
326
+ LAST_ATTEMPT_NUMBER__FIELD_NAME = WrapperField("LAST_ATTEMPT_NUMBER", FieldType.INTEGER)
327
+ LAST_ATTEMPT_RESULT__FIELD_NAME = WrapperField("LAST_ATTEMPT_RESULT", FieldType.STRING)
328
+ REQUEST_BODY__FIELD_NAME = WrapperField("REQUEST_BODY", FieldType.STRING)
329
+ WEBHOOK_GUID__FIELD_NAME = WrapperField("WEBHOOK_GUID", FieldType.STRING)
330
+ WEBHOOK_URL__FIELD_NAME = WrapperField("WEBHOOK_URL", FieldType.STRING)
331
+
332
+
333
+ class VeloxWebhookExecutionAttemptPseudoDef:
334
+ DATA_TYPE_NAME: str = "VELOXWEBHOOK_EXECUTION_ATTEMPT"
335
+ ATTEMPT_DURATION__FIELD_NAME = WrapperField("ATTEMPT_DURATION", FieldType.INTEGER)
336
+ ATTEMPT_NUMBER__FIELD_NAME = WrapperField("ATTEMPT_NUMBER", FieldType.INTEGER)
337
+ ATTEMPT_RESULT__FIELD_NAME = WrapperField("ATTEMPT_RESULT", FieldType.STRING)
338
+ ATTEMPT_TIMESTAMP__FIELD_NAME = WrapperField("ATTEMPT_TIMESTAMP", FieldType.DATE)
339
+ EXECUTION_GUID__FIELD_NAME = WrapperField("EXECUTION_GUID", FieldType.STRING)
340
+ GUID__FIELD_NAME = WrapperField("GUID", FieldType.STRING)
341
+ RESPONSE_BODY__FIELD_NAME = WrapperField("RESPONSE_BODY", FieldType.STRING)
342
+ RESPONSE_CODE__FIELD_NAME = WrapperField("RESPONSE_CODE", FieldType.INTEGER)
343
+ WEBHOOK_GUID__FIELD_NAME = WrapperField("WEBHOOK_GUID", FieldType.STRING)
344
+ WEBHOOK_URL__FIELD_NAME = WrapperField("WEBHOOK_URL", FieldType.STRING)
345
+
346
+
347
+ class VeloxWebhookExecutionLogPseudoDef:
348
+ DATA_TYPE_NAME: str = "VELOXWEBHOOK_EXECUTION_LOG"
349
+ ATTEMPT_GUID__FIELD_NAME = WrapperField("ATTEMPT_GUID", FieldType.STRING)
350
+ LOG_LEVEL__FIELD_NAME = WrapperField("LOG_LEVEL", FieldType.STRING)
351
+ LOG_LINE_NUM__FIELD_NAME = WrapperField("LOG_LINE_NUM", FieldType.INTEGER)
352
+ LOG_MESSAGE__FIELD_NAME = WrapperField("LOG_MESSAGE", FieldType.STRING)
353
+ LOG_TIMESTAMP__FIELD_NAME = WrapperField("LOG_TIMESTAMP", FieldType.DATE)
354
+
355
+
356
+ class VeloxRuleCostPseudoDef:
357
+ DATA_TYPE_NAME: str = "VELOX_RULE_COST"
358
+ ACTION_COST__FIELD_NAME = WrapperField("ACTION_COST", FieldType.LONG)
359
+ ACTION_COUNT__FIELD_NAME = WrapperField("ACTION_COUNT", FieldType.LONG)
360
+ ANCESTOR_DESCENDANT_COUNT__FIELD_NAME = WrapperField("ANCESTOR_DESCENDANT_COUNT", FieldType.LONG)
361
+ PARENT_CHILD_COUNT__FIELD_NAME = WrapperField("PARENT_CHILD_COUNT", FieldType.LONG)
362
+ PROCESSING_TIME__FIELD_NAME = WrapperField("PROCESSING_TIME", FieldType.LONG)
363
+ RULE_GUID__FIELD_NAME = WrapperField("RULE_GUID", FieldType.STRING)
364
+ SOURCE_RECORD_COUNT__FIELD_NAME = WrapperField("SOURCE_RECORD_COUNT", FieldType.LONG)
365
+ TIMESTAMP__FIELD_NAME = WrapperField("TIMESTAMP", FieldType.DATE)
366
+ TOTAL_COST__FIELD_NAME = WrapperField("TOTAL_COST", FieldType.LONG)
367
+ TRANSACTION_GUID__FIELD_NAME = WrapperField("TRANSACTION_GUID", FieldType.STRING)
368
+ USERNAME__FIELD_NAME = WrapperField("USERNAME", FieldType.STRING)
369
+
370
+
371
+ class VeloxConversationPseudoDef:
372
+ DATA_TYPE_NAME: str = "VeloxConversation"
373
+ CONVERSATION_DESCRIPTION__FIELD_NAME = WrapperField("ConversationDescription", FieldType.STRING)
374
+ CONVERSATION_GUID__FIELD_NAME = WrapperField("ConversationGuid", FieldType.STRING)
375
+ CONVERSATION_NAME__FIELD_NAME = WrapperField("ConversationName", FieldType.STRING)
376
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
377
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.DATE)
378
+ SERVER_PLUGIN_PATH__FIELD_NAME = WrapperField("Server_Plugin_Path", FieldType.STRING)
379
+
380
+
381
+ class VeloxConversationMessagePseudoDef:
382
+ DATA_TYPE_NAME: str = "VeloxConversationMessage"
383
+ CONVERSATION_GUID__FIELD_NAME = WrapperField("ConversationGuid", FieldType.STRING)
384
+ MESSAGE__FIELD_NAME = WrapperField("Message", FieldType.STRING)
385
+ MESSAGE_GUID__FIELD_NAME = WrapperField("MessageGuid", FieldType.STRING)
386
+ MESSAGE_SENDER__FIELD_NAME = WrapperField("MessageSender", FieldType.STRING)
387
+ MESSAGE_TIMESTAMP__FIELD_NAME = WrapperField("MessageTimestamp", FieldType.DATE)
388
+
389
+
390
+ class VeloxScriptPseudoDef:
391
+ DATA_TYPE_NAME: str = "VeloxScript"
392
+ CODE__FIELD_NAME = WrapperField("Code", FieldType.STRING)
393
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
394
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.LONG)
395
+ LAST_MODIFIED_BY__FIELD_NAME = WrapperField("LastModifiedBy", FieldType.STRING)
396
+ LAST_MODIFIED_DATE__FIELD_NAME = WrapperField("LastModifiedDate", FieldType.LONG)
397
+ PATH__FIELD_NAME = WrapperField("Path", FieldType.STRING)
398
+ PLUGIN_DESCRIPTION__FIELD_NAME = WrapperField("PluginDescription", FieldType.STRING)
399
+ PLUGIN_LINE1_TEXT__FIELD_NAME = WrapperField("PluginLine1Text", FieldType.STRING)
400
+ PLUGIN_LINE2_TEXT__FIELD_NAME = WrapperField("PluginLine2Text", FieldType.STRING)
401
+ PLUGIN_ORDER__FIELD_NAME = WrapperField("PluginOrder", FieldType.INTEGER)
402
+ PLUGIN_POINT__FIELD_NAME = WrapperField("PluginPoint", FieldType.STRING)
403
+ PROJECT_GUID__FIELD_NAME = WrapperField("ProjectGuid", FieldType.STRING)
404
+ SCRIPT_GUID__FIELD_NAME = WrapperField("ScriptGuid", FieldType.STRING)
405
+
406
+
407
+ class VeloxScriptProjectPseudoDef:
408
+ DATA_TYPE_NAME: str = "VeloxScriptProject"
409
+ CLASS_PATH__FIELD_NAME = WrapperField("ClassPath", FieldType.STRING)
410
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
411
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.LONG)
412
+ DEPLOYMENT_OUT_OF_DATE__FIELD_NAME = WrapperField("DeploymentOutOfDate", FieldType.BOOLEAN)
413
+ DESCRIPTION__FIELD_NAME = WrapperField("Description", FieldType.STRING)
414
+ PROJECT_GUID__FIELD_NAME = WrapperField("ProjectGuid", FieldType.STRING)
415
+ PROJECT_NAME__FIELD_NAME = WrapperField("ProjectName", FieldType.STRING)
416
+ SCRIPT_LANGUAGE__FIELD_NAME = WrapperField("ScriptLanguage", FieldType.STRING)
417
+
418
+
419
+ class WorkflowPseudoDef:
420
+ DATA_TYPE_NAME: str = "Workflow"
421
+ ALL_ACCESS__FIELD_NAME = WrapperField("AllAccess", FieldType.BOOLEAN)
422
+ CATEGORY__FIELD_NAME = WrapperField("Category", FieldType.STRING)
423
+ CREATED_BY__FIELD_NAME = WrapperField("CreatedBy", FieldType.STRING)
424
+ DATE_CREATED__FIELD_NAME = WrapperField("DateCreated", FieldType.DATE)
425
+ DATE_EDITED__FIELD_NAME = WrapperField("DateEdited", FieldType.DATE)
426
+ DIRECT_LAUNCH__FIELD_NAME = WrapperField("DirectLaunch", FieldType.BOOLEAN)
427
+ EDITED_BY__FIELD_NAME = WrapperField("EditedBy", FieldType.STRING)
428
+ IS_TEMPLATE__FIELD_NAME = WrapperField("IsTemplate", FieldType.BOOLEAN)
429
+ LONG_DESC__FIELD_NAME = WrapperField("LongDesc", FieldType.STRING)
430
+ NAME__FIELD_NAME = WrapperField("Name", FieldType.STRING)
431
+ SHORT_DESC__FIELD_NAME = WrapperField("ShortDesc", FieldType.STRING)
432
+ WORKFLOW_ID__FIELD_NAME = WrapperField("WorkflowId", FieldType.LONG)
433
+ WORKFLOW_VERSION__FIELD_NAME = WrapperField("WorkflowVersion", FieldType.LONG)
434
+
435
+
436
+ class WorkflowOptionPseudoDef:
437
+ DATA_TYPE_NAME: str = "WorkflowOption"
438
+ OPTION_KEY__FIELD_NAME = WrapperField("OptionKey", FieldType.STRING)
439
+ OPTION_VALUE__FIELD_NAME = WrapperField("OptionValue", FieldType.STRING)
440
+ WORKFLOW_ID__FIELD_NAME = WrapperField("WorkflowId", FieldType.LONG)
@@ -23,6 +23,7 @@ from sapiopylib.rest.utils.recordmodel.RecordModelManager import RecordModelInst
23
23
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
24
24
  from sapiopylib.rest.utils.recordmodel.properties import Child
25
25
 
26
+ from sapiopycommons.eln.experiment_report_util import ExperimentReportUtil
26
27
  from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIdentifier, UserIdentifier, \
27
28
  DataTypeIdentifier, RecordModel
28
29
  from sapiopycommons.general.exceptions import SapioException
@@ -67,9 +68,9 @@ class ExperimentHandler:
67
68
  """Whether this ExperimentHandler has queried the system for all steps in the experiment."""
68
69
  __steps: dict[str, ElnEntryStep]
69
70
  """Steps from this experiment. All steps are cached the first time any individual step is accessed."""
70
- __step_options: dict[ElnEntryStep, dict[str, str]]
71
- """Entry options for each step in this experiment. Only cached for each individual step when they are first accessed.
72
- The cache is updated whenever the entry options for a step are changed by this handler."""
71
+ __step_options: dict[int, dict[str, str]]
72
+ """Entry options for each step in this experiment. All entry options are cached the first time any individual step's
73
+ options are queried. The cache is updated whenever the entry options for a step are changed by this handler."""
73
74
 
74
75
  # Constants
75
76
  __ENTRY_COMPLETE_STATUSES = [ExperimentEntryStatus.Completed, ExperimentEntryStatus.CompletedApproved]
@@ -549,6 +550,39 @@ class ExperimentHandler:
549
550
  data_type: str = AliasUtil.to_data_type_name(data_type)
550
551
  return [x for x in all_steps if data_type in x.get_data_type_names()]
551
552
 
553
+ def get_step_by_option(self, key: str, value: str | None = None) -> ElnEntryStep:
554
+ """
555
+ Retrieve the step in this experiment that contains an entry option with the provided key and value.
556
+ Throws an exception if no entries or multiple entries in the experiment match.
557
+
558
+ :param key: The key of the entry option to match on.
559
+ :param value: The value of the entry option to match on. If not provided, then only matches on key.
560
+ :return: The entry in this experiment that matches the provided entry option key and value.
561
+ """
562
+ steps: list[ElnEntryStep] = self.get_steps_by_option(key, value)
563
+ count: int = len(steps)
564
+ if count != 1:
565
+ option = key + ("::" + value if value is not None else "")
566
+ raise SapioException(f"{('No' if count == 0 else 'Multiple')} entries in this experiment match the "
567
+ f"provided option: {option}")
568
+ return steps[0]
569
+
570
+ def get_steps_by_option(self, key: str, value: str | None = None) -> list[ElnEntryStep]:
571
+ """
572
+ Retrieve every step in this experiment that contains an entry option with the provided key and value.
573
+
574
+ :param key: The key of the entry option to match on.
575
+ :param value: The value of the entry option to match on. If not provided, then only matches on key.
576
+ :return: The entries in this experiment that match the provided entry option key and value.
577
+ """
578
+ ret_list: list[ElnEntryStep] = []
579
+ for step in self.get_all_steps():
580
+ options: dict[str, str] = self.get_step_options(step)
581
+ if key in options:
582
+ if value is None or options[key] == value:
583
+ ret_list.append(step)
584
+ return ret_list
585
+
552
586
  def get_step_records(self, step: Step) -> list[DataRecord]:
553
587
  """
554
588
  Query for the data records for the given step. The returned records are not cached by the ExperimentHandler.
@@ -966,7 +1000,7 @@ class ExperimentHandler:
966
1000
  if clear_template_item_fulfilled_timestamp is True:
967
1001
  entry.template_item_fulfilled_timestamp = None
968
1002
  if entry_options_map is not None:
969
- self.__step_options.update({step: entry_options_map})
1003
+ self.__step_options.update({step.get_id(): entry_options_map})
970
1004
 
971
1005
  def get_step_option(self, step: Step, option: str) -> str:
972
1006
  """
@@ -996,8 +1030,8 @@ class ExperimentHandler:
996
1030
  list of steps in the experiment and caches them.
997
1031
 
998
1032
  Getting the step options requires a webservice query, which is made the first time any step option
999
- method is called for a specific step. The step options are cached so that subsequent calls of this
1000
- method for that step don't make a webservice call.
1033
+ method is called for any step in this experiment. The step options are cached so that subsequent calls of this
1034
+ method don't make a webservice call.
1001
1035
 
1002
1036
  :param step:
1003
1037
  The step to get the options of.
@@ -1005,7 +1039,10 @@ class ExperimentHandler:
1005
1039
  If given a name, throws an exception if no step of the given name exists in the experiment.
1006
1040
  :return: The map of options for the input step.
1007
1041
  """
1008
- return self.__get_step_options(step)
1042
+ step = self.__to_eln_step(step)
1043
+ if step not in self.__step_options:
1044
+ self.__step_options.update(ExperimentReportUtil.get_experiment_entry_options(self.user, self.get_all_steps()))
1045
+ return self.__step_options[step.get_id()]
1009
1046
 
1010
1047
  def add_step_options(self, step: Step, mapping: Mapping[str, str]):
1011
1048
  """
@@ -1175,16 +1212,3 @@ class ExperimentHandler:
1175
1212
  return self.__exp_options
1176
1213
  self.__exp_options = self.__eln_man.get_notebook_experiment_options(self.__exp_id)
1177
1214
  return self.__exp_options
1178
-
1179
- def __get_step_options(self, step: Step) -> dict[str, str]:
1180
- """
1181
- Cache the options for the input step if they haven't been cached yet.
1182
-
1183
- :return: The entry options for the input step.
1184
- """
1185
- step = self.__to_eln_step(step)
1186
- if step in self.__step_options:
1187
- return self.__step_options.get(step)
1188
- options: dict[str, str] = step.get_options()
1189
- self.__step_options.update({step: options})
1190
- return options
@@ -1,16 +1,18 @@
1
+ from sapiopylib.rest.ELNService import ElnManager
1
2
  from sapiopylib.rest.User import SapioUser
2
3
  from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
4
+ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, ElnExperimentQueryCriteria
3
5
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
4
6
 
5
7
  from sapiopycommons.customreport.custom_report_builder import CustomReportBuilder
6
8
  from sapiopycommons.customreport.term_builder import TermBuilder
7
- from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil
9
+ from sapiopycommons.datatype.pseudo_data_types import EnbEntryOptionsPseudoDef, NotebookExperimentOptionPseudoDef, \
10
+ NotebookExperimentPseudoDef, ExperimentEntryRecordPseudoDef, EnbEntryPseudoDef
11
+ from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil, FieldValue, \
12
+ ExperimentEntryIdentifier, ExperimentIdentifier
8
13
  from sapiopycommons.general.custom_report_util import CustomReportUtil
9
14
  from sapiopycommons.recordmodel.record_handler import RecordHandler
10
15
 
11
- _NOTEBOOK_ID = "EXPERIMENTID"
12
- _RECORD_ID = "RECORDID"
13
-
14
16
 
15
17
  # FR-46908 - Provide a utility class that holds experiment related custom reports e.g. getting all the experiments
16
18
  # that given records were used in or getting all records of a datatype used in given experiments.
@@ -23,8 +25,8 @@ class ExperimentReportUtil:
23
25
  If a record wasn't used in any experiments then it will be mapped to an empty list.
24
26
 
25
27
  :param context: The current webhook context or a user object to send requests from.
26
- :param records: a list of records of the same data type.
27
- :return: a dictionary mapping each record to a list of ids of each experiment it was used in.
28
+ :param records: A list of records of the same data type.
29
+ :return: A dictionary mapping each record to a list of ids of each experiment it was used in.
28
30
  """
29
31
  if not records:
30
32
  return {}
@@ -38,24 +40,25 @@ class ExperimentReportUtil:
38
40
  id_to_record: dict[int, SapioRecord] = RecordHandler.map_by_id(records)
39
41
  record_to_exps: dict[SapioRecord, set[int]] = {record: set() for record in records}
40
42
  for row in rows:
41
- record_id: int = row[_RECORD_ID]
42
- exp_id: int = row[_NOTEBOOK_ID]
43
+ record_id: int = row["RecordId"]
44
+ exp_id: int = row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
43
45
  record = id_to_record[record_id]
44
46
  record_to_exps[record].add(exp_id)
45
47
 
46
48
  return {record: list(exps) for record, exps in record_to_exps.items()}
47
49
 
48
50
  @staticmethod
49
- def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[int],
51
+ def map_experiments_to_records_of_type(context: UserIdentifier, exp_ids: list[ExperimentIdentifier],
50
52
  wrapper_type: type[WrappedType]) -> dict[int, list[WrappedType]]:
51
53
  """
52
- Return a dictionary mapping each experiment id to a list of records of the given type that were used in each experiment.
53
- If an experiment didn't use any records of the given type then it will be mapped to an empty list.
54
+ Return a dictionary mapping each experiment id to a list of records of the given type that were used in each
55
+ experiment. If an experiment didn't use any records of the given type then it will be mapped to an empty list.
54
56
 
55
57
  :param context: The current webhook context or a user object to send requests from.
56
- :param exp_ids: a list of experiment ids. These are specifically the Notebook Experiment ids which can be found in the title of the experiment.
58
+ :param exp_ids: A list of experiment identifiers.
57
59
  :param wrapper_type: The record model wrapper to use, corresponds to which data type we will query for.
58
- :return: a dictionary mapping each experiment id to a list of records of the given type that were used in that experiment.
60
+ :return: A dictionary mapping each experiment id to a list of records of the given type that were used in that
61
+ experiment.
59
62
  """
60
63
  if not exp_ids:
61
64
  return {}
@@ -64,20 +67,130 @@ class ExperimentReportUtil:
64
67
  record_handler = RecordHandler(user)
65
68
  data_type_name: str = wrapper_type.get_wrapper_data_type_name()
66
69
 
70
+ exp_ids: list[int] = AliasUtil.to_notebook_ids(exp_ids)
67
71
  rows = ExperimentReportUtil.__get_record_experiment_relation_rows(user, data_type_name, exp_ids=exp_ids)
68
- record_ids: set[int] = {row[_RECORD_ID] for row in rows}
72
+ record_ids: set[int] = {row["RecordId"] for row in rows}
69
73
  records = record_handler.query_models_by_id(wrapper_type, record_ids)
70
74
 
71
75
  id_to_record: dict[int, WrappedType] = RecordHandler.map_by_id(records)
72
76
  exp_to_records: dict[int, set[SapioRecord]] = {exp: set() for exp in exp_ids}
73
77
  for row in rows:
74
- record_id: int = row[_RECORD_ID]
75
- exp_id: int = row[_NOTEBOOK_ID]
78
+ record_id: int = row["RecordId"]
79
+ exp_id: int = row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
76
80
  record = id_to_record[record_id]
77
81
  exp_to_records[exp_id].add(record)
78
82
 
79
83
  return {exp: list(records) for exp, records in exp_to_records.items()}
80
84
 
85
+ @staticmethod
86
+ def get_experiment_options(context: UserIdentifier, experiments: list[ExperimentIdentifier]) \
87
+ -> dict[int, dict[str, str]]:
88
+ """
89
+ Run a custom report to retrieve the experiment options for all the provided experiments. Effectively a batched
90
+ version of the get_notebook_experiment_options function of ElnManager.
91
+
92
+ :param context: The current webhook context or a user object to send requests from.
93
+ :param experiments: The experiment identifiers to retrieve the experiment options for.
94
+ :return: A dictionary mapping the notebook experiment ID to the options for that experiment.
95
+ """
96
+ exp_ids: list[int] = AliasUtil.to_notebook_ids(experiments)
97
+
98
+ report_builder = CustomReportBuilder(NotebookExperimentOptionPseudoDef.DATA_TYPE_NAME)
99
+ root = TermBuilder.is_term(NotebookExperimentOptionPseudoDef.DATA_TYPE_NAME,
100
+ NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME,
101
+ exp_ids)
102
+ report_builder.set_root_term(root)
103
+ report_builder.add_column(NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME)
104
+ report_builder.add_column(NotebookExperimentOptionPseudoDef.OPTION_KEY__FIELD_NAME)
105
+ report_builder.add_column(NotebookExperimentOptionPseudoDef.OPTION_VALUE__FIELD_NAME)
106
+ report = report_builder.build_report_criteria()
107
+
108
+ # Ensure that each experiment appears in the dictionary, even if it has no experiment options.
109
+ options: dict[int, dict[str, str]] = {x: {} for x in exp_ids}
110
+ results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
111
+ for row in results:
112
+ exp_id: int = row[NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
113
+ key: str = row[NotebookExperimentOptionPseudoDef.OPTION_KEY__FIELD_NAME.field_name]
114
+ value: str = row[NotebookExperimentOptionPseudoDef.OPTION_VALUE__FIELD_NAME.field_name]
115
+ options[exp_id][key] = value
116
+ return options
117
+
118
+ @staticmethod
119
+ def get_experiment_entry_options(context: UserIdentifier, entries: list[ExperimentEntryIdentifier]) \
120
+ -> dict[int, dict[str, str]]:
121
+ """
122
+ Run a custom report to retrieve the entry options for all the provided entries. Effectively a batched
123
+ version of the get_experiment_entry_options function of ElnManager.
124
+
125
+ :param context: The current webhook context or a user object to send requests from.
126
+ :param entries: The experiment entry identifiers to retrieve the entry options for.
127
+ :return: A dictionary mapping the entry ID to the options for that entry.
128
+ """
129
+ entries: list[int] = AliasUtil.to_entry_ids(entries)
130
+ report_builder = CustomReportBuilder(EnbEntryOptionsPseudoDef.DATA_TYPE_NAME)
131
+ root = TermBuilder.is_term(EnbEntryOptionsPseudoDef.DATA_TYPE_NAME,
132
+ EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME,
133
+ entries)
134
+ report_builder.set_root_term(root)
135
+ report_builder.add_column(EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME)
136
+ report_builder.add_column(EnbEntryOptionsPseudoDef.ENTRY_OPTION_KEY__FIELD_NAME)
137
+ report_builder.add_column(EnbEntryOptionsPseudoDef.ENTRY_OPTION_VALUE__FIELD_NAME)
138
+ report = report_builder.build_report_criteria()
139
+
140
+ # Ensure that each entry appears in the dictionary, even if it has no entry options.
141
+ options: dict[int, dict[str, str]] = {x: {} for x in entries}
142
+ results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
143
+ for row in results:
144
+ entry_id: int = row[EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME.field_name]
145
+ key: str = row[EnbEntryOptionsPseudoDef.ENTRY_OPTION_KEY__FIELD_NAME.field_name]
146
+ value: str = row[EnbEntryOptionsPseudoDef.ENTRY_OPTION_VALUE__FIELD_NAME.field_name]
147
+ options[entry_id][key] = value
148
+ return options
149
+
150
+ @staticmethod
151
+ def get_experiments_by_name(context: UserIdentifier, name: str) -> list[ElnExperiment]:
152
+ """
153
+ Run a custom report that retrieves every experiment in the system with a given name.
154
+
155
+ :param context: The current webhook context or a user object to send requests from.
156
+ :param name: The name of the experiment to query for.
157
+ :return: A list of every experiment in the system with a name that matches the input.
158
+ """
159
+ return ExperimentReportUtil.get_experiments_by_names(context, [name])[name]
160
+
161
+ @staticmethod
162
+ def get_experiments_by_names(context: UserIdentifier, names: list[str]) -> dict[str, list[ElnExperiment]]:
163
+ """
164
+ Run a custom report that retrieves every experiment in the system with a name from a list of names.
165
+
166
+ :param context: The current webhook context or a user object to send requests from.
167
+ :param names: The names of the experiment to query for.
168
+ :return: A dictionary mapping the experiment name to a list of every experiment in the system with that name.
169
+ """
170
+ user = AliasUtil.to_sapio_user(context)
171
+
172
+ report_builder = CustomReportBuilder(NotebookExperimentPseudoDef.DATA_TYPE_NAME)
173
+ report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME)
174
+ root = TermBuilder.is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
175
+ NotebookExperimentPseudoDef.EXPERIMENT_NAME__FIELD_NAME,
176
+ names)
177
+ report_builder.set_root_term(root)
178
+
179
+ # Ensure that each entry appears in the dictionary, even if it has no experiments.
180
+ ret_val: dict[str, list[ElnExperiment]] = {x: [] for x in names}
181
+
182
+ exp_ids: list[int] = []
183
+ for row in CustomReportUtil.run_custom_report(user, report_builder.build_report_criteria()):
184
+ exp_ids.append(row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name])
185
+ if not exp_ids:
186
+ return ret_val
187
+
188
+ criteria = ElnExperimentQueryCriteria(notebook_experiment_id_white_list=exp_ids)
189
+ experiments: list[ElnExperiment] = ElnManager(user).get_eln_experiment_by_criteria(criteria)
190
+ for experiment in experiments:
191
+ ret_val.get(experiment.notebook_experiment_name).append(experiment)
192
+ return ret_val
193
+
81
194
  @staticmethod
82
195
  def __get_record_experiment_relation_rows(user: SapioUser, data_type_name: str, record_ids: list[int] | None = None,
83
196
  exp_ids: list[int] | None = None) -> list[dict[str, int]]:
@@ -88,30 +201,44 @@ class ExperimentReportUtil:
88
201
  assert (record_ids or exp_ids)
89
202
 
90
203
  if record_ids:
91
- records_term = TermBuilder.is_term(data_type_name, "RECORDID", record_ids)
204
+ records_term = TermBuilder.is_term(data_type_name, "RecordId", record_ids)
92
205
  else:
93
206
  # Get all records of the given type
94
207
  records_term = TermBuilder.all_records_term(data_type_name)
95
208
 
96
209
  if exp_ids:
97
- exp_term = TermBuilder.is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", exp_ids)
210
+ exp_term = TermBuilder.is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
211
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
212
+ exp_ids)
98
213
  else:
99
214
  # Get all experiments
100
- exp_term = TermBuilder.gte_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "0")
215
+ exp_term = TermBuilder.gte_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
216
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
217
+ 0)
101
218
 
102
219
  root_term = TermBuilder.and_terms(records_term, exp_term)
103
220
 
104
221
  # Join records on the experiment entry records that correspond to them.
105
- records_entry_join = TermBuilder.compare_is_term("EXPERIMENTENTRYRECORD", "RECORDID", data_type_name, "RECORDID")
222
+ records_entry_join = TermBuilder.compare_is_term(ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME,
223
+ ExperimentEntryRecordPseudoDef.RECORD_ID__FIELD_NAME,
224
+ data_type_name,
225
+ "RecordId")
106
226
  # Join entry records on the experiment entries they are in.
107
- experiment_entry_enb_entry_join = TermBuilder.compare_is_term("ENBENTRY", "ENTRYID", "EXPERIMENTENTRYRECORD", "ENTRYID")
227
+ experiment_entry_enb_entry_join = TermBuilder.compare_is_term(EnbEntryPseudoDef.DATA_TYPE_NAME,
228
+ EnbEntryPseudoDef.ENTRY_ID__FIELD_NAME,
229
+ ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME,
230
+ ExperimentEntryRecordPseudoDef.ENTRY_ID__FIELD_NAME)
108
231
  # Join entries on the experiments they are in.
109
- enb_entry_experiment_join = TermBuilder.compare_is_term("NOTEBOOKEXPERIMENT", "EXPERIMENTID", "ENBENTRY", "EXPERIMENTID")
232
+ enb_entry_experiment_join = TermBuilder.compare_is_term(NotebookExperimentPseudoDef.DATA_TYPE_NAME,
233
+ NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
234
+ EnbEntryPseudoDef.DATA_TYPE_NAME,
235
+ EnbEntryPseudoDef.EXPERIMENT_ID__FIELD_NAME)
110
236
 
111
237
  report_builder = CustomReportBuilder(data_type_name)
112
238
  report_builder.set_root_term(root_term)
113
- report_builder.add_column("RECORDID", FieldType.LONG, data_type=data_type_name)
114
- report_builder.add_column("EXPERIMENTID", FieldType.LONG, data_type="NOTEBOOKEXPERIMENT")
239
+ report_builder.add_column("RecordId", FieldType.LONG)
240
+ report_builder.add_column(NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME,
241
+ data_type=NotebookExperimentPseudoDef.DATA_TYPE_NAME)
115
242
  report_builder.add_join(records_entry_join)
116
243
  report_builder.add_join(experiment_entry_enb_entry_join)
117
244
  report_builder.add_join(enb_entry_experiment_join)
@@ -5,8 +5,9 @@ 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
8
9
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
9
- from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol
10
+ from sapiopylib.rest.utils.Protocols import ElnExperimentProtocol, ElnEntryStep
10
11
  from sapiopylib.rest.utils.recordmodel.PyRecordModel import PyRecordModel
11
12
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedRecordModel, WrappedType, WrapperField
12
13
 
@@ -31,8 +32,10 @@ dictionaries.."""
31
32
  HasFieldWrappers = type[WrappedType] | WrappedRecordModel
32
33
  """An identifier for classes that have wrapper fields."""
33
34
  ExperimentIdentifier = ElnExperimentProtocol | ElnExperiment | int
34
- """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for te experiment's notebook
35
+ """An ExperimentIdentifier is either an experiment protocol, experiment, or an integer for the experiment's notebook
35
36
  ID."""
37
+ ExperimentEntryIdentifier = ElnEntryStep | ExperimentEntry | int
38
+ """An ExperimentEntryIdentifier is either an ELN entry step, experiment entry, or an integer for the entry's ID."""
36
39
  FieldMap = dict[str, FieldValue]
37
40
  """A field map is simply a dict of data field names to values. The purpose of aliasing this is to help distinguish
38
41
  any random dict in a webhook from one which is explicitly used for record fields."""
@@ -223,7 +226,51 @@ class AliasUtil:
223
226
  if isinstance(experiment, ElnExperiment):
224
227
  return experiment.notebook_experiment_id
225
228
  return experiment.get_id()
226
-
229
+
230
+ @staticmethod
231
+ def to_notebook_ids(experiments: list[ExperimentIdentifier]) -> list[int]:
232
+ """
233
+ Convert a list of objects that identify ELN experiments to their notebook IDs.
234
+
235
+ :return: The list of notebook IDs for the experiment identifiers.
236
+ """
237
+ notebook_ids: list[int] = []
238
+ for experiment in experiments:
239
+ notebook_ids.append(AliasUtil.to_notebook_id(experiment))
240
+ return notebook_ids
241
+
242
+ @staticmethod
243
+ def to_entry_id(entry: ExperimentEntryIdentifier) -> int:
244
+ """
245
+ Convert an object that identifies an experiment entry to its entry ID.
246
+
247
+ :return: The entry ID for the entry identifier.
248
+ """
249
+ if isinstance(entry, int):
250
+ return entry
251
+ elif isinstance(entry, ExperimentEntry):
252
+ return entry.entry_id
253
+ elif isinstance(entry, ElnEntryStep):
254
+ return entry.get_id()
255
+ raise SapioException(f"Unrecognized entry identifier of type {type(entry)}")
256
+
257
+ @staticmethod
258
+ def to_entry_ids(entries: list[ExperimentEntryIdentifier]) -> list[int]:
259
+ """
260
+ Convert a list of objects that identify experiment entries to their entry IDs.
261
+
262
+ :return: The list of entry IDs for the entry identifiers.
263
+ """
264
+ entry_ids: list[int] = []
265
+ for entry in entries:
266
+ entry_ids.append(AliasUtil.to_entry_id(entry))
267
+ return entry_ids
268
+
227
269
  @staticmethod
228
270
  def to_sapio_user(context: UserIdentifier) -> SapioUser:
271
+ """
272
+ Convert an object that could be either a SapioUser or SapioWebhookContext to just a SapioUser.
273
+
274
+ :return: A SapioUser object.
275
+ """
229
276
  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}"
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.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.10.14a341
3
+ Version: 2024.10.25a345
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -2,17 +2,18 @@ sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  sapiopycommons/callbacks/callback_util.py,sha256=nb6cXK8yFq96gtG0Z2NiK-qdNaRh88bavUH-ZoBjh18,67953
4
4
  sapiopycommons/chem/IndigoMolecules.py,sha256=3f-aig3AJkKJhRmhlQ0cI-5G8oeaQk_3foJTDZCvoko,2040
5
- sapiopycommons/chem/Molecules.py,sha256=0B_SsXB2swg2DiP50p0tcNOVO1ajlxumSI42YyDiSHI,11517
5
+ sapiopycommons/chem/Molecules.py,sha256=SQKnqdZnhYj_6HGtEZmE_1DormonRR1-nBAQ__z4gms,11485
6
6
  sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sapiopycommons/customreport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- sapiopycommons/customreport/column_builder.py,sha256=sS_wZYOR72rs3syTNjwCVP4h8M8N0b0burkTxFQItVU,3019
8
+ sapiopycommons/customreport/column_builder.py,sha256=0RO53e9rKPZ07C--KcepN6_tpRw_FxF3O9vdG0ilKG8,3014
9
9
  sapiopycommons/customreport/custom_report_builder.py,sha256=o2O89OrWPm0OYS8Ux6EKZTg6hcUzfz3ZxAgnzJg1wEw,6601
10
10
  sapiopycommons/customreport/term_builder.py,sha256=oVsr7iFPnug2TrZUCcAMhyps-b62kDodPcBxyQeneUY,16763
11
11
  sapiopycommons/datatype/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  sapiopycommons/datatype/attachment_util.py,sha256=_l2swuP8noIGAl4bwzBUEhr6YlN_OVZl3-gi1XqFHYA,3364
13
+ sapiopycommons/datatype/pseudo_data_types.py,sha256=Fe75Rnq5evyeJM1nC0sLkLGKAC74g2-GEeTdMeId80o,27649
13
14
  sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- sapiopycommons/eln/experiment_handler.py,sha256=eUVKHmgd4oowWg37mBnp4Ag_NsXBkDhm6mV4j36HhKs,66850
15
- sapiopycommons/eln/experiment_report_util.py,sha256=B55hXbTGndCEI19LiqhjvO46_chujZUK0qDTWHjzfcU,6398
15
+ sapiopycommons/eln/experiment_handler.py,sha256=jtJC-ZU8fDud5UD8IVLJZVOqgm8QMkyA1kZZujf3fcg,68454
16
+ sapiopycommons/eln/experiment_report_util.py,sha256=nt-zs6ag1oBAN3NSHcdLBtY1EWxsiI10NfW15xTnn4g,14522
16
17
  sapiopycommons/eln/plate_designer.py,sha256=FYJfhhNq8hdfuXgDYOYHy6g0m2zNwQXZWF_MTPzElDg,7184
17
18
  sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  sapiopycommons/files/complex_data_loader.py,sha256=T39veNhvYl6j_uZjIIJ8Mk5Aa7otR5RB-g8XlAdkksA,1421
@@ -26,14 +27,14 @@ sapiopycommons/flowcyto/flow_cyto.py,sha256=YlkKJR_zEHYRuNW0bnTqlTyZeXs0lOaeSCfG
26
27
  sapiopycommons/flowcyto/flowcyto_data.py,sha256=mYKFuLbtpJ-EsQxLGtu4tNHVlygTxKixgJxJqD68F58,2596
27
28
  sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  sapiopycommons/general/accession_service.py,sha256=HYgyOsH_UaoRnoury-c2yTW8SeG4OtjLemdpCzoV4R8,13484
29
- sapiopycommons/general/aliases.py,sha256=Gih9shHsj765q4HimfFTTI7wWDPAtXjoqCgHisyIQZY,10409
30
- sapiopycommons/general/audit_log.py,sha256=tJi4uU4qRY2WWcK4ItkjRvoCHCwwiU9LwCNv4lP5-QQ,8713
30
+ sapiopycommons/general/aliases.py,sha256=eyW5VSK8q5xCjWv9fwd3gKEmK_7pboVLa4DPQpJXpe4,12264
31
+ sapiopycommons/general/audit_log.py,sha256=KQI0AGgN9WLwKqnHE4Tm0xeBCfpVBf8rIQ2HFmnyFGI,8956
31
32
  sapiopycommons/general/custom_report_util.py,sha256=BGu9Ki0wn3m4Nk-LKM6inDSfe8ULUSG9d-HJJNOTtGc,15653
32
33
  sapiopycommons/general/exceptions.py,sha256=GY7fe0qOgoy4kQVn_Pn3tdzHsJZyNIpa6VCChg6tzuM,1813
33
34
  sapiopycommons/general/popup_util.py,sha256=L-4qpTemSZdlD6_6oEsDYIzLOCiZgDK6wC6DqUwzOYA,31925
34
35
  sapiopycommons/general/sapio_links.py,sha256=o9Z-8y2rz6AI0Cy6tq58ElPge9RBnisGc9NyccbaJxs,2610
35
36
  sapiopycommons/general/storage_util.py,sha256=ovmK_jN7v09BoX07XxwShpBUC5WYQOM7dbKV_VeLXJU,8892
36
- sapiopycommons/general/time_util.py,sha256=sXThADCRAQDWYDD9C5CdhcKYIt3qOaVNyZfGBR7HW9A,8701
37
+ sapiopycommons/general/time_util.py,sha256=jUAWmQLNcLHZa4UYB4ht_I3d6uoi63VxYdo7T80Ydw0,7458
37
38
  sapiopycommons/multimodal/multimodal.py,sha256=A1QsC8QTPmgZyPr7KtMbPRedn2Ie4WIErodUvQ9otgU,6724
38
39
  sapiopycommons/multimodal/multimodal_data.py,sha256=t-0uY4cVgm88uXaSOL4ZeB6zmdHufowXuLFlMk61wFg,15087
39
40
  sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -43,10 +44,12 @@ sapiopycommons/recordmodel/record_handler.py,sha256=Uxjrq6f_cWFbqi7KRLySdOvmQGtb
43
44
  sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
45
  sapiopycommons/rules/eln_rule_handler.py,sha256=JYzDA_14D2nLnlqwbpIxVOrfKWzbOS27AYf4TQfGr4Q,10469
45
46
  sapiopycommons/rules/on_save_rule_handler.py,sha256=Rkqvph20RbNq6m-RF4fbvCP-YfD2CZYBM2iTr3nl0eY,10236
47
+ sapiopycommons/sftpconnect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ sapiopycommons/sftpconnect/sftp_builder.py,sha256=eKYMiyBc10DNTfbeidQUcfZgFTwhu5ZU-nNJMCK_eos,3014
46
49
  sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
50
  sapiopycommons/webhook/webhook_handlers.py,sha256=JTquLBln49L1pJ9txJ4oc4Hpzy9kYtMKs0m4SLaFx78,18363
48
51
  sapiopycommons/webhook/webservice_handlers.py,sha256=1J56zFI0pWl5MHoNTznvcZumITXgAHJMluj8-2BqYEw,3315
49
- sapiopycommons-2024.10.14a341.dist-info/METADATA,sha256=HAeNuNnK_r78w9E501s3a6mdD7RdbJLNOsY5W-XTViU,3177
50
- sapiopycommons-2024.10.14a341.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
51
- sapiopycommons-2024.10.14a341.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
52
- sapiopycommons-2024.10.14a341.dist-info/RECORD,,
52
+ sapiopycommons-2024.10.25a345.dist-info/METADATA,sha256=SSy4mFChmb1yl1geoOfc81CC9PW0uxqulO8eHqyRDZA,3177
53
+ sapiopycommons-2024.10.25a345.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
54
+ sapiopycommons-2024.10.25a345.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
55
+ sapiopycommons-2024.10.25a345.dist-info/RECORD,,