sapiopycommons 2025.3.10a455__py3-none-any.whl → 2025.3.17a456__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 (29) hide show
  1. sapiopycommons/callbacks/callback_util.py +1220 -366
  2. sapiopycommons/chem/Molecules.py +0 -2
  3. sapiopycommons/customreport/auto_pagers.py +270 -0
  4. sapiopycommons/datatype/data_fields.py +1 -1
  5. sapiopycommons/eln/experiment_handler.py +2 -1
  6. sapiopycommons/eln/experiment_report_util.py +7 -7
  7. sapiopycommons/files/file_bridge.py +76 -0
  8. sapiopycommons/files/file_bridge_handler.py +325 -110
  9. sapiopycommons/files/file_data_handler.py +2 -2
  10. sapiopycommons/files/file_util.py +36 -11
  11. sapiopycommons/files/file_validator.py +6 -5
  12. sapiopycommons/files/file_writer.py +1 -1
  13. sapiopycommons/flowcyto/flow_cyto.py +1 -1
  14. sapiopycommons/general/accession_service.py +1 -1
  15. sapiopycommons/general/aliases.py +48 -28
  16. sapiopycommons/general/audit_log.py +2 -2
  17. sapiopycommons/general/custom_report_util.py +24 -1
  18. sapiopycommons/general/directive_util.py +86 -0
  19. sapiopycommons/general/exceptions.py +41 -2
  20. sapiopycommons/general/popup_util.py +2 -2
  21. sapiopycommons/multimodal/multimodal.py +1 -0
  22. sapiopycommons/processtracking/custom_workflow_handler.py +3 -3
  23. sapiopycommons/recordmodel/record_handler.py +5 -3
  24. sapiopycommons/samples/aliquot.py +48 -0
  25. sapiopycommons/webhook/webhook_handlers.py +445 -55
  26. {sapiopycommons-2025.3.10a455.dist-info → sapiopycommons-2025.3.17a456.dist-info}/METADATA +1 -1
  27. {sapiopycommons-2025.3.10a455.dist-info → sapiopycommons-2025.3.17a456.dist-info}/RECORD +29 -26
  28. {sapiopycommons-2025.3.10a455.dist-info → sapiopycommons-2025.3.17a456.dist-info}/WHEEL +0 -0
  29. {sapiopycommons-2025.3.10a455.dist-info → sapiopycommons-2025.3.17a456.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,5 @@
1
1
  # Author Yechen Qiao
2
2
  # Common Molecule Utilities for Molecule Transfers with Sapio
3
- from typing import cast
4
3
 
5
4
  from rdkit import Chem
6
5
  from rdkit.Chem import Crippen, MolToInchi
@@ -9,7 +8,6 @@ from rdkit.Chem import rdMolDescriptors
9
8
  from rdkit.Chem.EnumerateStereoisomers import StereoEnumerationOptions, EnumerateStereoisomers
10
9
  from rdkit.Chem.MolStandardize import rdMolStandardize
11
10
  from rdkit.Chem.SaltRemover import SaltRemover
12
- from rdkit.Chem.rdChemReactions import ChemicalReaction
13
11
  from rdkit.Chem.rdchem import Mol, RWMol, Bond
14
12
 
15
13
  from sapiopycommons.chem.IndigoMolecules import indigo, renderer, indigo_inchi
@@ -0,0 +1,270 @@
1
+ from abc import ABC
2
+ from copy import copy
3
+ from queue import Queue
4
+
5
+ from sapiopylib.rest.CustomReportService import CustomReportManager
6
+ from sapiopylib.rest.DataMgmtService import DataMgmtServer
7
+ from sapiopylib.rest.pojo.CustomReport import CustomReportCriteria, CustomReport, RawReportTerm, ReportColumn
8
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
9
+ from sapiopylib.rest.utils.autopaging import SapioPyAutoPager, PagerResultCriteriaType, _default_report_page_size, \
10
+ _default_record_page_size
11
+ from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
12
+
13
+ from sapiopycommons.general.aliases import FieldValue, UserIdentifier, AliasUtil, RecordModel
14
+ from sapiopycommons.general.custom_report_util import CustomReportUtil
15
+ from sapiopycommons.general.exceptions import SapioException
16
+ from sapiopycommons.recordmodel.record_handler import RecordHandler
17
+
18
+
19
+ # FR-47389: Create auto pagers for running custom/system/quick reports that return dictionaries or records for each row.
20
+ class _DictReportPagerBase(SapioPyAutoPager[CustomReportCriteria, dict[str, FieldValue]], ABC):
21
+ """
22
+ A base class for automatically paging through a report and returning the results as a list of dictionaries.
23
+ """
24
+ _columns: list[ReportColumn]
25
+ _report_man: CustomReportManager
26
+
27
+ def __init__(self, user: UserIdentifier, first_page_criteria: CustomReportCriteria):
28
+ self._columns = first_page_criteria.column_list
29
+ super().__init__(AliasUtil.to_sapio_user(user), first_page_criteria)
30
+ self._report_man = DataMgmtServer.get_custom_report_manager(self.user)
31
+
32
+ def get_all_at_once(self) -> list[dict[str, FieldValue]]:
33
+ """
34
+ Get the results of all pages. Be cautious of client memory usage.
35
+ """
36
+ if self.has_iterated:
37
+ raise BrokenPipeError("Cannot use this method if the iterator has already been used.")
38
+ return [x for x in self]
39
+
40
+ def default_first_page_criteria(self) -> PagerResultCriteriaType:
41
+ raise ValueError("Cannot generate a default first page criteria for custom reports.")
42
+
43
+ def get_next_page_result(self) -> tuple[CustomReportCriteria | None, Queue[dict[str, FieldValue]]]:
44
+ report: CustomReport = self._report_man.run_custom_report(self.next_page_criteria)
45
+ queue: Queue[dict[str, FieldValue]] = Queue()
46
+ for row in _process_results(report.result_table, self._columns):
47
+ queue.put(row)
48
+ if report.has_next_page:
49
+ next_page_criteria = copy(self.next_page_criteria)
50
+ next_page_criteria.page_number += 1
51
+ return next_page_criteria, queue
52
+ else:
53
+ return None, queue
54
+
55
+
56
+ class CustomReportDictAutoPager(_DictReportPagerBase):
57
+ """
58
+ A class that automatically pages through a custom report and returns the results as a list of dictionaries.
59
+ """
60
+ def __init__(self, user: UserIdentifier, report_criteria: CustomReportCriteria,
61
+ page_number: int = 0, page_size: int = _default_report_page_size):
62
+ """
63
+ :param user: The current webhook context or a user object to send requests from.
64
+ :param report_criteria: The custom report criteria to run.
65
+ :param page_number: The page number to start on. The first page is page 0.
66
+ :param page_size: The number of results to return per page.
67
+ """
68
+ first_page_criteria: CustomReportCriteria = copy(report_criteria)
69
+ first_page_criteria.page_number = page_number
70
+ first_page_criteria.page_size = page_size
71
+ super().__init__(user, first_page_criteria)
72
+
73
+
74
+ class SystemReportDictAutoPager(_DictReportPagerBase):
75
+ """
76
+ A class that automatically pages through a system report and returns the results as a list of dictionaries.
77
+
78
+ System reports are also known as predefined searches in the system and must be defined in the data designer for
79
+ a specific data type. That is, saved searches created by users cannot be run using this function.
80
+ """
81
+ def __init__(self, user: UserIdentifier, report_name: str,
82
+ page_number: int = 0, page_size: int = _default_report_page_size):
83
+ """
84
+ :param user: The current webhook context or a user object to send requests from.
85
+ :param report_name: The name of the system report to run.
86
+ :param page_number: The page number to start on. The first page is page 0.
87
+ :param page_size: The number of results to return per page.
88
+ """
89
+ first_page_criteria: CustomReportCriteria = CustomReportUtil.get_system_report_criteria(user, report_name)
90
+ first_page_criteria.page_number = page_number
91
+ first_page_criteria.page_size = page_size
92
+ super().__init__(user, first_page_criteria)
93
+
94
+
95
+ class QuickReportDictAutoPager(_DictReportPagerBase):
96
+ """
97
+ A class that automatically pages through a quick report and returns the results as a list of dictionaries.
98
+ """
99
+ def __init__(self, user: UserIdentifier, report_term: RawReportTerm,
100
+ page_number: int = 0, page_size: int = _default_report_page_size):
101
+ """
102
+ :param user: The current webhook context or a user object to send requests from.
103
+ :param report_term: The raw report term to use for the quick report.
104
+ :param page_number: The page number to start on. The first page is page 0.
105
+ :param page_size: The number of results to return per page.
106
+ """
107
+ first_page_criteria: CustomReportCriteria = CustomReportUtil.get_quick_report_criteria(user, report_term)
108
+ first_page_criteria.page_number = page_number
109
+ first_page_criteria.page_size = page_size
110
+ super().__init__(user, first_page_criteria)
111
+
112
+
113
+ class _RecordReportPagerBase(SapioPyAutoPager[CustomReportCriteria, WrappedType], ABC):
114
+ """
115
+ A base class for automatically paging through a report and returning the results as a list of records.
116
+ """
117
+ _columns: list[ReportColumn]
118
+ _wrapper: type[WrappedType]
119
+ _data_type: str
120
+ _rec_handler: RecordHandler
121
+ _report_man: CustomReportManager
122
+
123
+ def __init__(self, user: UserIdentifier, first_page_criteria: CustomReportCriteria, wrapper_type: type[WrappedType]):
124
+ self._columns = first_page_criteria.column_list
125
+ self._wrapper = wrapper_type
126
+ self._data_type = wrapper_type.get_wrapper_data_type_name()
127
+ self._rec_handler = RecordHandler(user)
128
+ super().__init__(AliasUtil.to_sapio_user(user), first_page_criteria)
129
+ self._report_man = DataMgmtServer.get_custom_report_manager(self.user)
130
+
131
+ def get_all_at_once(self) -> list[RecordModel]:
132
+ """
133
+ Get the results of all pages. Be cautious of client memory usage.
134
+ """
135
+ if self.has_iterated:
136
+ raise BrokenPipeError("Cannot use this method if the iterator has already been used.")
137
+ return [x for x in self]
138
+
139
+ def default_first_page_criteria(self) -> PagerResultCriteriaType:
140
+ raise ValueError("Cannot generate a default first page criteria for custom reports.")
141
+
142
+ def get_next_page_result(self) -> tuple[CustomReportCriteria | None, Queue[WrappedType]]:
143
+ report: CustomReport = self._report_man.run_custom_report(self.next_page_criteria)
144
+ queue: Queue[WrappedType] = Queue()
145
+ id_index: int = -1
146
+ for i, column in enumerate(self._columns):
147
+ if column.data_type_name == self._data_type and column.data_field_name == "RecordId":
148
+ id_index = i
149
+ break
150
+ if id_index == -1:
151
+ raise SapioException(f"This report does not contain a Record ID column for the given record model type "
152
+ f"{self._data_type}.")
153
+ ids: list[int] = [row[id_index] for row in report.result_table]
154
+ for row in self._rec_handler.query_models_by_id(self._wrapper, ids, page_size=report.page_size):
155
+ queue.put(row)
156
+ if report.has_next_page:
157
+ next_page_criteria = copy(self.next_page_criteria)
158
+ next_page_criteria.page_number += 1
159
+ return next_page_criteria, queue
160
+ else:
161
+ return None, queue
162
+
163
+
164
+ class CustomReportRecordAutoPager(_RecordReportPagerBase):
165
+ """
166
+ A class that automatically pages through a custom report and returns the results as a list of records.
167
+ """
168
+ def __init__(self, user: UserIdentifier, report_criteria: CustomReportCriteria, wrapper_type: type[WrappedType],
169
+ page_number: int = 0, page_size: int = _default_record_page_size):
170
+ """
171
+ :param user: The current webhook context or a user object to send requests from.
172
+ :param report_criteria: The custom report criteria to run.
173
+ :param wrapper_type: The record model wrapper type to use for the records.
174
+ :param page_number: The page number to start on. The first page is page 0.
175
+ :param page_size: The number of results to return per page.
176
+ """
177
+ first_page_criteria: CustomReportCriteria = copy(report_criteria)
178
+ _add_record_id_column(first_page_criteria, wrapper_type)
179
+ first_page_criteria.page_number = page_number
180
+ first_page_criteria.page_size = page_size
181
+ super().__init__(user, first_page_criteria, wrapper_type)
182
+
183
+
184
+ class SystemReportRecordAutoPager(_RecordReportPagerBase):
185
+ """
186
+ A class that automatically pages through a system report and returns the results as a list of records.
187
+
188
+ System reports are also known as predefined searches in the system and must be defined in the data designer for
189
+ a specific data type. That is, saved searches created by users cannot be run using this function.
190
+ """
191
+ def __init__(self, user: UserIdentifier, report_name: str, wrapper_type: type[WrappedType],
192
+ page_number: int = 0, page_size: int = _default_record_page_size):
193
+ """
194
+ :param user: The current webhook context or a user object to send requests from.
195
+ :param report_name: The name of the system report to run.
196
+ :param wrapper_type: The record model wrapper type to use for the records.
197
+ :param page_number: The page number to start on. The first page is page 0.
198
+ :param page_size: The number of results to return per page.
199
+ """
200
+ first_page_criteria: CustomReportCriteria = CustomReportUtil.get_system_report_criteria(user, report_name)
201
+ _add_record_id_column(first_page_criteria, wrapper_type)
202
+ first_page_criteria.page_number = page_number
203
+ first_page_criteria.page_size = page_size
204
+ super().__init__(user, first_page_criteria, wrapper_type)
205
+
206
+
207
+ class QuickReportRecordAutoPager(_RecordReportPagerBase):
208
+ """
209
+ A class that automatically pages through a quick report and returns the results as a list of records.
210
+ """
211
+ def __init__(self, user: UserIdentifier, report_term: RawReportTerm, wrapper_type: type[WrappedType],
212
+ page_number: int = 0, page_size: int = _default_record_page_size):
213
+ """
214
+ :param user: The current webhook context or a user object to send requests from.
215
+ :param report_term: The raw report term to use for the quick report.
216
+ :param wrapper_type: The record model wrapper type to use for the records.
217
+ :param page_number: The page number to start on. The first page is page 0.
218
+ :param page_size: The number of results to return per page.
219
+ """
220
+ if report_term.data_type_name != wrapper_type.get_wrapper_data_type_name():
221
+ raise SapioException("The data type name of the report term must match the data type name of the wrapper type.")
222
+ first_page_criteria: CustomReportCriteria = CustomReportUtil.get_quick_report_criteria(user, report_term)
223
+ first_page_criteria.page_number = page_number
224
+ first_page_criteria.page_size = page_size
225
+ super().__init__(user, first_page_criteria, wrapper_type)
226
+
227
+
228
+ def _add_record_id_column(report: CustomReportCriteria, wrapper_type: type[WrappedType]) -> None:
229
+ """
230
+ Given a custom report criteria, ensure that the report contains a Record ID column for the given record model's
231
+ data type. Add one if it is missing.
232
+ """
233
+ dt: str = wrapper_type.get_wrapper_data_type_name()
234
+ # Ensure that the root data type is the one we're looking for.
235
+ report.root_data_type = dt
236
+ # Enforce that the given custom report has a record ID column.
237
+ if not any([x.data_type_name == dt and x.data_field_name == "RecordId" for x in report.column_list]):
238
+ report.column_list.append(ReportColumn(dt, "RecordId", FieldType.LONG))
239
+
240
+
241
+ def _process_results(rows: list[list[FieldValue]], columns: list[ReportColumn]) -> list[dict[str, FieldValue]]:
242
+ """
243
+ Given the results of a report as a list of row values and the report's columns, combine these lists to
244
+ result in a singular list of dictionaries for each row in the results.
245
+ """
246
+ # It may be the case that two columns have the same data field name but differing data type names.
247
+ # If this occurs, then we need to be able to differentiate these columns in the resulting dictionary.
248
+ prepend_dt: set[str] = set()
249
+ encountered_names: list[str] = []
250
+ for column in columns:
251
+ field_name: str = column.data_field_name
252
+ if field_name in encountered_names:
253
+ prepend_dt.add(field_name)
254
+ else:
255
+ encountered_names.append(field_name)
256
+
257
+ ret: list[dict[str, FieldValue]] = []
258
+ for row in rows:
259
+ row_data: dict[str, FieldValue] = {}
260
+ filter_row: bool = False
261
+ for value, column in zip(row, columns):
262
+ header: str = column.data_field_name
263
+ # If two columns share the same data field name, prepend the data type name of the column to the
264
+ # data field name.
265
+ if header in prepend_dt:
266
+ header = column.data_type_name + "." + header
267
+ row_data.update({header: value})
268
+ if filter_row is False:
269
+ ret.append(row_data)
270
+ return ret
@@ -58,4 +58,4 @@ class ProcessWorkflowTrackingFields:
58
58
  WORKFLOW_PROCESS_TAT__FIELD = WrapperField("WorkflowProcessTAT", FieldType.DOUBLE)
59
59
  WORKFLOW_START_USER_ID__FIELD = WrapperField("WorkflowStartUserId", FieldType.STRING)
60
60
  WORKFLOW_TAT__FIELD = WrapperField("WorkflowTAT", FieldType.DOUBLE)
61
- WORKFLOW_VERSION__FIELD = WrapperField("WorkflowVersion", FieldType.LONG)
61
+ WORKFLOW_VERSION__FIELD = WrapperField("WorkflowVersion", FieldType.LONG)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from collections.abc import Mapping, Iterable
5
+ from typing import TypeAlias
5
6
  from weakref import WeakValueDictionary
6
7
 
7
8
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
@@ -28,7 +29,7 @@ from sapiopycommons.general.aliases import AliasUtil, SapioRecord, ExperimentIde
28
29
  DataTypeIdentifier, RecordModel
29
30
  from sapiopycommons.general.exceptions import SapioException
30
31
 
31
- Step = str | ElnEntryStep
32
+ Step: TypeAlias = str | ElnEntryStep
32
33
  """An object representing an identifier to an ElnEntryStep. May be either the name of the step or the ElnEntryStep
33
34
  itself."""
34
35
 
@@ -6,13 +6,13 @@ from sapiopylib.rest.pojo.eln.ElnExperiment import ElnExperiment, ElnExperimentQ
6
6
  from sapiopylib.rest.pojo.eln.SapioELNEnums import ElnExperimentStatus, ElnBaseDataType
7
7
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
8
8
 
9
+ from sapiopycommons.customreport.auto_pagers import CustomReportDictAutoPager
9
10
  from sapiopycommons.customreport.custom_report_builder import CustomReportBuilder
10
11
  from sapiopycommons.customreport.term_builder import TermBuilder
11
12
  from sapiopycommons.datatype.pseudo_data_types import EnbEntryOptionsPseudoDef, NotebookExperimentOptionPseudoDef, \
12
13
  NotebookExperimentPseudoDef, ExperimentEntryRecordPseudoDef, EnbEntryPseudoDef
13
14
  from sapiopycommons.general.aliases import SapioRecord, UserIdentifier, AliasUtil, FieldValue, \
14
15
  ExperimentEntryIdentifier, ExperimentIdentifier
15
- from sapiopycommons.general.custom_report_util import CustomReportUtil
16
16
  from sapiopycommons.general.exceptions import SapioException
17
17
  from sapiopycommons.recordmodel.record_handler import RecordHandler
18
18
 
@@ -201,7 +201,7 @@ class ExperimentReportUtil:
201
201
  criteria = report_builder.build_report_criteria()
202
202
 
203
203
  ret_val: dict[SapioRecord, int] = {}
204
- rows: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, criteria)
204
+ rows: list[dict[str, FieldValue]] = CustomReportDictAutoPager(context, criteria).get_all_at_once()
205
205
  for row in rows:
