sapiopycommons 2025.8.14a703__py3-none-any.whl → 2026.1.22a847__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.
Files changed (57) hide show
  1. sapiopycommons/ai/agent_service_base.py +2051 -0
  2. sapiopycommons/ai/converter_service_base.py +163 -0
  3. sapiopycommons/ai/external_credentials.py +131 -0
  4. sapiopycommons/ai/protoapi/agent/agent_pb2.py +87 -0
  5. sapiopycommons/ai/protoapi/agent/agent_pb2.pyi +282 -0
  6. sapiopycommons/ai/protoapi/agent/agent_pb2_grpc.py +154 -0
  7. sapiopycommons/ai/protoapi/agent/entry_pb2.py +49 -0
  8. sapiopycommons/ai/protoapi/agent/entry_pb2.pyi +40 -0
  9. sapiopycommons/ai/protoapi/agent/entry_pb2_grpc.py +24 -0
  10. sapiopycommons/ai/protoapi/agent/item/item_container_pb2.py +61 -0
  11. sapiopycommons/ai/protoapi/agent/item/item_container_pb2.pyi +181 -0
  12. sapiopycommons/ai/protoapi/agent/item/item_container_pb2_grpc.py +24 -0
  13. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.py +41 -0
  14. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.pyi +36 -0
  15. sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2_grpc.py +24 -0
  16. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py +51 -0
  17. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi +59 -0
  18. sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py +24 -0
  19. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py +123 -0
  20. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi +599 -0
  21. sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py +24 -0
  22. sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.py +59 -0
  23. sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.pyi +68 -0
  24. sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2_grpc.py +149 -0
  25. sapiopycommons/ai/protoapi/pipeline/script/script_pb2.py +69 -0
  26. sapiopycommons/ai/protoapi/pipeline/script/script_pb2.pyi +109 -0
  27. sapiopycommons/ai/protoapi/pipeline/script/script_pb2_grpc.py +153 -0
  28. sapiopycommons/ai/protoapi/pipeline/step_output_pb2.py +49 -0
  29. sapiopycommons/ai/protoapi/pipeline/step_output_pb2.pyi +56 -0
  30. sapiopycommons/ai/protoapi/pipeline/step_output_pb2_grpc.py +24 -0
  31. sapiopycommons/ai/protoapi/pipeline/step_pb2.py +43 -0
  32. sapiopycommons/ai/protoapi/pipeline/step_pb2.pyi +44 -0
  33. sapiopycommons/ai/protoapi/pipeline/step_pb2_grpc.py +24 -0
  34. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py +39 -0
  35. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi +33 -0
  36. sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py +24 -0
  37. sapiopycommons/ai/protobuf_utils.py +583 -0
  38. sapiopycommons/ai/request_validation.py +561 -0
  39. sapiopycommons/ai/server.py +152 -0
  40. sapiopycommons/ai/test_client.py +534 -0
  41. sapiopycommons/callbacks/callback_util.py +26 -7
  42. sapiopycommons/eln/experiment_handler.py +12 -5
  43. sapiopycommons/files/file_util.py +128 -1
  44. sapiopycommons/files/temp_files.py +82 -0
  45. sapiopycommons/general/aliases.py +4 -1
  46. sapiopycommons/general/macros.py +172 -0
  47. sapiopycommons/general/time_util.py +199 -4
  48. sapiopycommons/recordmodel/record_handler.py +47 -12
  49. sapiopycommons/rules/eln_rule_handler.py +3 -0
  50. sapiopycommons/rules/on_save_rule_handler.py +3 -0
  51. sapiopycommons/webhook/webservice_handlers.py +1 -1
  52. {sapiopycommons-2025.8.14a703.dist-info → sapiopycommons-2026.1.22a847.dist-info}/METADATA +2 -2
  53. sapiopycommons-2026.1.22a847.dist-info/RECORD +113 -0
  54. sapiopycommons/ai/tool_of_tools.py +0 -917
  55. sapiopycommons-2025.8.14a703.dist-info/RECORD +0 -72
  56. {sapiopycommons-2025.8.14a703.dist-info → sapiopycommons-2026.1.22a847.dist-info}/WHEEL +0 -0
  57. {sapiopycommons-2025.8.14a703.dist-info → sapiopycommons-2026.1.22a847.dist-info}/licenses/LICENSE +0 -0
@@ -133,9 +133,10 @@ class TimeUtil:
133
133
  return datetime.fromtimestamp(millis / 1000, tz).strftime(time_format)
134
134
 
135
135
  @staticmethod
136
- def format_to_millis(time_point: str, time_format: str, timezone: str | int = None) -> int:
136
+ def format_to_millis(time_point: str, time_format: str, timezone: str | int = None) -> int | None:
137
137
  """
138
- Convert the input time from the provided format to milliseconds.
138
+ Convert the input time from the provided format to milliseconds. If None is passed to the time_point parameter,
139
+ None will be returned.
139
140
 
140
141
  :param time_point: The time in some date/time format to convert from.
141
142
  :param time_format: The format that the time_point is in. Documentation for how the time formatting works
@@ -144,6 +145,9 @@ class TimeUtil:
144
145
  timezone variable set by the TimeUtil. A list of valid timezones can be found at
145
146
  https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. May also accept a UTC offset in seconds.
146
147
  """
148
+ if time_point is None:
149
+ return None
150
+
147
151
  tz = TimeUtil.__to_tz(timezone)
