sapiopycommons 2024.8.28a315__py3-none-any.whl → 2024.8.29a317__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.
- sapiopycommons/callbacks/callback_util.py +37 -133
- sapiopycommons/datatype/attachment_util.py +10 -11
- sapiopycommons/eln/experiment_handler.py +48 -209
- sapiopycommons/eln/experiment_report_util.py +129 -33
- sapiopycommons/files/complex_data_loader.py +4 -5
- sapiopycommons/files/file_bridge.py +14 -15
- sapiopycommons/files/file_bridge_handler.py +5 -27
- sapiopycommons/files/file_data_handler.py +5 -2
- sapiopycommons/files/file_util.py +5 -38
- sapiopycommons/files/file_validator.py +11 -26
- sapiopycommons/files/file_writer.py +15 -44
- sapiopycommons/general/aliases.py +3 -147
- sapiopycommons/general/custom_report_util.py +32 -34
- sapiopycommons/general/popup_util.py +0 -17
- sapiopycommons/general/time_util.py +0 -40
- sapiopycommons/multimodal/multimodal_data.py +1 -0
- sapiopycommons/processtracking/endpoints.py +22 -22
- sapiopycommons/recordmodel/record_handler.py +77 -228
- sapiopycommons/rules/eln_rule_handler.py +25 -34
- sapiopycommons/rules/on_save_rule_handler.py +31 -34
- sapiopycommons/webhook/webhook_handlers.py +26 -90
- {sapiopycommons-2024.8.28a315.dist-info → sapiopycommons-2024.8.29a317.dist-info}/METADATA +1 -1
- sapiopycommons-2024.8.29a317.dist-info/RECORD +43 -0
- sapiopycommons/customreport/__init__.py +0 -0
- sapiopycommons/customreport/column_builder.py +0 -60
- sapiopycommons/customreport/custom_report_builder.py +0 -125
- sapiopycommons/customreport/term_builder.py +0 -299
- sapiopycommons/general/audit_log.py +0 -196
- sapiopycommons/general/sapio_links.py +0 -50
- sapiopycommons/webhook/webservice_handlers.py +0 -67
- sapiopycommons-2024.8.28a315.dist-info/RECORD +0 -50
- {sapiopycommons-2024.8.28a315.dist-info → sapiopycommons-2024.8.29a317.dist-info}/WHEEL +0 -0
- {sapiopycommons-2024.8.28a315.dist-info → sapiopycommons-2024.8.29a317.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,125 +0,0 @@
|
|
|
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)
|
|
@@ -1,299 +0,0 @@
|
|
|
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 | None
|
|
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 None, then use an empty string for the search instead.
|
|
285
|
-
if value is None:
|
|
286
|
-
value = ""
|
|
287
|
-
# If the given value is an iterable object, then the return value is the contents of that iterable
|
|
288
|
-
# in a comma separated list surrounded by curly braces.
|
|
289
|
-
elif isinstance(value, Iterable):
|
|
290
|
-
# When converting a list of values to a string, values in the list which are already strings should be
|
|
291
|
-
# put in quotation marks so that strings that contain commas do not get split up. All other value
|
|
292
|
-
# types can be simply converted to a string, though.
|
|
293
|
-
def convert_list_value(val: TermValue) -> str:
|
|
294
|
-
return f"'{val}'" if isinstance(val, str) else str(val)
|
|
295
|
-
value = "{" + ",".join([convert_list_value(x) for x in value]) + "}"
|
|
296
|
-
else:
|
|
297
|
-
# Otherwise, the value is simply cast to a string.
|
|
298
|
-
value = str(value)
|
|
299
|
-
return value
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
from sapiopylib.rest.User import SapioUser
|
|
4
|
-
from sapiopylib.rest.pojo.CustomReport import ReportColumn, CustomReportCriteria
|
|
5
|
-
from sapiopylib.rest.pojo.datatype.FieldDefinition import FieldType
|
|
6
|
-
|
|
7
|
-
from sapiopycommons.customreport.term_builder import TermBuilder
|
|
8
|
-
from sapiopycommons.general.aliases import RecordIdentifier, AliasUtil, UserIdentifier, FieldIdentifier, FieldValue
|
|
9
|
-
from sapiopycommons.general.custom_report_util import CustomReportUtil
|
|
10
|
-
|
|
11
|
-
EVENTTYPE_COLUMN = "EVENTTYPE"
|
|
12
|
-
TIMESTAMP_COLUMN = "TIMESTAMP"
|
|
13
|
-
DATATYPENAME_COLUMN = "DATATYPENAME"
|
|
14
|
-
RECORDID_COLUMN = "RECORDID"
|
|
15
|
-
DESCRIPTION_COLUMN = "DESCRIPTION"
|
|
16
|
-
USERNAME_COLUMN = "USERNAME"
|
|
17
|
-
USERCOMMENT_COLUMN = "USERCOMMENT"
|
|
18
|
-
RECORDNAME_COLUMN = "RECORDNAME"
|
|
19
|
-
DATAFIELDNAME_COLUMN = "DATAFIELDNAME"
|
|
20
|
-
ORIGINALVALUE_COLUMN = "ORIGINALVALUE"
|
|
21
|
-
NEWVALUE_COLUMN = "NEWVALUE"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class EventType(Enum):
|
|
25
|
-
"""An enum to represent the possible event type values with the event type column in the audit log table."""
|
|
26
|
-
ADD = 0
|
|
27
|
-
DELETE = 1
|
|
28
|
-
MODIFY = 2
|
|
29
|
-
INFO = 3
|
|
30
|
-
ERROR = 4
|
|
31
|
-
WARNING = 5
|
|
32
|
-
IMPORT = 6
|
|
33
|
-
GENERATE = 7
|
|
34
|
-
EXPORT = 8
|
|
35
|
-
ADDREF = 9
|
|
36
|
-
REMOVEREF = 10
|
|
37
|
-
ESIGNATURE = 11
|
|
38
|
-
ROLEASSIGNMENT = 12
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class AuditLogEntry:
|
|
42
|
-
|
|
43
|
-
__event_type: EventType
|
|
44
|
-
__date: int
|
|
45
|
-
__data_type_name: str
|
|
46
|
-
__record_id: int
|
|
47
|
-
__description: str
|
|
48
|
-
__users_login_name: str
|
|
49
|
-
__comment: str
|
|
50
|
-
__data_record_name: str
|
|
51
|
-
__data_field_name: str
|
|
52
|
-
__original_value: str
|
|
53
|
-
__new_value: str
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def event_type(self) -> EventType:
|
|
57
|
-
return self.__event_type
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def date(self) -> int:
|
|
61
|
-
return self.__date
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def data_type_name(self) -> str:
|
|
65
|
-
return self.__data_type_name
|
|
66
|
-
|
|
67
|
-
@property
|
|
68
|
-
def record_id(self) -> int:
|
|
69
|
-
return self.__record_id
|
|
70
|
-
|
|
71
|
-
@property
|
|
72
|
-
def description(self) -> str:
|
|
73
|
-
return self.__description
|
|
74
|
-
|
|
75
|
-
@property
|
|
76
|
-
def users_login_name(self) -> str:
|
|
77
|
-
return self.__users_login_name
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def comment(self) -> str:
|
|
81
|
-
return self.__comment
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def data_record_name(self) -> str:
|
|
85
|
-
return self.__data_record_name
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def data_field_name(self) -> str:
|
|
89
|
-
return self.__data_field_name
|
|
90
|
-
|
|
91
|
-
@property
|
|
92
|
-
def original_value(self) -> str:
|
|
93
|
-
return self.__original_value
|
|
94
|
-
|
|
95
|
-
@property
|
|
96
|
-
def new_value(self) -> str:
|
|
97
|
-
return self.__new_value
|
|
98
|
-
|
|
99
|
-
def __init__(self, report_row: dict[str, FieldValue]):
|
|
100
|
-
self.__event_type = EventType((report_row[EVENTTYPE_COLUMN]))
|
|
101
|
-
self.__date = report_row[TIMESTAMP_COLUMN]
|
|
102
|
-
self.__data_type_name = report_row[DATATYPENAME_COLUMN]
|
|
103
|
-
self.__record_id = report_row[RECORDID_COLUMN]
|
|
104
|
-
self.__description = report_row[DESCRIPTION_COLUMN]
|
|
105
|
-
self.__users_login_name = report_row[USERNAME_COLUMN]
|
|
106
|
-
self.__comment = report_row[USERCOMMENT_COLUMN]
|
|
107
|
-
self.__data_record_name = report_row[RECORDNAME_COLUMN]
|
|
108
|
-
self.__data_field_name = report_row[DATAFIELDNAME_COLUMN]
|
|
109
|
-
self.__original_value = report_row[ORIGINALVALUE_COLUMN]
|
|
110
|
-
self.__new_value = report_row[NEWVALUE_COLUMN]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class AuditLog:
|
|
114
|
-
AUDIT_LOG_PSEUDO_DATATYPE: str = "AUDITLOG"
|
|
115
|
-
EVENT_TYPE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, EVENTTYPE_COLUMN, FieldType.ENUM)
|
|
116
|
-
DATE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, TIMESTAMP_COLUMN, FieldType.DATE)
|
|
117
|
-
DATA_TYPE_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATATYPENAME_COLUMN, FieldType.STRING)
|
|
118
|
-
RECORD_ID: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, FieldType.LONG)
|
|
119
|
-
DESCRIPTION: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DESCRIPTION_COLUMN, FieldType.STRING)
|
|
120
|
-
USERS_LOGIN_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERNAME_COLUMN, FieldType.STRING)
|
|
121
|
-
COMMENT: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, USERCOMMENT_COLUMN, FieldType.STRING)
|
|
122
|
-
DATA_RECORD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, RECORDNAME_COLUMN, FieldType.STRING)
|
|
123
|
-
DATA_FIELD_NAME: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, FieldType.STRING)
|
|
124
|
-
ORIGINAL_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, ORIGINALVALUE_COLUMN, FieldType.STRING)
|
|
125
|
-
NEW_VALUE: ReportColumn = ReportColumn(AUDIT_LOG_PSEUDO_DATATYPE, NEWVALUE_COLUMN, FieldType.STRING)
|
|
126
|
-
|
|
127
|
-
AUDIT_LOG_COLUMNS = [EVENT_TYPE, DATE, DATA_TYPE_NAME, RECORD_ID, DESCRIPTION, USERS_LOGIN_NAME, COMMENT,
|
|
128
|
-
DATA_RECORD_NAME, DATA_FIELD_NAME, ORIGINAL_VALUE, NEW_VALUE]
|
|
129
|
-
user: SapioUser
|
|
130
|
-
|
|
131
|
-
def __init__(self, context: UserIdentifier):
|
|
132
|
-
self.user = AliasUtil.to_sapio_user(context)
|
|
133
|
-
|
|
134
|
-
@staticmethod
|
|
135
|
-
def create_data_record_audit_log_report(records: list[RecordIdentifier],
|
|
136
|
-
fields: list[FieldIdentifier] | None = None) -> CustomReportCriteria:
|
|
137
|
-
"""
|
|
138
|
-
This method creates a CustomReportCriteria object for running an audit log query based on data records.
|
|
139
|
-
|
|
140
|
-
Creates a CustomReportCriteria object with a query term based on the record ids/records passed into the method.
|
|
141
|
-
Optionally, the fields parameter can be populated to limit the search to particular fields. If the fields
|
|
142
|
-
parameter is not populated, the search will include results for all field changes.
|
|
143
|
-
|
|
144
|
-
:param records: The DataRecords, RecordModels, or record ids to base the search on.
|
|
145
|
-
:param fields: The data field names to include changes for.
|
|
146
|
-
:return: The constructed CustomReportCriteria object, which can be used to run a report on the audit log.
|
|
147
|
-
"""
|
|
148
|
-
# Build the raw report term querying for any entry with a matching record ID value to the record ID's
|
|
149
|
-
# passed in.
|
|
150
|
-
record_ids = AliasUtil.to_record_ids(records)
|
|
151
|
-
root_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, RECORDID_COLUMN, record_ids)
|
|
152
|
-
|
|
153
|
-
# If the user passed in any specific fields, then we should limit the query to those fields.
|
|
154
|
-
if fields:
|
|
155
|
-
fields: list[str] = AliasUtil.to_data_field_names(fields)
|
|
156
|
-
field_term = TermBuilder.is_term(AuditLog.AUDIT_LOG_PSEUDO_DATATYPE, DATAFIELDNAME_COLUMN, fields)
|
|
157
|
-
root_term = TermBuilder.and_terms(root_term, field_term)
|
|
158
|
-
|
|
159
|
-
return CustomReportCriteria(AuditLog.AUDIT_LOG_COLUMNS, root_term)
|
|
160
|
-
|
|
161
|
-
def run_data_record_audit_log_report(self, records: list[RecordIdentifier],
|
|
162
|
-
fields: list[FieldIdentifier] | None = None) \
|
|
163
|
-
-> dict[RecordIdentifier, list[AuditLogEntry]]:
|
|
164
|
-
"""
|
|
165
|
-
This method runs a custom report for changes made to the given data records using the audit log.
|
|
166
|
-
See "create_data_record_audit_log_report" for more details about the data record audit log report.
|
|
167
|
-
|
|
168
|
-
:param records: The DataRecords, RecordModels, or record ids to base the search on.
|
|
169
|
-
:param fields: The data field names to include changes for.
|
|
170
|
-
:return: A dictionary where the keys are the record identifiers passed in, and the values are a list of
|
|
171
|
-
AuditLogEntry objects which match the record id value of those records.
|
|
172
|
-
"""
|
|
173
|
-
fields: list[str] = AliasUtil.to_data_field_names(fields)
|
|
174
|
-
# First, we must build our report criteria for running the Custom Report.
|
|
175
|
-
criteria = AuditLog.create_data_record_audit_log_report(records, fields)
|
|
176
|
-
|
|
177
|
-
# Then we must run the custom report using that criteria.
|
|
178
|
-
raw_report_data: list[dict[str, FieldValue]] = CustomReportUtil.run_custom_report(self.user, criteria)
|
|
179
|
-
|
|
180
|
-
# This section will prepare a map matching the original RecordIdentifier by record id.
|
|
181
|
-
# This is because the audit log entries will have record ids, but we want the keys in our result map
|
|
182
|
-
# to match the record identifiers that the user passed in, for convenience.
|
|
183
|
-
record_identifier_mapping: dict[int, RecordIdentifier] = dict()
|
|
184
|
-
for record in records:
|
|
185
|
-
record_id = AliasUtil.to_record_id(record)
|
|
186
|
-
record_identifier_mapping[record_id] = record
|
|
187
|
-
|
|
188
|
-
# Finally, we compile our audit data into a map where the keys are the record identifiers passed in,
|
|
189
|
-
# and the value is a list of applicable audit log entries.
|
|
190
|
-
final_audit_data: dict[RecordIdentifier, list[AuditLogEntry]] = dict()
|
|
191
|
-
for audit_entry_data in raw_report_data:
|
|
192
|
-
audit_entry: AuditLogEntry = AuditLogEntry(audit_entry_data)
|
|
193
|
-
identifier: RecordIdentifier = record_identifier_mapping.get(audit_entry.record_id)
|
|
194
|
-
final_audit_data.setdefault(identifier, []).append(audit_entry)
|
|
195
|
-
|
|
196
|
-
return final_audit_data
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
from sapiopylib.rest.User import SapioUser
|
|
2
|
-
from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
|
|
3
|
-
|
|
4
|
-
from sapiopycommons.general.aliases import RecordIdentifier, ExperimentIdentifier, AliasUtil, DataTypeIdentifier
|
|
5
|
-
from sapiopycommons.general.exceptions import SapioException
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SapioNavigationLinker:
|
|
9
|
-
"""
|
|
10
|
-
Given a URL to a system's webservice API (example: https://company.exemplareln.com/webservice/api), construct
|
|
11
|
-
URLs for navigation links to various locations in the system.
|
|
12
|
-
"""
|
|
13
|
-
base_url: str
|
|
14
|
-
|
|
15
|
-
def __init__(self, url: str | SapioUser | SapioWebhookContext):
|
|
16
|
-
"""
|
|
17
|
-
:param url: A user or context object that is being used to send requests to a Sapio system, or a URL to a
|
|
18
|
-
system's webservice API.
|
|
19
|
-
"""
|
|
20
|
-
if isinstance(url, SapioWebhookContext):
|
|
21
|
-
url = url.user.url
|
|
22
|
-
elif isinstance(url, SapioUser):
|
|
23
|
-
url = url.url
|
|
24
|
-
self.base_url = url.rstrip("/").replace('webservice/api', 'veloxClient')
|
|
25
|
-
|
|
26
|
-
def data_record(self, record_identifier: RecordIdentifier, data_type_name: DataTypeIdentifier | None = None) -> str:
|
|
27
|
-
"""
|
|
28
|
-
:param record_identifier: An object that can be used to identify a record in the system, be that a record ID,
|
|
29
|
-
a data record, or a record model.
|
|
30
|
-
:param data_type_name: If the provided record identifier is a record ID, then the data type name of the record
|
|
31
|
-
must be provided in this parameter. Otherwise, this parameter is ignored.
|
|
32
|
-
:return: A URL for navigating to the input record.
|
|
33
|
-
"""
|
|
34
|
-
record_id: int = AliasUtil.to_record_id(record_identifier)
|
|
35
|
-
if data_type_name:
|
|
36
|
-
data_type_name = AliasUtil.to_data_type_name(data_type_name)
|
|
37
|
-
if not isinstance(record_identifier, int):
|
|
38
|
-
data_type_name = AliasUtil.to_data_type_name(record_identifier)
|
|
39
|
-
if not data_type_name:
|
|
40
|
-
raise SapioException("Unable to create a data record link without a data type name. "
|
|
41
|
-
"Only a record ID was provided.")
|
|
42
|
-
return self.base_url + f"/#dataType={data_type_name};recordId={record_id};view=dataRecord"
|
|
43
|
-
|
|
44
|
-
def experiment(self, experiment: ExperimentIdentifier) -> str:
|
|
45
|
-
"""
|
|
46
|
-
:param experiment: An object that can be used to identify an experiment in the system, be that an experiment
|
|
47
|
-
object, experiment protocol, or a notebook ID.
|
|
48
|
-
:return: A URL for navigating to the input experiment.
|
|
49
|
-
"""
|
|
50
|
-
return self.base_url + f"/#notebookExperimentId={AliasUtil.to_notebook_id(experiment)};view=eln"
|