sapiopycommons 2024.8.7a303__tar.gz → 2024.8.19a305__tar.gz

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 (59) hide show
  1. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/PKG-INFO +1 -1
  2. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/pyproject.toml +1 -1
  3. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/callbacks/callback_util.py +63 -3
  4. sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/column_builder.py +60 -0
  5. sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/custom_report_builder.py +125 -0
  6. sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport/term_builder.py +296 -0
  7. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/experiment_handler.py +153 -36
  8. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_bridge_handler.py +21 -0
  9. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_util.py +25 -1
  10. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_validator.py +20 -4
  11. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_writer.py +44 -15
  12. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/aliases.py +77 -6
  13. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/popup_util.py +17 -0
  14. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/time_util.py +40 -0
  15. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/recordmodel/record_handler.py +36 -1
  16. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/rules/eln_rule_handler.py +23 -0
  17. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/rules/on_save_rule_handler.py +23 -0
  18. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/webhook/webhook_handlers.py +32 -3
  19. sapiopycommons-2024.8.19a305/src/sapiopycommons/webhook/webservice_handlers.py +67 -0
  20. sapiopycommons-2024.8.19a305/tests/_do_not_add_init_py_here +0 -0
  21. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/.gitignore +0 -0
  22. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/LICENSE +0 -0
  23. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/README.md +0 -0
  24. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/__init__.py +0 -0
  25. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/callbacks/__init__.py +0 -0
  26. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/IndigoMolecules.py +0 -0
  27. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/Molecules.py +0 -0
  28. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/chem/__init__.py +0 -0
  29. {sapiopycommons-2024.8.7a303/src/sapiopycommons/datatype → sapiopycommons-2024.8.19a305/src/sapiopycommons/customreport}/__init__.py +0 -0
  30. {sapiopycommons-2024.8.7a303/src/sapiopycommons/eln → sapiopycommons-2024.8.19a305/src/sapiopycommons/datatype}/__init__.py +0 -0
  31. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/datatype/attachment_util.py +0 -0
  32. {sapiopycommons-2024.8.7a303/src/sapiopycommons/files → sapiopycommons-2024.8.19a305/src/sapiopycommons/eln}/__init__.py +0 -0
  33. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/experiment_report_util.py +0 -0
  34. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/eln/plate_designer.py +0 -0
  35. {sapiopycommons-2024.8.7a303/src/sapiopycommons/general → sapiopycommons-2024.8.19a305/src/sapiopycommons/files}/__init__.py +0 -0
  36. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/complex_data_loader.py +0 -0
  37. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_bridge.py +0 -0
  38. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/files/file_data_handler.py +0 -0
  39. {sapiopycommons-2024.8.7a303/src/sapiopycommons/processtracking → sapiopycommons-2024.8.19a305/src/sapiopycommons/general}/__init__.py +0 -0
  40. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/accession_service.py +0 -0
  41. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/audit_log.py +0 -0
  42. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/custom_report_util.py +0 -0
  43. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/exceptions.py +0 -0
  44. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/sapio_links.py +0 -0
  45. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/general/storage_util.py +0 -0
  46. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/multimodal/multimodal.py +0 -0
  47. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/multimodal/multimodal_data.py +0 -0
  48. {sapiopycommons-2024.8.7a303/src/sapiopycommons/recordmodel → sapiopycommons-2024.8.19a305/src/sapiopycommons/processtracking}/__init__.py +0 -0
  49. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  50. {sapiopycommons-2024.8.7a303/src/sapiopycommons/rules → sapiopycommons-2024.8.19a305/src/sapiopycommons/recordmodel}/__init__.py +0 -0
  51. {sapiopycommons-2024.8.7a303/src/sapiopycommons/webhook → sapiopycommons-2024.8.19a305/src/sapiopycommons/rules}/__init__.py +0 -0
  52. /sapiopycommons-2024.8.7a303/tests/_do_not_add_init_py_here → /sapiopycommons-2024.8.19a305/src/sapiopycommons/webhook/__init__.py +0 -0
  53. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/accession_test.py +0 -0
  54. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/bio_reg_test.py +0 -0
  55. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/chem_test.py +0 -0
  56. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/data_type_models.py +0 -0
  57. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/kappa.chains.fasta +0 -0
  58. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/mafft_test.py +0 -0
  59. {sapiopycommons-2024.8.7a303 → sapiopycommons-2024.8.19a305}/tests/test.gb +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.8.7a303