148
152
  return int(datetime.strptime(time_point, time_format).replace(tzinfo=tz).timestamp() * 1000)
149
153
 
@@ -166,11 +170,12 @@ class TimeUtil:
166
170
  return TimeUtil.shift_millis(millis, to_timezone, from_timezone)
167
171
 
168
172
  @staticmethod
169
- def shift_millis(millis: int, to_timezone: str = "UTC", from_timezone: str | None = None) -> int:
173
+ def shift_millis(millis: int, to_timezone: str = "UTC", from_timezone: str | None = None) -> int | None:
170
174
  """
171
175
  Take a number of milliseconds for a time in from_timezone and output the epoch timestamp that would display that
172
176
  same time in to_timezone. A use case for this is when dealing with static date fields to convert a provided
173
177
  timestamp to the value necessary to display that timestamp in the same way when viewed in the static date field.
178
+ If None is passed to the millis parameter, None will be returned.
174
179
 
175
180
  :param millis: The time in milliseconds to convert from.
176
181
  :param to_timezone: The timezone to shift to. If not provided, uses UTC.
@@ -180,17 +185,21 @@ class TimeUtil:
180
185
  :return: The epoch timestamp that would display as the same time in to_timezone as the given time in
181
186
  from_timezone.
182
187
  """
188
+ if millis is None:
189
+ return None
190
+
183
191
  to_offset: int = TimeUtil.__get_timezone_offset(to_timezone) * 1000
184
192
  from_offset: int = TimeUtil.__get_timezone_offset(from_timezone) * 1000
185
193
  return millis + from_offset - to_offset
186
194
 
187
195
  @staticmethod
188
196
  def shift_format(time_point: str, time_format: str, to_timezone: str = "UTC", from_timezone: str | None = None) \
189
- -> int:
197
+ -> int | None:
190
198
  """
191
199
  Take a timestamp for a time in from_timezone and output the epoch timestamp that would display that same time
192
200
  in to_timezone. A use case for this is when dealing with static date fields to convert a provided timestamp to
193
201
  the value necessary to display that timestamp in the same way when viewed in the static date field.
202
+ If None is passed to the time_point parameter, None will be returned.
194
203
 
195
204
  :param time_point: The time in some date/time format to convert from.
196
205
  :param time_format: The format that the time_point is in. Documentation for how the time formatting works
@@ -202,6 +211,9 @@ class TimeUtil:
202
211
  :return: The epoch timestamp that would display as the same time in to_timezone as the given time in
203
212
  from_timezone.
204
213
  """
214
+ if time_point is None:
215
+ return None
216
+
205
217
  millis: int = TimeUtil.format_to_millis(time_point, time_format, from_timezone)
206
218
  return TimeUtil.shift_millis(millis, to_timezone, from_timezone)
207
219
 
@@ -215,9 +227,192 @@ class TimeUtil:
215
227
  :param time_format: The format that the time_point should be in. Documentation for how the time formatting works
216
228
  can be found at https://docs.python.org/3.10/library/datetime.html#strftime-and-strptime-behavior
