Skip to content

Commit 5f3eea1

Browse files
committed
feat: add MatchAttributeFilter to compute model
This reflects the filter type recently added on the backend. Support it also in the compute_to_sdk_converter JIRA: CQ-2005 risk: low
1 parent d6382d3 commit 5f3eea1

15 files changed

Lines changed: 387 additions & 3 deletions

packages/gooddata-sdk/src/gooddata_sdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@
278278
CompoundMetricValueFilter,
279279
Filter,
280280
InlineFilter,
281+
MatchAttributeFilter,
281282
MetricValueComparisonCondition,
282283
MetricValueFilter,
283284
MetricValueRangeCondition,

packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
CompoundMetricValueFilter,
1111
Filter,
1212
InlineFilter,
13+
MatchAttributeFilter,
1314
MetricValueComparisonCondition,
1415
MetricValueFilter,
1516
MetricValueRangeCondition,
@@ -75,6 +76,16 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter:
7576
f = filter_dict["negativeAttributeFilter"]
7677
return NegativeAttributeFilter(label=ref_extract(f["label"]), values=f["notIn"]["values"])
7778

79+
if "matchAttributeFilter" in filter_dict:
80+
f = filter_dict["matchAttributeFilter"]
81+
return MatchAttributeFilter(
82+
label=ref_extract(f["label"]),
83+
match_type=f["matchType"],
84+
literal=f["literal"],
85+
case_sensitive=f.get("caseSensitive", False),
86+
negate=f.get("negate", False),
87+
)
88+
7889
if "relativeDateFilter" in filter_dict:
7990
f = filter_dict["relativeDateFilter"]
8091

packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from gooddata_api_client.models import (
2222
CompoundMeasureValueFilterCompoundMeasureValueFilter as CompoundMeasureValueFilterBody,
2323
)
24+
from gooddata_api_client.models import MatchAttributeFilterMatchAttributeFilter as MatchAttributeFilterBody
2425
from gooddata_api_client.models import NegativeAttributeFilterNegativeAttributeFilter as NegativeAttributeFilterBody
2526
from gooddata_api_client.models import PositiveAttributeFilterPositiveAttributeFilter as PositiveAttributeFilterBody
2627
from gooddata_api_client.models import RangeMeasureValueFilterRangeMeasureValueFilter as RangeMeasureValueFilterBody
@@ -152,6 +153,109 @@ def description(self, labels: dict[str, str], format_locale: str | None = None)
152153
return f"{labels.get(label_id, label_id)}: {values}"
153154

154155