3
+ Version: 2024.8.19a305
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>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sapiopycommons"
7
- version='2024.08.07a303'
7
+ version='2024.08.19a305'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  from typing import Any
5
+ from weakref import WeakValueDictionary
5
6
 
6
7
  from sapiopylib.rest.ClientCallbackService import ClientCallback
7
8
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
@@ -24,6 +25,7 @@ from sapiopylib.rest.utils.FormBuilder import FormBuilder
24
25
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
25
26
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
26
27
 
28
+ from sapiopycommons.files.file_util import FileUtil
27
29
  from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
28
30
  from sapiopycommons.general.custom_report_util import CustomReportUtil
29
31
  from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
@@ -37,17 +39,36 @@ class CallbackUtil:
37
39
  width_pixels: int | None
38
40
  width_percent: float | None
39
41
 
42
+ __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
43
+ __initialized: bool
44
+
45
+ def __new__(cls, context: SapioWebhookContext | SapioUser):
46
+ """
47
+ :param context: The current webhook context or a user object to send requests from.
48
+ """
49
+ user = context if isinstance(context, SapioUser) else context.user
50
+ obj = cls.__instances.get(user)
51
+ if not obj:
52
+ obj = object.__new__(cls)
53
+ obj.__initialized = False
54
+ cls.__instances[user] = obj
55
+ return obj
56
+
40
57
  def __init__(self, context: SapioWebhookContext | SapioUser):
41
58
  """
42
59
  :param context: The current webhook context or a user object to send requests from.
43
60
  """
61
+ if self.__initialized:
62
+ return
63
+ self.__initialized = True
64
+
44
65
  self.user = context if isinstance(context, SapioUser) else context.user
45
66
  self.callback = DataMgmtServer.get_client_callback(self.user)
46
67
  self.dt_cache = DataTypeCacheManager(self.user)
47
68
  self.width_pixels = None
48
69
  self.width_percent = None
49
70
 
50
- def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
71
+ def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
51
72
  """
52
73
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
53
74
 
@@ -55,6 +76,8 @@ class CallbackUtil:
55
76
  :param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
56
77
  will appear as.
57
78
  """
79
+ if width_pixels is not None and width_percent is not None:
80
+ raise SapioException("Cannot set both width_pixels and width_percent at once.")
58
81
  self.width_pixels = width_pixels
59
82
  self.width_percent = width_percent
60
83
 
@@ -69,6 +92,33 @@ class CallbackUtil:
69
92
  """
70
93
  self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
71
94
 
95
+ def display_info(self, message: str) -> None:
96
+ """
97
+ Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
98
+ to the same dialog if it is still opened by the user.
99
+
100
+ :param message: The message to display to the user.
101
+ """
102
+ self.callback.display_info(message)
103
+
104
+ def display_warning(self, message: str) -> None:
105
+ """
106
+ Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
107
+ to the same dialog if it is still opened by the user.
108
+
109
+ :param message: The message to display to the user.
110
+ """
111
+ self.callback.display_warning(message)
112
+
113
+ def display_error(self, message: str) -> None:
114
+ """
115
+ Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
116
+ to the same dialog if it is still opened by the user.
117
+
118
+ :param message: The message to display to the user.
119
+ """
120
+ self.callback.display_error(message)
121
+
72
122
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
73
123
  user_can_cancel: bool = False) -> str:
74
124
  """
@@ -810,7 +860,7 @@ class CallbackUtil:
810
860
 
811
861
  # If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
812
862
  if isinstance(custom_search, CustomReportCriteria):
813
- custom_search: CustomReport = CustomReport(False, None, custom_search)
863
+ custom_search: CustomReport = CustomReport(False, [], custom_search)
814
864
  # If a string was provided, locate the report criteria for the predefined search in the system matching this
815
865
  # name.
816
866
  if isinstance(custom_search, str):
@@ -929,7 +979,7 @@ class CallbackUtil:
929
979
  if len(allowed_extensions) != 0:
930
980
  matches: bool = False
931
981
  for ext in allowed_extensions:
932
- if file_path.endswith("." + ext):
982
+ if file_path.endswith("." + ext.lstrip(".")):
933
983
  matches = True
934
984
  break
935
985
  if matches is False:
@@ -946,6 +996,16 @@ class CallbackUtil:
946
996
  data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
947
997
  self.callback.send_file(file_name, False, data)
948
998
 
999
+ def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1000
+ """
1001
+ Send a collection of files to the user in a zip file.
1002
+
1003
+ :param zip_name: The name of the zip file.
1004
+ :param files: A dictionary of the files to add to the zip file.
1005
+ """
1006
+ data = io.BytesIO(FileUtil.zip_files(files))
1007
+ self.callback.send_file(zip_name, False, data)
1008
+
949
1009
 
950
1010
  class FieldModifier:
951
1011
  """
@@ -0,0 +1,60 @@
1
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn
2
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
3
+
4
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil
5
+ from sapiopycommons.general.exceptions import SapioException
6
+
7
+ # The system fields that every record has and their field types. System fields aren't generated as record model fields
8
+ # for all platform version, hence the need to create a dict for them in the off chance that they're not present on
9
+ # the model wrapper.
10
+ SYSTEM_FIELDS: dict[str, FieldType] = {
11
+ "DataRecordName": FieldType.IDENTIFIER,
12
+ "RecordId": FieldType.LONG,
13
+ "DateCreated": FieldType.DATE,
14
+ "CreatedBy": FieldType.STRING,
15
+ "VeloxLastModifiedDate": FieldType.DATE,
16
+ "VeloxLastModifiedBy": FieldType.STRING
17
+ }
18
+
19
+
20
+ class ColumnBuilder:
21
+ """
22
+ A class for building report columns for custom reports.
23
+ """
24
+ @staticmethod
25
+ def build_column(data_type: DataTypeIdentifier, field: FieldIdentifier, field_type: FieldType | None = None) \
26
+ -> ReportColumn:
27
+ """
28
+ Build a ReportColumn from a variety of possible inputs.
29
+
30
+ :param data_type: An object that can be used to identify a data type.
31
+ :param field: An object that can be used to identify a data field.
32
+ :param field_type: The field type of the provided field. This is only required if the field type cannot be
33
+ determined from the given data type and field, which occurs when the given field is a string and the
34
+ given data type is not a wrapped record model or record model wrapper.
35
+ :return: A ReportColumn for the inputs.
36
+ """
37
+ # Get the data type and field names from the inputs.
38
+ data_type_name = AliasUtil.to_data_type_name(data_type)
39
+ field_name = AliasUtil.to_data_field_name(field)
40
+ if field_type is None:
41
+ field_type = ColumnBuilder.__field_type(data_type, field)
42
+ if field_type is None:
43
+ raise SapioException("The field_type parameter is required for the provided data_type and field inputs.")
44
+ return ReportColumn(data_type_name, field_name, field_type)
45
+
46
+ @staticmethod
47
+ def __field_type(data_type: DataTypeIdentifier, field: FieldIdentifier) -> FieldType | None:
48
+ """
49
+ Given a record model wrapper and a field name, return the field type for that field. Accounts for system fields.
50
+
51
+ :param data_type: The record model wrapper that the field is on.
52
+ :param field: The field name to return the type of.
53
+ :return: The field type of the given field name.
54
+ """
55
+ # Check if the field name is a system field. If it us, use the field type defined in this file.
56
+ field_name: str = AliasUtil.to_data_field_name(field)
57
+ if field_name in SYSTEM_FIELDS:
58
+ return SYSTEM_FIELDS.get(field_name)
59
+ # Otherwise, check if the field type can be found from the wrapper.
60
+ return AliasUtil.to_field_type(field_name, data_type)
@@ -0,0 +1,125 @@
1
+ from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria, AbstractReportTerm, \
2
+ ExplicitJoinDefinition, RelatedRecordCriteria, QueryRestriction, FieldCompareReportTerm
3
+ from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
4
+
5
+ from sapiopycommons.customreport.column_builder import ColumnBuilder
6
+ from sapiopycommons.general.aliases import DataTypeIdentifier, FieldIdentifier, AliasUtil, SapioRecord
7
+ from sapiopycommons.general.exceptions import SapioException
8
+
9
+
10
+ class CustomReportBuilder:
11
+ """
12
+ A class used for building custom reports. Look into using the TermBuilder and ColumnBuilder classes for building
13
+ parts of a custom report.
14
+ """
15
+ root_data_type: DataTypeIdentifier
16
+ data_type_name: str
17
+ root_term: AbstractReportTerm | None
18
+ record_criteria: RelatedRecordCriteria
19
+ column_list: list[ReportColumn]
20
+ join_list: list[ExplicitJoinDefinition]
21
+
22
+ def __init__(self, root_data_type: DataTypeIdentifier):
23
+ """
24
+ :param root_data_type: An object that can be used to identify a data type name. Used as the root data type name
25
+ of this search.
26
+ """
27
+ self.root_data_type = root_data_type
28
+ self.data_type_name = AliasUtil.to_data_type_name(root_data_type)
29
+ self.root_term = None
30
+ self.record_criteria = RelatedRecordCriteria(QueryRestriction.QUERY_ALL)
31
+ self.column_list = []
32
+ self.join_list = []
33
+
34
+ def has_root_term(self) -> bool:
35
+ """
36
+ :return: Whether this report builder has had its root term set.
37
+ """
38
+ return self.root_term is not None
39
+
40
+ def set_root_term(self, term: AbstractReportTerm) -> None:
41
+ """
42
+ Set the root term of the report. Use the TermBuilder class to construct the report terms.
43
+
44
+ :param term: The term to set as the root term.
45
+ """
46
+ self.root_term = term
47
+
48
+ def has_columns(self) -> bool:
49
+ """
50
+ :return: Whether this report builder has any report columns.
51
+ """
52
+ return bool(self.column_list)
53
+
54
+ def add_column(self, field: FieldIdentifier, field_type: FieldType = None,
55
+ *, data_type: DataTypeIdentifier | None = None) -> None:
56
+ """
57
+ Add a column to this report builder.
58
+
59
+ :param field: An object that can be used to identify a data field.
60
+ :param field_type: The field type of the provided field. This is only required if the field type cannot be
61
+ determined from the given data type and field, which occurs when the given field is a string and the
62
+ given data type is not a wrapped record model or record model wrapper.
63
+ :param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
64
+ provided when this builder was initialized. You'll only want to specify this value when adding a column
65
+ that is from a different data type than the root data type.
66
+ """
67
+ if data_type is None:
68
+ data_type = self.root_data_type
69
+ self.column_list.append(ColumnBuilder.build_column(data_type, field, field_type))
70
+
71
+ def add_columns(self, fields: list[FieldIdentifier], *, data_type: DataTypeIdentifier | None = None) -> None:
72
+ """
73
+ Add columns to this report builder.
74
+
75
+ :param fields: A list of objects that can be used to identify data fields.
76
+ :param data_type: An object that can be used to identify a data type. If not provided, uses the root data type
77
+ provided when this builder was initialized. You'll only want to specify this value when adding a column
78
+ that is from a different data type than the root data type.
79
+ """
80
+ for field in fields:
81
+ self.add_column(field, data_type=data_type)
82
+
83
+ def set_query_restriction(self, base_record: SapioRecord, search_related: QueryRestriction) -> None:
84
+ """
85
+ Set a restriction on the report for this report builder such that the returned results must be related in
86
+ some way to the provided base record. Without this, the report searches all records in the system that match the
87
+ root term.
88
+
89
+ :param base_record: The base record to run the search from.
90
+ :param search_related: Determine the relationship of the related records that can appear in the search, be those
91
+ children, parents, descendants, or ancestors.
92
+ """
93
+ if search_related == QueryRestriction.QUERY_ALL:
94
+ raise SapioException("The search_related must be something other than QUERY_ALL when setting a query restriction.")
95
+ self.record_criteria = RelatedRecordCriteria(search_related,
96
+ AliasUtil.to_record_id(base_record),
97
+ AliasUtil.to_data_type_name(base_record))
98
+
99
+ def add_join(self, comparison_term: FieldCompareReportTerm) -> None:
100
+ """
101
+ Add a join statement to this report builder.
102
+
103
+ :param comparison_term: The field comparison term to join with. The left side data type name of this term will
104
+ be the data type that is joined against.
105
+ """
106
+ self.join_list.append(ExplicitJoinDefinition(comparison_term.left_data_type_name, comparison_term))
107
+
108
+ def build_report_criteria(self, page_size: int = 0, page_number: int = -1, case_sensitive: bool = False,
109
+ owner_restriction_set: list[str] = None) -> CustomReportCriteria:
110
+ """
111
+ Generate a CustomReportCriteria using the column list, root term, and root data type from this report builder.
112
+ You can use the CustomReportManager or CustomReportUtil to run the constructed report.
113
+
114
+ :param page_size: The page size of the custom report.
115
+ :param page_number: The page number of the current report.
116
+ :param case_sensitive: When searching texts, should the search be case-sensitive?
117
+ :param owner_restriction_set: Specifies to only return records if the record is owned by this list of usernames.
118
+ :return: A CustomReportCriteria from this report builder.
119
+ """
120
+ if not self.has_root_term():
121
+ raise SapioException("Cannot build a report with no root term.")
122
+ if not self.has_columns():
123
+ raise SapioException("Cannot build a report with no columns.")
124
+ return CustomReportCriteria(self.column_list, self.root_term, self.record_criteria, self.data_type_name,
125
+ case_sensitive, page_size, page_number, owner_restriction_set, self.join_list)
@@ -0,0 +1,296 @@
1
+ from typing import Iterable
2
+
3
+ from sapiopylib.rest.pojo.CustomReport import RawTermOperation, CompositeTermOperation, RawReportTerm, \
4
+ CompositeReportTerm, AbstractReportTerm, FieldCompareReportTerm
5
+
6
+ from sapiopycommons.general.aliases import DataTypeIdentifier, AliasUtil, FieldIdentifier
7
+
8
+ # Raw term operations, for comparing field values.
9
+ EQ = RawTermOperation.EQUAL_TO_OPERATOR
10
+ NEQ = RawTermOperation.NOT_EQUAL_TO_OPERATOR
11
+ LT = RawTermOperation.LESS_THAN_OPERATOR
12
+ LTE = RawTermOperation.LESS_THAN_OR_EQUAL_OPERATOR
13
+ GT = RawTermOperation.GREATER_THAN_OPERATOR
14
+ GTE = RawTermOperation.GREATER_THAN_OR_EQUAL_OPERATOR
15
+
16
+ # Composite term operations, for comparing two terms.
17
+ AND = CompositeTermOperation.AND_OPERATOR
18
+ OR = CompositeTermOperation.OR_OPERATOR
19
+
20
+ # Forms that field term values can take.
21
+ TermValue = str | int | float | bool | Iterable
22
+
23
+
24
+ class TermBuilder:
25
+ """
26
+ A class that allows for the easier constructions of custom report terms.
27
+ """
28
+ @staticmethod
29
+ def all_records_term(data_type: DataTypeIdentifier) -> RawReportTerm:
30
+ """
31
+ Create a raw report term that captures all records of the given data type.
32
+
33
+ :param data_type: The data type of this term.
34
+ :return: A raw report term for "data_type.RecordId >= 0".
35
+ """
36
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), "RecordId", GTE, TermBuilder.to_term_val(0))
37
+
38
+ @staticmethod
39
+ def is_term(data_type: DataTypeIdentifier, field: FieldIdentifier, value: TermValue,
40
+ *, trim: bool = False) -> RawReportTerm:
41
+ """
42
+ Create a raw report term for comparing a field value with an equals operation.
43
+
44
+ :param data_type: The data type of this term.
45
+ :param field: The data field of this term.
46
+ :param value: The value to compare for this term.
47
+ :param trim: Whether the string of the given value should be trimmed of trailing and leading whitespace.
48
+ :return: A raw report term for "data_type.field = value".
49
+ """
50
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), AliasUtil.to_data_field_name(field), EQ,
51
+ TermBuilder.to_term_val(value), trim)
52
+
53
+ @staticmethod
54
+ def not_term(data_type: DataTypeIdentifier, field: FieldIdentifier, value: TermValue,
55
+ *, trim: bool = False) -> RawReportTerm:
56
+ """
57
+ Create a raw report term for comparing a field value with a not equals operation.
58
+
59
+ :param data_type: The data type of this term.
60
+ :param field: The data field of this term.
61
+ :param value: The value to compare for this term.
62
+ :param trim: Whether the string of the given value should be trimmed of trailing and leading whitespace.
63
+ :return: A raw report term for "data_type.field != value".
64
+ """
65
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), AliasUtil.to_data_field_name(field), NEQ,
66
+ TermBuilder.to_term_val(value), trim)
67
+
68
+ @staticmethod
69
+ def lt_term(data_type: DataTypeIdentifier, field: FieldIdentifier, value: TermValue,
70
+ *, trim: bool = False) -> RawReportTerm:
71
+ """
72
+ Create a raw report term for comparing a field value with a less than operation.
73
+
74
+ :param data_type: The data type of this term.
75
+ :param field: The data field of this term.
76
+ :param value: The value to compare for this term.
77
+ :param trim: Whether the string of the given value should be trimmed of trailing and leading whitespace.
78
+ :return: A raw report term for "data_type.field < value".
79
+ """
80
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), AliasUtil.to_data_field_name(field), LT,
81
+ TermBuilder.to_term_val(value), trim)
82
+
83
+ @staticmethod
84
+ def lte_term(data_type: DataTypeIdentifier, field: FieldIdentifier, value: TermValue,
85
+ *, trim: bool = False) -> RawReportTerm:
86
+ """
87
+ Create a raw report term for comparing a field value with a less than or equal to operation.
88
+
89
+ :param data_type: The data type of this term.
90
+ :param field: The data field of this term.
91
+ :param value: The value to compare for this term.
92
+ :param trim: Whether the string of the given value should be trimmed of trailing and leading whitespace.
93
+ :return: A raw report term for "data_type.field <= value".
94
+ """
95
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), AliasUtil.to_data_field_name(field), LTE,
96
+ TermBuilder.to_term_val(value), trim)
97
+
98
+ @staticmethod
99
+ def gt_term(data_type: DataTypeIdentifier, field: FieldIdentifier, value: TermValue,
100
+ *, trim: bool = False) -> RawReportTerm:
101
+ """
102
+ Create a raw report term for comparing a field value with a greater than operation.
103
+
104
+ :param data_type: The data type of this term.
105
+ :param field: The data field of this term.
106
+ :param value: The value to compare for this term.
107
+ :param trim: Whether the string of the given value should be trimmed of trailing and leading whitespace.
108
+ :return: A raw report term for "data_type.field > value".
109
+ """
110
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), AliasUtil.to_data_field_name(field), GT,
111
+ TermBuilder.to_term_val(value), trim)
112
+
113
+ @staticmethod
114
+ def gte_term(data_type: DataTypeIdentifier, field: FieldIdentifier, value: TermValue,
115
+ *, trim: bool = False) -> RawReportTerm:
116
+ """
117
+ Create a raw report term for comparing a field value with a greater than or equal to operation.
118
+
119
+ :param data_type: The data type of this term.
120
+ :param field: The data field of this term.
121
+ :param value: The value to compare for this term.
122
+ :param trim: Whether the string of the given value should be trimmed of trailing and leading whitespace.
123
+ :return: A raw report term for "data_type.field >= value".
124
+ """
125
+ return RawReportTerm(AliasUtil.to_data_type_name(data_type), AliasUtil.to_data_field_name(field), GTE,
126
+ TermBuilder.to_term_val(value), trim)
127
+
128
+ @staticmethod
129
+ def compare_is_term(data_type_A: DataTypeIdentifier, field_A: FieldIdentifier,
130
+ data_type_B: DataTypeIdentifier, field_B: FieldIdentifier,
131
+ *, trim: bool = False) -> FieldCompareReportTerm:
132
+ """
133
+ Create a field comparison report term for comparing field values between data types with an equals operation.
134
+
135
+ :param data_type_A: The data type for the left side of this term.
136
+ :param field_A: The data field for the left side of this term.
137
+ :param data_type_B: The data type for the right side of this term.
138
+ :param field_B: The data field for the right side of this term.
139
+ :param trim: Whether the field values should be trimmed of trailing and leading whitespace for comparing.
140
+ :return: A field comparison report term for "data_type_A.field_A = data_type_B.field_B".
141
+ """
142
+ return FieldCompareReportTerm(AliasUtil.to_data_type_name(data_type_A), AliasUtil.to_data_field_name(field_A), EQ,
143
+ AliasUtil.to_data_type_name(data_type_B), AliasUtil.to_data_field_name(field_B), trim)
144
+
145
+ @staticmethod
146
+ def compare_not_term(data_type_A: DataTypeIdentifier, field_A: FieldIdentifier,
147
+ data_type_B: DataTypeIdentifier, field_B: FieldIdentifier,
148
+ *, trim: bool = False) -> FieldCompareReportTerm:
149
+ """
150
+ Create a field comparison report term for comparing field values between data types with a not equals operation.
151
+
152
+ :param data_type_A: The data type for the left side of this term.
153
+ :param field_A: The data field for the left side of this term.
154
+ :param data_type_B: The data type for the right side of this term.
155
+ :param field_B: The data field for the right side of this term.
156
+ :param trim: Whether the field values should be trimmed of trailing and leading whitespace for comparing.
157
+ :return: A field comparison report term for "data_type_A.field_A != data_type_B.field_B".
158
+ """
159
+ return FieldCompareReportTerm(AliasUtil.to_data_type_name(data_type_A), AliasUtil.to_data_field_name(field_A), NEQ,
160
+ AliasUtil.to_data_type_name(data_type_B), AliasUtil.to_data_field_name(field_B), trim)
161
+
162
+ @staticmethod
163
+ def compare_lt_term(data_type_A: DataTypeIdentifier, field_A: FieldIdentifier,
164
+ data_type_B: DataTypeIdentifier, field_B: FieldIdentifier,
165
+ *, trim: bool = False) -> FieldCompareReportTerm:
166
+ """
167
+ Create a field comparison report term for comparing field values between data types with a less than operation.
168
+
169
+ :param data_type_A: The data type for the left side of this term.
170
+ :param field_A: The data field for the left side of this term.
171
+ :param data_type_B: The data type for the right side of this term.
172
+ :param field_B: The data field for the right side of this term.
173
+ :param trim: Whether the field values should be trimmed of trailing and leading whitespace for comparing.
174
+ :return: A field comparison report term for "data_type_A.field_A < data_type_B.field_B".
175
+ """
176
+ return FieldCompareReportTerm(AliasUtil.to_data_type_name(data_type_A), AliasUtil.to_data_field_name(field_A), LT,
177
+ AliasUtil.to_data_type_name(data_type_B), AliasUtil.to_data_field_name(field_B), trim)
178
+
179
+ @staticmethod
180
+ def compare_lte_term(data_type_A: DataTypeIdentifier, field_A: FieldIdentifier,
181
+ data_type_B: DataTypeIdentifier, field_B: FieldIdentifier,
182
+ *, trim: bool = False) -> FieldCompareReportTerm:
183
+ """
184
+ Create a field comparison report term for comparing field values between data types with a less than or equal
185
+ to operation.
186
+
187
+ :param data_type_A: The data type for the left side of this term.
188
+ :param field_A: The data field for the left side of this term.
189
+ :param data_type_B: The data type for the right side of this term.
190
+ :param field_B: The data field for the right side of this term.
191
+ :param trim: Whether the field values should be trimmed of trailing and leading whitespace for comparing.
192
+ :return: A field comparison report term for "data_type_A.field_A <= data_type_B.field_B".
193
+ """
194
+ return FieldCompareReportTerm(AliasUtil.to_data_type_name(data_type_A), AliasUtil.to_data_field_name(field_A), LTE,
195
+ AliasUtil.to_data_type_name(data_type_B), AliasUtil.to_data_field_name(field_B), trim)
196
+
197
+ @staticmethod
198
+ def compare_gt_term(data_type_A: DataTypeIdentifier, field_A: FieldIdentifier,
199
+ data_type_B: DataTypeIdentifier, field_B: FieldIdentifier,
200
+ *, trim: bool = False) -> FieldCompareReportTerm:
201
+ """
202
+ Create a field comparison report term for comparing field values between data types with a greater than
203
+ operation.
204
+
205
+ :param data_type_A: The data type for the left side of this term.
206
+ :param field_A: The data field for the left side of this term.
207
+ :param data_type_B: The data type for the right side of this term.
208
+ :param field_B: The data field for the right side of this term.
209
+ :param trim: Whether the field values should be trimmed of trailing and leading whitespace for comparing.
210
+ :return: A field comparison report term for "data_type_A.field_A > data_type_B.field_B".
211
+ """
212
+ return FieldCompareReportTerm(AliasUtil.to_data_type_name(data_type_A), AliasUtil.to_data_field_name(field_A), GT,
213
+ AliasUtil.to_data_type_name(data_type_B), AliasUtil.to_data_field_name(field_B), trim)
214
+
215
+ @staticmethod
216
+ def compare_gte_term(data_type_A: DataTypeIdentifier, field_A: FieldIdentifier,
217
+ data_type_B: DataTypeIdentifier, field_B: FieldIdentifier,
218
+ *, trim: bool = False) -> FieldCompareReportTerm:
219
+ """
220
+ Create a field comparison report term for comparing field values between data types with a greater than or
221
+ equal to operation.
222
+
223
+ :param data_type_A: The data type for the left side of this term.
224
+ :param field_A: The data field for the left side of this term.
225
+ :param data_type_B: The data type for the right side of this term.
226
+ :param field_B: The data field for the right side of this term.
227
+ :param trim: Whether the field values should be trimmed of trailing and leading whitespace for comparing.
228
+ :return: A field comparison report term for "data_type_A.field_A >= data_type_B.field_B".
229
+ """
230
+ return FieldCompareReportTerm(AliasUtil.to_data_type_name(data_type_A), AliasUtil.to_data_field_name(field_A), GTE,
231
+ AliasUtil.to_data_type_name(data_type_B), AliasUtil.to_data_field_name(field_B), trim)
232
+
233
+ @staticmethod
234
+ def or_terms(a: AbstractReportTerm, b: AbstractReportTerm, *, is_negated: bool = False) -> CompositeReportTerm:
235
+ """
236
+ Combine two report terms with an OR operation.
237
+
238
+ :param a: The first term in the operation.
239
+ :param b: The second term in the operation.
240
+ :param is_negated: Whether the returned term should be negated (i.e. turn this into a nor operation).
241
+ :return: A composite report term for "A or B".
242
+ """
243
+ return CompositeReportTerm(a, OR, b, is_negated)
244
+
245
+ @staticmethod
246
+ def and_terms(a: AbstractReportTerm, b: AbstractReportTerm, *, is_negated: bool = False) -> CompositeReportTerm:
247
+ """
248
+ Combine two report terms with an AND operation.
249
+
250
+ :param a: The first term in the operation.
251
+ :param b: The second term in the operation.
252
+ :param is_negated: Whether the returned term should be negated (i.e. turn this into a nand operation).
253
+ :return: A composite report term for "A and B".
254
+ """
255
+ return CompositeReportTerm(a, AND, b, is_negated)
256
+
257
+ @staticmethod
258
+ def xor_terms(a: AbstractReportTerm, b: AbstractReportTerm, *, is_negated: bool = False) -> CompositeReportTerm:
259
+ """
260
+ Combine two report terms with a XOR operation. Note that a XOR operation doesn't actually exist for custom
261
+ reports. This instead constructs a term that is "(A or B) and !(A and B)", which is equivalent to a XOR
262
+ operation.
263
+
264
+ :param a: The first term in the operation.
265
+ :param b: The second term in the operation.
266
+ :param is_negated: Whether the returned term should be negated (i.e. turn this into an xnor operation).
267
+ :return: A composite report term for "A xor B".
268
+ """
269
+ return TermBuilder.and_terms(TermBuilder.or_terms(a, b),
270
+ TermBuilder.and_terms(a, b, is_negated=True),
271
+ is_negated=is_negated)
272
+
273
+ @staticmethod
274
+ def to_term_val(value: TermValue) -> str:
275
+ """
276
+ Convert the given value to be used in a custom report term to a string. Term values may be strings, integers,
277
+ floats, booleans, or lists of values.
278
+
279
+ :param value: A value to be used in a custom report term.
280
+ :return: The provided value formatted as a string that can be used
281
+ """
282
+ # If the given value is already a string, then nothing needs to be done with it.
283
+ if not isinstance(value, str):
284
+ # If the given value is an iterable object, then the return value is the contents of that iterable
285
+ # in a comma separated list surrounded by curly braces.
286
+ if isinstance(value, Iterable):
287
+ # When converting a list of values to a string, values in the list which are already strings should be
288
+ # put in quotation marks so that strings that contain commas do not get split up. All other value
289
+ # types can be simply converted to a string, though.
290
+ def convert_list_value(val: TermValue) -> str:
291
+ return f"'{val}'" if isinstance(val, str) else str(val)
292
+ value = "{" + ",".join([convert_list_value(x) for x in value]) + "}"
293
+ else:
294
+ # Otherwise, the value is simply cast to a string.
295
+ value = str(value)
296
+ return value