217
229
  """
230
+ if time_point is None:
231
+ return False
218
232
  try:
219
233
  # If this function successfully runs, then the time_point matches the time_format.
220
234
  TimeUtil.format_to_millis(time_point, time_format, "UTC")
221
235
  return True
222
236
  except Exception:
223
237
  return False
238
+
239
+
240
+ MILLISECONDS_IN_A_SECOND: int = 1000
241
+ MILLISECONDS_IN_A_MINUTE: int = 60 * MILLISECONDS_IN_A_SECOND
242
+ MILLISECONDS_IN_AN_HOUR: int = 60 * MILLISECONDS_IN_A_MINUTE
243
+ MILLISECONDS_IN_A_DAY: int = 24 * MILLISECONDS_IN_AN_HOUR
244
+
245
+ class ElapsedTime:
246
+ _start: int
247
+ _end: int
248
+
249
+ _total_days: float
250
+ _total_hours: float
251
+ _total_minutes: float
252
+ _total_seconds: float
253
+ _total_milliseconds: int
254
+
255
+ _days: int
256
+ _hours: int
257
+ _minutes: int
258
+ _seconds: int
259
+ _milliseconds: int
260
+
261
+ def __init__(self, start: int | float, end: int | float | None = None):
262
+ """
263
+ :param start: The start timestamp in milliseconds (int) or seconds (float).
264
+ :param end: The end timestamp in milliseconds (int) or seconds (float). If None, uses the current epoch time in
265
+ milliseconds.
266
+ """
267
+ if isinstance(start, float):
268
+ start = int(start * 1000)
269
+
270
+ if end is None:
271
+ end = TimeUtil.now_in_millis()
272
+ elif isinstance(end, float):
273
+ end = int(end * 1000)
274
+
275
+ self._start = start
276
+ self._end = end
277
+
278
+ self._total_milliseconds = end - start
279
+ self._total_seconds = self._total_milliseconds / MILLISECONDS_IN_A_SECOND
280
+ self._total_minutes = self._total_milliseconds / MILLISECONDS_IN_A_MINUTE
281
+ self._total_hours = self._total_milliseconds / MILLISECONDS_IN_AN_HOUR
282
+ self._total_days = self._total_milliseconds / MILLISECONDS_IN_A_DAY
283
+
284
+ elapsed: int = end - start
285
+ self._days: int = elapsed // MILLISECONDS_IN_A_DAY
286
+ elapsed -= self._days * MILLISECONDS_IN_A_DAY
287
+ self._hours: int = elapsed // MILLISECONDS_IN_AN_HOUR
288
+ elapsed -= self._hours * MILLISECONDS_IN_AN_HOUR
289
+ self._minutes: int = elapsed // MILLISECONDS_IN_A_MINUTE
290
+ elapsed -= self._minutes * MILLISECONDS_IN_A_MINUTE
291
+ self._seconds: int = elapsed // MILLISECONDS_IN_A_SECOND
292
+ elapsed -= self._seconds * MILLISECONDS_IN_A_SECOND
293
+ self._milliseconds = elapsed
294
+
295
+ @staticmethod
296
+ def as_eta(total: int, progress: int, start: int | float, now: int | float | None = None) -> ElapsedTime:
297
+ """
298
+ Calculate the estimated time remaining for a task based on how much time has passed so far and how many items
299
+ have been completed. The estimated time remaining is calculated by determining the average time per item
300
+ completed so far and multiplying that by the number of items remaining.
301
+
302
+ :param total: The total number of items that need to be completed for the task.
303
+ :param progress: The number of items that have been completed so far.
304
+ :param start: The start time of a task in milliseconds (int) or seconds (float).
305
+ :param now: The amount of time that has passed so far while performing the task in milliseconds (int)
306
+ or seconds (float). If None, uses the current epoch time in milliseconds.
307
+ :return: An ElapsedTime object representing the estimated time remaining.
308
+ """
309
+ if now is None:
310
+ now = TimeUtil.now_in_millis()
311
+ is_int: bool = isinstance(start, int) and isinstance(now, int)
312
+ # How much time has elapsed so far.
313
+ elapsed: int | float = now - start
314
+ # The average time it has taken to complete each record so far.
315
+ per_record: int | float = (elapsed // progress) if is_int else (elapsed / progress)
316
+ # The estimated time remaining based on the average time per record.
317
+ remaining: int | float = (total - progress) * per_record
318
+ return ElapsedTime(now, now + remaining)
319
+
320
+ def __str__(self) -> str:
321
+ time_str: str = f"{self._hours:02d}:{self._minutes:02d}:{self._seconds:02d}.{self._milliseconds:03d}"
322
+ if self._days:
323
+ return f"{self._days}d {time_str}"
324
+ return time_str
325
+
326
+ @property
327
+ def start(self) -> int:
328
+ """
329
+ The start timestamp in milliseconds.
330
+ """
331
+ return self._start
332
+
333
+ @property
334
+ def end(self) -> int:
335
+ """
336
+ The end timestamp in milliseconds.
337
+ """
338
+ return self._end
339
+
340
+ @property
341
+ def total_days(self) -> float:
342
+ """
343
+ The total number of days in the elapsed time. For example, an elapsed time of 1.5 days would
344
+ return 1.5.
345
+ """
346
+ return self._total_days
347
+
348
+ @property
349
+ def total_hours(self) -> float:
350
+ """
351
+ The total number of hours in the elapsed time. For example, an elapsed time of 1.5 days would
352
+ return 36.0.
353
+ """
354
+ return self._total_hours
355
+
356
+ @property
357
+ def total_minutes(self) -> float:
358
+ """
359
+ The total number of minutes in the elapsed time. For example, an elapsed time of 1.5 days would
360
+ return 2,160.0.
361
+ """
362
+ return self._total_minutes
363
+
364
+ @property
365
+ def total_seconds(self) -> float:
366
+ """
367
+ The total number of seconds in the elapsed time. For example, an elapsed time of 1.5 days would
368
+ return 129,600.0.
369
+ """
370
+ return self._total_seconds
371
+
372
+ @property
373
+ def total_milliseconds(self) -> int:
374
+ """
375
+ The total number of milliseconds in the elapsed time. For example, an elapsed time of 1.5 days would
376
+ return 129,600,000.
377
+ """
378
+ return self._total_milliseconds
379
+
380
+ @property
381
+ def days(self) -> int:
382
+ """
383
+ The number of full days in the elapsed time. For example, an elapsed time of 1.5 days would
384
+ return 1.
385
+ """
386
+ return self._days
387
+
388
+ @property
389
+ def hours(self) -> int:
390
+ """
391
+ The number of full hours in the elapsed time, not counting full days. For example, an elapsed time of 1.5 days
392
+ would return 12.
393
+ """
394
+ return self._hours
395
+
396
+ @property
397
+ def minutes(self) -> int:
398
+ """
399
+ The number of full minutes in the elapsed time, not counting full hours. For example, an elapsed time of
400
+ 1.5 hours would return 30.
401
+ """
402
+ return self._minutes
403
+
404
+ @property
405
+ def seconds(self) -> int:
406
+ """
407
+ The number of full seconds in the elapsed time, not counting full minutes. For example, an elapsed time of 1
408
+ minute and 45 seconds would return 45.
409
+ """
410
+ return self._seconds
411
+
412
+ @property
413
+ def milliseconds(self) -> int:
414
+ """
415
+ The number of milliseconds in the elapsed time, not counting full seconds. For example, an elapsed time of
416
+ 1.25 seconds would return 250.
417
+ """
418
+ return self._milliseconds
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import io
4
4
  import warnings
5
5
  from collections.abc import Iterable
6
- from typing import Collection, TypeVar, TypeAlias
6
+ from typing import Collection, TypeVar
7
7
  from weakref import WeakValueDictionary
8
8
 
9
9
  from sapiopycommons.general.aliases import RecordModel, SapioRecord, FieldMap, FieldIdentifier, AliasUtil, \
@@ -31,13 +31,6 @@ from sapiopylib.rest.utils.recordmodel.ancestry import RecordModelAncestorManage
31
31
  from sapiopylib.rest.utils.recordmodel.properties import Parents, Parent, Children, Child, ForwardSideLink, \
32
32
  ReverseSideLink
33
33
 
34
- # Aliases for longer name.
35
- _PropertyGetter: TypeAlias = AbstractRecordModelPropertyGetter
36
- _PropertyAdder: TypeAlias = AbstractRecordModelPropertyAdder
37
- _PropertyRemover: TypeAlias = AbstractRecordModelPropertyRemover
38
- _PropertySetter: TypeAlias = AbstractRecordModelPropertySetter
39
- _PropertyType: TypeAlias = RecordModelPropertyType
40
-
41
34
  # CR-47717: Use TypeVars in the type hints of certain functions to prevent PyCharm from erroneously flagging certain
42
35
  # return type hints as incorrect.
43
36
  IsRecordModel = TypeVar('IsRecordModel', bound=RecordModel)
@@ -100,6 +93,10 @@ class RecordHandler:
100
93
  PyRecordModel instead of a WrappedRecordModel.
101
94
  :return: The record model for the input.
102
95
  """