156+
# mapping between the allowed match operators and their human-readable descriptions
157+
_ATTRIBUTE_MATCH_OPERATORS: dict[str, str] = {
158+
"STARTS_WITH": "starts with",
159+
"ENDS_WITH": "ends with",
160+
"CONTAINS": "contains",
161+
}
162+
163+
164+
class MatchAttributeFilter(Filter):
165+
def __init__(
166+
self,
167+
label: Union[ObjId, str, Attribute],
168+
literal: str,
169+
match_type: str,
170+
negate: bool = False,
171+
case_sensitive: bool = False,
172+
) -> None:
173+
super().__init__()
174+
175+
self._label = _extract_id_or_local_id(label)
176+
self._literal = literal
177+
178+
if match_type not in _ATTRIBUTE_MATCH_OPERATORS:
179+
raise ValueError(
180+
f"Match type must be one of {', '.join(_ATTRIBUTE_MATCH_OPERATORS.keys())}, got: {match_type}"
181+
)
182+
183+
self._match_type = match_type
184+
self._negate = negate
185+
self._case_sensitive = case_sensitive
186+
187+
@property
188+
def label(self) -> Union[ObjId, str]:
189+
return self._label
190+
191+
@label.setter
192+
def label(self, label: Union[ObjId, str]) -> None:
193+
self._label = label
194+
195+
@property
196+
def literal(self) -> str:
197+
return self._literal
198+
199+
@literal.setter
200+
def literal(self, literal: str) -> None:
201+
self._literal = literal
202+
203+
@property
204+
def match_type(self) -> str:
205+
return self._match_type
206+
207+
@match_type.setter
208+
def match_type(self, match_type: str) -> None:
209+
self._match_type = match_type
210+
211+
@property
212+
def negate(self) -> bool:
213+
return self._negate
214+
215+
@negate.setter
216+
def negate(self, negate: bool) -> None:
217+
self._negate = negate
218+
219+
@property
220+
def case_sensitive(self) -> bool:
221+
return self._case_sensitive
222+
223+
@case_sensitive.setter
224+
def case_sensitive(self, case_sensitive: bool) -> None:
225+
self._case_sensitive = case_sensitive
226+
227+
def is_noop(self) -> bool:
228+
return (not self._negate) and (not self._literal)
229+
230+
def as_api_model(self) -> afm_models.MatchAttributeFilter:
231+
label_id = _to_identifier(self._label)
232+
body = MatchAttributeFilterBody(
233+
label=label_id,
234+
literal=self._literal,
235+
match_type=self._match_type,
236+
negate=self._negate,
237+
case_sensitive=self._case_sensitive,
238+
)
239+
return afm_models.MatchAttributeFilter(body)
240+
241+
def description(self, labels: dict[str, str], format_locale: str | None = None) -> str:
242+
label_id = self.label.id if isinstance(self.label, ObjId) else self.label
243+
prefix = "not " if self._negate else ""
244+
return (
245+
f"{labels.get(label_id, label_id)}: {prefix}{_ATTRIBUTE_MATCH_OPERATORS[self._match_type]} {self.literal}"
246+
)
247+
248+
def __eq__(self, other: object) -> bool:
249+
return (
250+
isinstance(other, MatchAttributeFilter)
251+
and self._label == other._label
252+
and self._literal == other._literal
253+
and self._match_type == other._match_type
254+
and self._negate == other._negate
255+
and self._case_sensitive == other._case_sensitive
256+
)
257+
258+
155259
_GRANULARITY: set[str] = {
156260
"YEAR",
157261
"QUARTER",
@@ -557,7 +661,7 @@ def as_api_model(self) -> Union[afm_models.ComparisonMeasureValueFilter, afm_mod
557661
def description(self, labels: dict[str, str], format_locale: str | None = None) -> str:
558662
metric_id = self.metric.id if isinstance(self.metric, ObjId) else self.metric
559663
if self.operator in ["BETWEEN", "NOT_BETWEEN"] and len(self.values) == 2:
560-
not_between = "not" if self.operator == "NOT_BETWEEN" else ""
664+
not_between = "not " if self.operator == "NOT_BETWEEN" else ""
561665
values = cast(tuple[float, float], self.values)
562666
return f"{labels.get(metric_id, metric_id)}: {not_between}between {values[0]} - {values[1]}"
563667
else:

packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
CompoundMetricValueFilter,
99
ComputeToSdkConverter,
1010
InlineFilter,
11+
MatchAttributeFilter,
1112
MetricValueComparisonCondition,
1213
MetricValueFilter,
1314
MetricValueRangeCondition,
@@ -88,6 +89,60 @@ def test_negative_attribute_filter_conversion():
8889
assert result.values == ["val1", "val2"]
8990

9091

92+
def test_match_attribute_filter_conversion():
93+
filter_dict = json.loads(
94+
"""
95+
{
96+
"matchAttributeFilter": {
97+
"label": {
98+
"identifier": { "id": "attribute1", "type": "label" }
99+
},
100+
"matchType": "CONTAINS",
101+
"literal": "search term",
102+
"caseSensitive": false,
103+
"negate": false
104+
}
105+
}
106+
"""
107+
)
108+
109+
result = ComputeToSdkConverter.convert_filter(filter_dict)
110+
111+
assert isinstance(result, MatchAttributeFilter)
112+
assert result.label.id == "attribute1"
113+
assert result.match_type == "CONTAINS"
114+
assert result.literal == "search term"
115+
assert result.case_sensitive is False
116+
assert result.negate is False
117+
118+
119+
def test_match_attribute_filter_negated_case_sensitive():
120+
filter_dict = json.loads(
121+
"""
122+
{
123+
"matchAttributeFilter": {
124+
"label": {
125+
"identifier": { "id": "product_name", "type": "label" }
126+
},
127+
"matchType": "STARTS_WITH",
128+
"literal": "Premium",
129+
"caseSensitive": true,
130+
"negate": true
131+
}
132+
}
133+
"""
134+
)
135+
136+
result = ComputeToSdkConverter.convert_filter(filter_dict)
137+
138+
assert isinstance(result, MatchAttributeFilter)
139+
assert result.label.id == "product_name"
140+
assert result.match_type == "STARTS_WITH"
141+
assert result.literal == "Premium"
142+
assert result.case_sensitive is True
143+
assert result.negate is True
144+
145+
91146
def test_relative_date_filter_conversion():
92147
filter_dict = json.loads(
93148
"""
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"match_attribute_filter": {
3+
"case_sensitive": true,
4+
"label": {
5+
"local_identifier": "local_id"
6+
},
7+
"literal": "Foo",
8+
"match_type": "CONTAINS",
9+
"negate": false
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"match_attribute_filter": {
3+
"case_sensitive": false,
4+
"label": {
5+
"local_identifier": "local_id"
6+
},
7+
"literal": "foo",
8+
"match_type": "CONTAINS",
9+
"negate": false
10+
}
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"match_attribute_filter": {
3+
"case_sensitive": false,
4+
"label": {
5+
"identifier": {
6+
"id": "label.id",
7+
"type": "label"
8+
}
9+
},
10+
"literal": "bar",
11+
"match_type": "CONTAINS",
12+
"negate": false
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"match_attribute_filter": {
3+
"case_sensitive": false,
4+
"label": {
5+
"local_identifier": "local_id"
6+
},
7+
"literal": "suffix",
8+
"match_type": "ENDS_WITH",
9+
"negate": false
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"match_attribute_filter": {
3+
"case_sensitive": false,
4+
"label": {
5+
"local_identifier": "local_id"
6+
},
7+
"literal": "prefix",
8+
"match_type": "STARTS_WITH",
9+
"negate": false
10+
}
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"match_attribute_filter": {
3+
"case_sensitive": true,
4+
"label": {
5+
"local_identifier": "local_id"
6+
},
7+
"literal": "Pre",
8+
"match_type": "STARTS_WITH",
9+
"negate": true
10+
}
11+
}

0 commit comments

Comments
 (0)