206
206
  dt: str = row[EnbEntryPseudoDef.DATA_TYPE_NAME__FIELD_NAME.field_name]
207
207
  exp_id: int = row[EnbEntryPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
@@ -268,7 +268,7 @@ class ExperimentReportUtil:
268
268
 
269
269
  # Ensure that each experiment appears in the dictionary, even if it has no experiment options.
270
270
  options: dict[int, dict[str, str]] = {x: {} for x in exp_ids}
271
- results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
271
+ results: list[dict[str, FieldValue]] = CustomReportDictAutoPager(context, report).get_all_at_once()
272
272
  for row in results:
273
273
  exp_id: int = row[NotebookExperimentOptionPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
274
274
  key: str = row[NotebookExperimentOptionPseudoDef.OPTION_KEY__FIELD_NAME.field_name]
@@ -299,7 +299,7 @@ class ExperimentReportUtil:
299
299
 
300
300
  # Ensure that each entry appears in the dictionary, even if it has no entry options.
301
301
  options: dict[int, dict[str, str]] = {x: {} for x in entries}
302
- results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
302
+ results: list[dict[str, FieldValue]] = CustomReportDictAutoPager(context, report).get_all_at_once()
303
303
  for row in results:
304
304
  entry_id: int = row[EnbEntryOptionsPseudoDef.ENTRY_ID__FIELD_NAME.field_name]
305
305
  key: str = row[EnbEntryOptionsPseudoDef.ENTRY_OPTION_KEY__FIELD_NAME.field_name]
@@ -332,7 +332,7 @@ class ExperimentReportUtil:
332
332
  report = report_builder.build_report_criteria()
333
333
 
334
334
  ret_val: dict[int, str] = {}
335
- results: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(context, report)
335
+ results: list[dict[str, FieldValue]] = CustomReportDictAutoPager(context, report).get_all_at_once()
336
336
  for row in results:
337
337
  exp_id: int = row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name]
338
338
  name: str = row["TemplateExperimentName"]
@@ -581,7 +581,7 @@ class ExperimentReportUtil:
581
581
  """
582
582
  user = AliasUtil.to_sapio_user(context)
583
583
  exp_ids: list[int] = []
584
- for row in CustomReportUtil.run_custom_report(user, report):
584
+ for row in CustomReportDictAutoPager(user, report):
585
585
  exp_ids.append(row[NotebookExperimentPseudoDef.EXPERIMENT_ID__FIELD_NAME.field_name])
586
586
  if not exp_ids:
587
587
  return []
@@ -646,4 +646,4 @@ class ExperimentReportUtil:
646
646
  report_builder.add_join(records_entry_join, ExperimentEntryRecordPseudoDef.DATA_TYPE_NAME)
647
647
  report_builder.add_join(experiment_entry_enb_entry_join, EnbEntryPseudoDef.DATA_TYPE_NAME)
648
648
  report_builder.add_join(enb_entry_experiment_join, NotebookExperimentPseudoDef.DATA_TYPE_NAME)
649
- return CustomReportUtil.run_custom_report(user, report_builder.build_report_criteria())
649
+ return CustomReportDictAutoPager(user, report_builder.build_report_criteria()).get_all_at_once()
@@ -1,6 +1,7 @@
1
1
  import base64
2
2
  import io
3
3
  import urllib.parse
4
+ from typing import Any
4
5
 
5
6
  from requests import Response
6
7
  from sapiopylib.rest.User import SapioUser
@@ -8,6 +9,36 @@ from sapiopylib.rest.User import SapioUser
8
9
  from sapiopycommons.general.aliases import UserIdentifier, AliasUtil
9
10
 
10
11
 
12
+ # FR-47387: Add support for the metadata endpoints in FileBridge.
13
+ class FileBridgeMetadata:
14
+ """
15
+ Metadata for a file or directory in FileBridge.
16
+ """
17
+ file_name: str
18
+ """The name of the file or directory."""
19
+ is_file: bool
20
+ """True if the metadata is for a file, False if it is for a directory."""
21
+ is_directory: bool
22
+ """True if the metadata is for a directory, False if it is for a file."""
23
+ size: int
24
+ """The size of the file in bytes. For directories, this value will always be zero."""
25
+ creation_time: int
26
+ """The time the file or directory was created, in milliseconds since the epoch."""
27
+ last_accessed_time: int
28
+ """The time the file or directory was last accessed, in milliseconds since the epoch."""
29
+ last_modified_time: int
30
+ """The time the file or directory was last modified, in milliseconds since the epoch."""
31
+
32
+ def __init__(self, json_dict: dict[str, Any]):
33
+ self.file_name = json_dict['fileName']
34
+ self.is_file = json_dict['isFile']
35
+ self.is_directory = json_dict['isDirectory']
36
+ self.size = json_dict['size']
37
+ self.creation_time = json_dict['creationTime']
38
+ self.last_accessed_time = json_dict['lastAccessTime']
39
+ self.last_modified_time = json_dict['lastModifiedTime']
40
+
41
+
11
42
  # FR-46064 - Initial port of PyWebhookUtils to sapiopycommons.
12
43
  class FileBridge:
13
44
  @staticmethod
@@ -137,3 +168,48 @@ class FileBridge:
137
168
  user: SapioUser = AliasUtil.to_sapio_user(context)
138
169
  response = user.delete(sub_path, params=params)
139
170
  user.raise_for_status(response)
171
+
172
+ @staticmethod
173
+ def file_metadata(context: UserIdentifier, bridge_name: str, file_path: str) -> FileBridgeMetadata:
174
+ """
175
+ Get metadata for a file or directory in FileBridge.
176
+
177
+ The file path may be to a directory, in which case only the metadata for that directory will be returned. If you
178
+ want the metadata for the contents of a directory, then use the directory_metadata function.
179
+
180
+ :param context: The current webhook context or a user object to send requests from.
181
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
182
+ file bridge configurations.
183
+ :param file_path: The path to the file to retrieve the metadata from.
184
+ :return: The metadata for the file.
185
+ """
186
+ sub_path = '/ext/filebridge/file/metadata'
187
+ params = {
188
+ 'Filepath': f"bridge://{bridge_name}/{file_path}"
189
+ }
190
+ user: SapioUser = AliasUtil.to_sapio_user(context)
191
+ response = user.get(sub_path, params=params)
192
+ user.raise_for_status(response)
193
+ response_body: dict[str, Any] = response.json()
194
+ return FileBridgeMetadata(response_body)
195
+
196
+ @staticmethod
197
+ def directory_metadata(context: UserIdentifier, bridge_name: str, file_path: str) -> list[FileBridgeMetadata]:
198
+ """
199
+ Get metadata for every file or nested directory in a directory in FileBridge.
200
+
201
+ :param context: The current webhook context or a user object to send requests from.
202
+ :param bridge_name: The name of the bridge to use. This is the "connection name" in the
203
+ file bridge configurations.
204
+ :param file_path: The path to the directory to retrieve the metadata of the contents.
205
+ :return: A list of the metadata for the contents of the directory.
206
+ """
207
+ sub_path = '/ext/filebridge/directory/metadata'
208
+ params = {
209
+ 'Filepath': f"bridge://{bridge_name}/{file_path}"
210
+ }
211
+ user: SapioUser = AliasUtil.to_sapio_user(context)
212
+ response = user.get(sub_path, params=params)
213
+ user.raise_for_status(response)
214
+ response_body: list[dict[str, Any]] = response.json()
215
+ return [FileBridgeMetadata(x) for x in response_body]