96
+ # PR-47792: Set the wrapper_type to None if a str was provided instead of a type[WrappedType]. The type hints
97
+ # say this shouldn't be done anyway, but using this as a safeguard against user error.
98
+ if isinstance(wrapper_type, str):
99
+ wrapper_type = None
103
100
  if wrapper_type is not None:
104
101
  self.__verify_data_type(record, wrapper_type)
105
102
  if isinstance(record, PyRecordModel):
@@ -642,6 +639,40 @@ class RecordHandler:
642
639
  with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
643
640
  self.dr_man.set_record_image(record, stream)
644
641
 
642
+ def get_file_blob_data(self, record: SapioRecord, field_name: FieldIdentifier) -> bytes:
643
+ """
644
+ Retrieve file blob data for a given record from one of its file blob fields.
645
+
646
+ :param record: The record model to retrieve from.
647
+ :param field_name: The name of the file blob field to retrieve the data from.
648
+ :return: The file bytes of the given record's file blob data for the input field.
649
+ """
650
+ record: DataRecord = AliasUtil.to_data_record(record)
651
+ field_name: str = AliasUtil.to_data_field_name(field_name)
652
+ with io.BytesIO() as data_sink:
653
+ def consume_data(chunk: bytes):
654
+ data_sink.write(chunk)
655
+
656
+ self.dr_man.get_file_blob_data(record, field_name, consume_data)
657
+ data_sink.flush()
658
+ data_sink.seek(0)
659
+ file_bytes = data_sink.read()
660
+ return file_bytes
661
+
662
+ def set_file_blob_data(self, record: SapioRecord, field_name: FieldIdentifier, file_name: str, file_data: str | bytes) -> None:
663
+ """
664
+ Set the file blob data for a given record on one of its file blob fields.
665
+
666
+ :param record: The record model to set the file blob data of.
667
+ :param field_name: The name of the file blob field to set the data for.
668
+ :param file_name: The name of the file being stored in the file blob field.
669
+ :param file_data: The file data of the blob to set on the record.
670
+ """
671
+ record: DataRecord = AliasUtil.to_data_record(record)
672
+ field_name: str = AliasUtil.to_data_field_name(field_name)
673
+ with io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data) as stream:
674
+ self.dr_man.set_file_blob_data(record, field_name, file_name, stream)
675
+
645
676
  @staticmethod
646
677
  def sum_of_field(models: Iterable[SapioRecord], field_name: FieldIdentifier) -> float:
647
678
  """
@@ -783,7 +814,8 @@ class RecordHandler:
783
814
  return [{field_name: value} for value in values]
784
815
 
785
816
  @staticmethod
786
- def get_from_all(records: Iterable[RecordModel], getter: _PropertyGetter[_PropertyType]) \
817
+ def get_from_all(records: Iterable[RecordModel],
818
+ getter: AbstractRecordModelPropertyGetter[RecordModelPropertyType]) \
787
819
  -> list[RecordModelPropertyType]:
788
820
  """
789
821
  Use a getter property on all records in a list of record models. For example, you can iterate over a list of
@@ -798,7 +830,8 @@ class RecordHandler:
798
830
  return [x.get(getter) for x in records]
799
831
 
800
832
  @staticmethod
801
- def set_on_all(records: Iterable[RecordModel], setter: _PropertySetter[_PropertyType]) \
833
+ def set_on_all(records: Iterable[RecordModel],
834
+ setter: AbstractRecordModelPropertySetter[RecordModelPropertyType]) \
802
835
  -> list[RecordModelPropertyType]:
803
836
  """
804
837
  Use a setter property on all records in a list of record models. For example, you can iterate over a list of
@@ -813,7 +846,8 @@ class RecordHandler:
813
846
  return [x.set(setter) for x in records]
814
847
 
815
848
  @staticmethod
816
- def add_to_all(records: Iterable[RecordModel], adder: _PropertyAdder[_PropertyType]) \
849
+ def add_to_all(records: Iterable[RecordModel],
850
+ adder: AbstractRecordModelPropertyAdder[RecordModelPropertyType]) \
817
851
  -> list[RecordModelPropertyType]:
818
852
  """
819
853
  Use an adder property on all records in a list of record models. For example, you can iterate over a list of
@@ -827,7 +861,8 @@ class RecordHandler:
827
861
  return [x.add(adder) for x in records]
828
862
 
829
863
  @staticmethod
830
- def remove_from_all(records: Iterable[RecordModel], remover: _PropertyRemover[_PropertyType]) \
864
+ def remove_from_all(records: Iterable[RecordModel],
865
+ remover: AbstractRecordModelPropertyRemover[RecordModelPropertyType]) \
831
866
  -> list[RecordModelPropertyType]:
832
867
  """
833
868
  Use a remover property on all records in a list of record models. For example, you can iterate over a list of
@@ -184,4 +184,7 @@ class ElnRuleHandler:
184
184
  instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
185
185
  """
186
186
  dt: str = AliasUtil.to_data_type_name(wrapper_type)
187
+ # PR-47792: Set the wrapper_type to None if a str was provided instead of a type[WrappedType].
188
+ if isinstance(wrapper_type, str):
189
+ wrapper_type = None
187
190
  return self._rec_handler.wrap_models(self.get_records(dt, entry), wrapper_type)
@@ -180,4 +180,7 @@ class OnSaveRuleHandler:
180
180
  instead of a model wrapper, then the returned records will be PyRecordModels instead of WrappedRecordModels.
181
181
  """
182
182
  dt: str = AliasUtil.to_data_type_name(wrapper_type)
183
+ # PR-47792: Set the wrapper_type to None if a str was provided instead of a type[WrappedType].
184
+ if isinstance(wrapper_type, str):
185
+ wrapper_type = None
183
186
  return self._rec_handler.wrap_models(self.get_records(dt, record_id), wrapper_type)
@@ -140,7 +140,7 @@ class AbstractWebserviceHandler(AbstractWebhookHandler):
140
140
  # Get the login credentials from the headers.
141
141
  auth: str = headers.get("Authorization")
142
142
  if auth and auth.startswith("Basic "):
143
- credentials: list[str] = b64decode(auth.split("Basic ")[1]).decode().split(":")
143
+ credentials: list[str] = b64decode(auth.split("Basic ")[1]).decode().split(":", 1)
144
144
  user = self.basic_auth(url, credentials[0], credentials[1])
145
145
  elif auth and auth.startswith("Bearer "):
146
146
  user = self.bearer_token_auth(url, auth.split("Bearer ")[1])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2025.8.14a703
3
+ Version: 2026.1.22a847
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>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: databind>=4.5
20
- Requires-Dist: sapiopylib>=2025.7.31a279
20
+ Requires-Dist: sapiopylib>=2025.10.7.295
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
 
@@ -0,0 +1,113 @@
1
+ sapiopycommons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sapiopycommons/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ sapiopycommons/ai/agent_service_base.py,sha256=EtImPyKZuRtHn2w0XjiqytHJPzDWOty6mY-2qciRvXk,108986
4
+ sapiopycommons/ai/converter_service_base.py,sha256=NjMCrpyoEh2TYrP9zN3SY0XtOir3IGWGPjU_snr6Usw,6360
5
+ sapiopycommons/ai/external_credentials.py,sha256=DKg3EMGrm6XSt09WbHWy3UHUphuakItiFeToluGSxlM,4427
6
+ sapiopycommons/ai/protobuf_utils.py,sha256=PeXpjfurg8EvvgM_jNLp0F1zAlV7cad8x0zewqi5Bhw,28235
7
+ sapiopycommons/ai/request_validation.py,sha256=fW3I_G6PUrJhpalQEfI1T5nAqcNUYYOVf_oJnlROrNM,30430
8
+ sapiopycommons/ai/server.py,sha256=VgHNvY2751qDZ8t7JTS67uxSnzOcpKdKc2Dick1p22g,6393
9
+ sapiopycommons/ai/test_client.py,sha256=hExCLASmqm59nTnhph9OIQpTSbde14cH4Sayik3Enpk,26350
10
+ sapiopycommons/ai/protoapi/agent/agent_pb2.py,sha256=dKKB52qOf7lCZWt9kAgqFu8bdhemMwGid8u0aC_Rb2s,10489
11
+ sapiopycommons/ai/protoapi/agent/agent_pb2.pyi,sha256=EkN98-gIGCbj4OuPOZMjZWDi-hhAgtThR0hR6Mk-FoM,20222
12
+ sapiopycommons/ai/protoapi/agent/agent_pb2_grpc.py,sha256=q4sac85YnJG6vjqNXKdNji3FaLSu0lEBnjKMKRBJm90,6851
13
+ sapiopycommons/ai/protoapi/agent/entry_pb2.py,sha256=EDFp_UNaWw_RnwrrEfACFRXUEUy18OaxPDB8ljWPKBk,3082
14
+ sapiopycommons/ai/protoapi/agent/entry_pb2.pyi,sha256=DMb3vd-O1cqo7Q_2ehx6ubFEa1SR_ho_qBrEvaIQTbk,2745
15
+ sapiopycommons/ai/protoapi/agent/entry_pb2_grpc.py,sha256=hiZtUu9Ij-zQoKeMGKt15-cRlxThdrfe8z5qxoKYKfk,918
16
+ sapiopycommons/ai/protoapi/agent/item/item_container_pb2.py,sha256=oBHslKp2eni0wicsuk5BfnWPLp6g5FzwR0CGBqfyqDQ,4830
17
+ sapiopycommons/ai/protoapi/agent/item/item_container_pb2.pyi,sha256=6JQ_i6DPs0lSTTSl8qImNsBg6aEMNjbyUUh6ARwz6YI,11626
18
+ sapiopycommons/ai/protoapi/agent/item/item_container_pb2_grpc.py,sha256=Uj5rcKQuXFU_dn1vnJEEblkK_kdh30BXQeBY0JgAkuI,932
19
+ sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.py,sha256=vJRtIbxRm-CM9WTfFlcn4V5oDCQeK1hRoVS9WwdYbjU,2481
20
+ sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2.pyi,sha256=3_vdkYM56c31cCVo6VvXDV9F4ENN7Mq20PAPHOHvHZw,1685
21
+ sapiopycommons/ai/protoapi/externalcredentials/external_credentials_pb2_grpc.py,sha256=mZLcay2VLNCuabBp8k2Sw3d6FcHXWFzze31Q3O0l7es,947
22
+ sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.py,sha256=Oral3k23576HIKu6mWIcZOuk87cvbpmRmiZJcU6kG1o,3344
23
+ sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2.pyi,sha256=tJXn_hO7Xii5ZQPCGvAYkrqiqnqNQxMEdKC6Wm_rmJE,2812
24
+ sapiopycommons/ai/protoapi/fielddefinitions/fields_pb2_grpc.py,sha256=scBvKFAGDWSAPJ4cnJCpvBNbROMjYGWF8y6gugOjudI,930
25
+ sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.py,sha256=CE4cM7UlT6fbTw0JafKiAH2jdEnmcuAzKYoF-3oYGcw,20864
26
+ sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2.pyi,sha256=h24h8dh6d4dsw0Bc9Oyfffdxv73aljfgt72ZJIXwxBw,34055
27
+ sapiopycommons/ai/protoapi/fielddefinitions/velox_field_def_pb2_grpc.py,sha256=hcd7T-_SbDWGG22-GigTihxCAPQ_hryCcX_hqFFF-KE,939
28
+ sapiopycommons/ai/protoapi/pipeline/step_output_pb2.py,sha256=nntHXqMbbgqhm9Ic17FMaIoR7CGIrzbzxknIAkTtr9w,3293
29
+ sapiopycommons/ai/protoapi/pipeline/step_output_pb2.pyi,sha256=Md2SaMnflALL_TnoAkRWrAsAYj0hMg-Yi76F0NfFnQg,3016
30
+ sapiopycommons/ai/protoapi/pipeline/step_output_pb2_grpc.py,sha256=7fWSkfpljHFr1RqFMuyIgQ87rlRBGluWLs02Bg_pJnc,927
31
+ sapiopycommons/ai/protoapi/pipeline/step_pb2.py,sha256=c5g5h4LIX1FKjVzoNZKku-jclnHhTCa64LBDLYd-BKA,2457
32
+ sapiopycommons/ai/protoapi/pipeline/step_pb2.pyi,sha256=tGFKu574nSLRURNhbxFcbajY66G-wfKIJm5D_dy58uk,2089
33
+ sapiopycommons/ai/protoapi/pipeline/step_pb2_grpc.py,sha256=mSdi68T_lXIxsaD23hIYaNk4TzxvNRAfD31xSLHX8lY,920
34
+ sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.py,sha256=4FfhtxyGMUyDUZO_GXQzmjsJU47z775XAprgTMjLHYk,4590
35
+ sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2.pyi,sha256=pe-cq7ITHC0egMs8Zz5dp2IPtC_QG_Hlt0FwTtqIFZs,4641
36
+ sapiopycommons/ai/protoapi/pipeline/converter/converter_pb2_grpc.py,sha256=lhmz45-cy9mJnRVZAD0T7s4pKmGQfw_Hg98DbVq9qHE,6507
37
+ sapiopycommons/ai/protoapi/pipeline/script/script_pb2.py,sha256=Rkhmbfm4jFqGU5v03iIWT8SBJ-9uLBYGIdDAkPMlh5A,6510
38
+ sapiopycommons/ai/protoapi/pipeline/script/script_pb2.pyi,sha256=7zgxMpp8KBzDTABrrX5roKhfoPda7v93lMLzIYxR4F8,6886
39
+ sapiopycommons/ai/protoapi/pipeline/script/script_pb2_grpc.py,sha256=o-uJ2sQ4HBrIJhPo0tU8qBvL8m3_xVqLePut4-iMa5A,6923
40
+ sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.py,sha256=HY0c7dvR6MpzDC43PTR6C3COYV2F_EnH1GuU2VXBOCs,2094
41
+ sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2.pyi,sha256=IyvYgtz-APrZjPvv5wISxSnPmpVK0moLa_nYaEyuFn0,1648
42
+ sapiopycommons/ai/protoapi/session/sapio_conn_info_pb2_grpc.py,sha256=Gk5SC9Dpx6JRJsKY5gONctgrLcIPTUwtH8oUw6jaN3w,930
43
+ sapiopycommons/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ sapiopycommons/callbacks/callback_util.py,sha256=qzSxMOnMT1fdPhKiL5tD4MYLgLKYVkYoSs5um9wZRZ0,154890
45
+ sapiopycommons/callbacks/field_builder.py,sha256=rnIP-RJafk3mZlAx1eJ8a0eSW9Ps_L6_WadCmusnENw,38772
46
+ sapiopycommons/chem/IndigoMolecules.py,sha256=7ucCaRMLu1zfH2uPIvXwRTSdpNcS03O1P9p_O-5B4xQ,5110
47
+ sapiopycommons/chem/Molecules.py,sha256=mVqPn32MPMjF0iZas-5MFkS-upIdoW5OB72KKZmJRJA,12523
48
+ sapiopycommons/chem/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ sapiopycommons/customreport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ sapiopycommons/customreport/auto_pagers.py,sha256=89p-tik0MhsOplYje6LbAW4WClldpAmb8YXFDoXhIlY,17144
51
+ sapiopycommons/customreport/column_builder.py,sha256=0RO53e9rKPZ07C--KcepN6_tpRw_FxF3O9vdG0ilKG8,3014
52
+ sapiopycommons/customreport/custom_report_builder.py,sha256=BlTxZ4t1sfZA2Ciur1EfYvkZxHxJ7ADwYNAe2zwiN0c,7176
53
+ sapiopycommons/customreport/term_builder.py,sha256=1_PGjxNUy5YWim8WJ_HJfiTq6i0D3gLPDxLySlFt30o,18573
54
+ sapiopycommons/datatype/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
+ sapiopycommons/datatype/attachment_util.py,sha256=N-nhsJ0oxa_Ft6Y6VWeNFYLzfuQqsjhHA6_-yIt2wVw,3596
56
+ sapiopycommons/datatype/data_fields.py,sha256=pczUlEcE0TeHEDU0Gkvu7voacSLPXCB7l9UbI1Tb6V0,5656
57
+ sapiopycommons/datatype/pseudo_data_types.py,sha256=lAJDnFuStrUP0mK5AuYlFvLerwjEB-ABd6Z4qlCrwJA,40637
58
+ sapiopycommons/eln/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
+ sapiopycommons/eln/experiment_cache.py,sha256=Zv4IcsAl95ftO2ul3DRc_Hyno0AfC3OvFV7RYb72ITo,9560
60
+ sapiopycommons/eln/experiment_handler.py,sha256=1RzrWOicF9VjcWlaziV46yBEk1BOP4BuTJ0JeYdn50I,99204
61
+ sapiopycommons/eln/experiment_report_util.py,sha256=GLpgwSEPuUqnY1v4oJ1ao60Va-YcgXh7E-cH9YnVeAg,37256
62
+ sapiopycommons/eln/experiment_step_factory.py,sha256=qw9UfLslVzB6dEIZPOZ85XHKpld81RhD4-csM6TgQNg,26099
63
+ sapiopycommons/eln/experiment_tags.py,sha256=7-fpOiSqrjbXmWIJhEhaxMgLsVCPAtKqH8xRzpDVKoE,356
64
+ sapiopycommons/eln/plate_designer.py,sha256=XFazSvhTbSy47t80-jc2tyx_-fQ_IUjKd18JQKEFcsY,13939
65
+ sapiopycommons/eln/step_creation.py,sha256=CFkGC-SxwAQpQlcs_obqLAVgmsNxKSGMqMtO_E6IVmw,10171
66
+ sapiopycommons/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
+ sapiopycommons/files/assay_plate_reader.py,sha256=3c2PQiiAbc2QJU9ZfNLzcTmvJrUwsbkIHO7R6R52xGU,3020
68
+ sapiopycommons/files/complex_data_loader.py,sha256=T39veNhvYl6j_uZjIIJ8Mk5Aa7otR5RB-g8XlAdkksA,1421
69
+ sapiopycommons/files/file_bridge.py,sha256=vKbqxPexi15epr_-_qLrEfYoxNxB031mXN92iVtOMqE,9511
70
+ sapiopycommons/files/file_bridge_handler.py,sha256=SEYDIQhSCmjI6qyLdDJE8JVKSd0WYvF7JvAq_Ahp9Do,25503
71
+ sapiopycommons/files/file_data_handler.py,sha256=f96MlkMuQhUCi4oLnzJK5AiuElCp5jLI8_sJkZVwpws,36779
72
+ sapiopycommons/files/file_text_converter.py,sha256=Gaj_divTiKXWd6flDOgrxNXpcn9fDWqxX6LUG0joePk,7516
73
+ sapiopycommons/files/file_util.py,sha256=WBA3FYG8R2HtfxjWSzQhZKW6_1s6JSxTo9lk3SeNDu8,37140
74
+ sapiopycommons/files/file_validator.py,sha256=ryg22-93csmRO_Pv0ZpWphNkB74xWZnHyJ23K56qLj0,28761
75
+ sapiopycommons/files/file_writer.py,sha256=hACVl0duCjP28gJ1NPljkjagNCLod0ygUlPbvUmRDNM,17605
76
+ sapiopycommons/files/temp_files.py,sha256=s2sGvn9uh2dTI8AVAQJWOf6RAZ0xZs7DSccCi4AGmlw,3175
77
+ sapiopycommons/flowcyto/flow_cyto.py,sha256=B6DFquLi-gcWfJWyP4vYfwTXXJKl6O9W5-k8FzkM0Oo,2610
78
+ sapiopycommons/flowcyto/flowcyto_data.py,sha256=mYKFuLbtpJ-EsQxLGtu4tNHVlygTxKixgJxJqD68F58,2596
79
+ sapiopycommons/general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
+ sapiopycommons/general/accession_service.py,sha256=ZvtvZg7d_siMJUedjrF14mcqo5ZqVA5IJxDa5enlB-8,12792
81
+ sapiopycommons/general/aliases.py,sha256=R7c1TgkyktKOrRYeMTpzMM61A7jYojJVM8ZcNu9Ktqo,14840
82
+ sapiopycommons/general/audit_log.py,sha256=sQAMcJx0cNkgZm7nTZSaGPxWvHG0_x6dBtU0jESavb4,9131
83
+ sapiopycommons/general/custom_report_util.py,sha256=9elLEUSgfM0gli8nRPz1uYkhaXN4Vnx3piSiNHv5IBs,19156
84
+ sapiopycommons/general/data_structure_util.py,sha256=fbQR_Fh4Scg67IpFPbQW9wVLw1oxlYxqp4LjBRTpjgU,4702
85
+ sapiopycommons/general/directive_util.py,sha256=7SeQrd2Ye5JHlXZtJZaVGgtaSLdq_Vm9EObuxf44Pz8,3905
86
+ sapiopycommons/general/exceptions.py,sha256=aPlzK1cvxeMU5UsokYlLrIBGltUfJZ7LH8zvLh9DxpI,3233
87
+ sapiopycommons/general/html_formatter.py,sha256=HE3OeGgwOw6x53zGSc4-UzP4-JoOmQIz3pX-DzNVg94,17138
88
+ sapiopycommons/general/macros.py,sha256=Tses4g247JADC93U2OROHdDadEuUPLhPXgeQljz-fK4,7127
89
+ sapiopycommons/general/popup_util.py,sha256=HKILegU1uCL_6abNlNL0Wn3xgX2JNa_kJeq7e5CZu6Q,31923
90
+ sapiopycommons/general/sapio_links.py,sha256=YkcVKNLrSGoM7tCCXBAsIbIxylctwdcEyhePrRMODe0,2859
91
+ sapiopycommons/general/storage_util.py,sha256=ovmK_jN7v09BoX07XxwShpBUC5WYQOM7dbKV_VeLXJU,8892
92
+ sapiopycommons/general/time_util.py,sha256=dB8_Ot61_ILcuKXeBt1jfa01UOoCcegnjqVdfr5tyQ8,19344
93
+ sapiopycommons/multimodal/multimodal.py,sha256=EP9WYzx1CvidmEBlvzO6tiF4HJwsPB1FgxpnbWzxnpA,6161
94
+ sapiopycommons/multimodal/multimodal_data.py,sha256=0BeVPr9HaC0hNTF1v1phTIKGruvNnwerHsD994qJKBg,15099
95
+ sapiopycommons/processtracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
+ sapiopycommons/processtracking/custom_workflow_handler.py,sha256=eYKdYlwo8xx-6AkB_iPUBNV9yDoNvW2h_Sm3i8JpmRU,25844
97
+ sapiopycommons/processtracking/endpoints.py,sha256=5AJLbhRKQsOeeOdQa888xcCJZD5aavxD-DHZ36Qob_M,12548
98
+ sapiopycommons/recordmodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
99
+ sapiopycommons/recordmodel/record_handler.py,sha256=TUkbMZEZEjO2fL77YvmA6nzRc5jCggzGApIL9AoNKOU,96916
100
+ sapiopycommons/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
+ sapiopycommons/rules/eln_rule_handler.py,sha256=Ec2hvxn6gmBvZjhX9-7WCFqafxFE9JSy2zCsvFsVyS4,11565
102
+ sapiopycommons/rules/on_save_rule_handler.py,sha256=HLdgUkxmaoHBK3jaycZlUHWam4kk36zmw7VDuRRiAx8,11332
103
+ sapiopycommons/samples/aliquot.py,sha256=mWOJUqaQh0t3HklNuGdmuV7D5zzXs6fpLwtDdM6_XTo,3018
104
+ sapiopycommons/sftpconnect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
+ sapiopycommons/sftpconnect/sftp_builder.py,sha256=lFK3FeXk-sFLefW0hqY8WGUQDeYiGaT6yDACzT_zFgQ,3015
106
+ sapiopycommons/webhook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
+ sapiopycommons/webhook/webhook_context.py,sha256=D793uLsb1691SalaPnBUk3rOSxn_hYLhdvkaIxjNXss,1909
108
+ sapiopycommons/webhook/webhook_handlers.py,sha256=7o_wXOruhT9auNh8OfhJAh4WhhiPKij67FMBSpGPICc,39939
109
+ sapiopycommons/webhook/webservice_handlers.py,sha256=cvW6Mk_110BzYqkbk63Kg7jWrltBCDALOlkJRu8h4VQ,14300
110
+ sapiopycommons-2026.1.22a847.dist-info/METADATA,sha256=Vx4vhKjIcObBi7v_mYqHErNc-Ku0zd8YDbw2WwF5qhE,3143
111
+ sapiopycommons-2026.1.22a847.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
112
+ sapiopycommons-2026.1.22a847.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
113
+ sapiopycommons-2026.1.22a847.dist-info/RECORD,,