Skip to content

Commit e1d6ad4

Browse files
authored
Merge pull request #1623 from hkad98/jkd/metric-value-filter
feat(gooddata-sdk): add dimensionality support to MetricValueFilter
2 parents 38b0798 + 4ada650 commit e1d6ad4

9 files changed

Lines changed: 274 additions & 10 deletions

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
from gooddata_sdk.utils import ref_extract, ref_extract_obj_id
3232

3333

34+
def _extract_dimensionality(f: dict[str, Any]) -> list[Union[str, ObjId, Attribute, Metric]] | None:
35+
# mypy is unable to automatically convert Union[str, ObjId] to Union[str, ObjId, Attribute, Metric]
36+
# so use explicit cast here
37+
if "dimensionality" not in f:
38+
return None
39+
return [cast(Union[str, ObjId, Attribute, Metric], ref_extract(a)) for a in f["dimensionality"]]
40+
41+
3442
class ComputeToSdkConverter:
3543
"""
3644
Provides functions to convert Compute API model objects represented as dictionaries to the SDK Compute model.
@@ -142,6 +150,7 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter:
142150
operator=f["operator"],
143151
values=f["value"],
144152
treat_nulls_as=f.get("treatNullValuesAs"),
153+
dimensionality=_extract_dimensionality(f),
145154
)
146155

147156
if "rangeMeasureValueFilter" in filter_dict:
@@ -152,6 +161,7 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter:
152161
operator=f["operator"],
153162
values=(f["from"], f["to"]),
154163
treat_nulls_as=f.get("treatNullValuesAs"),
164+
dimensionality=_extract_dimensionality(f),
155165
)
156166

157167
if "compoundMeasureValueFilter" in filter_dict:
@@ -174,22 +184,15 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter:
174184
metric=ref_extract(f["measure"]),
175185
conditions=conditions,
176186
treat_nulls_as=f.get("treatNullValuesAs"),
187+
dimensionality=_extract_dimensionality(f),
177188
)
178189

179190
if "rankingFilter" in filter_dict:
180191
f = filter_dict["rankingFilter"]
181192

182-
# mypy is unable to automatically convert Union[str, ObjId] to Union[str, ObjId, Attribute, Metric]
183-
# so use explicit cast here
184-
dimensionality = (
185-
[cast(Union[str, ObjId, Attribute, Metric], ref_extract(a)) for a in f["dimensionality"]]
186-
if "dimensionality" in f
187-
else None
188-
)
189-
190193
return RankingFilter(
191194
metrics=[ref_extract(m) for m in f["measures"]],
192-
dimensionality=dimensionality,
195+
dimensionality=_extract_dimensionality(f),
193196
operator=f["operator"],
194197
value=f["value"],
195198
)

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ def __init__(
586586
operator: str,
587587
values: Union[float, int, tuple[float, float]],
588588
treat_nulls_as: Union[float, None] = None,
589+
dimensionality: list[Union[str, ObjId, Attribute, Metric]] | None = None,
589590
) -> None:
590591
super().__init__()
591592

@@ -611,6 +612,7 @@ def __init__(
611612
self._metric = _extract_id_or_local_id(metric)
612613
self._operator = operator
613614
self._treat_nulls_as = treat_nulls_as
615+
self._dimensionality = [_extract_id_or_local_id(d) for d in dimensionality] if dimensionality else None
614616

615617
@property
616618
def metric(self) -> Union[ObjId, str]:
@@ -632,19 +634,25 @@ def values(self) -> Union[tuple[float], tuple[float, float]]:
632634
def treat_nulls_as(self) -> Union[float, None]:
633635
return self._treat_nulls_as
634636

637+
@property
638+
def dimensionality(self) -> list[Union[ObjId, str]] | None:
639+
return self._dimensionality
640+
635641
def is_noop(self) -> bool:
636642
return False
637643

638644
def as_api_model(self) -> Union[afm_models.ComparisonMeasureValueFilter, afm_models.RangeMeasureValueFilter]:
639645
measure = _to_identifier(self._metric)
640646

641-
kwargs = dict(
647+
kwargs: dict[str, Any] = dict(
642648
measure=measure,
643649
operator=self.operator,
644650
_check_type=False,
645651
)
646652
if self.treat_nulls_as is not None:
647653
kwargs["treat_null_values_as"] = self.treat_nulls_as
654+
if self._dimensionality is not None:
655+
kwargs["dimensionality"] = [_to_identifier(d) for d in self._dimensionality]
648656

649657
if _METRIC_VALUE_FILTER_OPERATORS[self.operator] == "comparison":
650658
kwargs["value"] = self.values[0]
@@ -727,11 +735,13 @@ def __init__(
727735
metric: Union[ObjId, str, Metric],
728736
conditions: list[MetricValueCondition],
729737
treat_nulls_as: Union[float, None] = None,
738+
dimensionality: list[Union[str, ObjId, Attribute, Metric]] | None = None,
730739
) -> None:
731740
super().__init__()
732741
self._metric = _extract_id_or_local_id(metric)
733742
self._conditions = conditions
734743
self._treat_nulls_as = treat_nulls_as
744+
self._dimensionality = [_extract_id_or_local_id(d) for d in dimensionality] if dimensionality else None
735745

736746
@property
737747
def metric(self) -> Union[ObjId, str]:
@@ -745,6 +755,10 @@ def conditions(self) -> list[MetricValueCondition]:
745755
def treat_nulls_as(self) -> Union[float, None]:
746756
return self._treat_nulls_as
747757

758+
@property
759+
def dimensionality(self) -> list[Union[ObjId, str]] | None:
760+
return self._dimensionality
761+
748762
def is_noop(self) -> bool:
749763
return len(self.conditions) == 0
750764

@@ -758,6 +772,8 @@ def as_api_model(self) -> afm_models.CompoundMeasureValueFilter:
758772
)
759773
if self.treat_nulls_as is not None:
760774
kwargs["treat_null_values_as"] = self.treat_nulls_as
775+
if self._dimensionality is not None:
776+
kwargs["dimensionality"] = [_to_identifier(d) for d in self._dimensionality]
761777

762778
body = CompoundMeasureValueFilterBody(**kwargs)
763779
return afm_models.CompoundMeasureValueFilter(body, _check_type=False)

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
MetricValueFilter,
1414
MetricValueRangeCondition,
1515
NegativeAttributeFilter,
16+
ObjId,
1617
PopDateMetric,
1718
PopDatesetMetric,
1819
PositiveAttributeFilter,
@@ -270,6 +271,77 @@ def test_compound_measure_value_filter_conversion():
270271
assert result.conditions[1].to_value == 20
271272

272273

274+
def test_comparison_measure_value_filter_with_dimensionality_conversion():
275+
filter_dict = json.loads(
276+
"""
277+
{
278+
"comparisonMeasureValueFilter": {
279+
"measure": { "localIdentifier": "measureLocalId" },
280+
"operator": "GREATER_THAN",
281+
"value": 100,
282+
"dimensionality": [
283+
{ "localIdentifier": "attributeLocalId" },
284+
{ "identifier": { "id": "label.id", "type": "label" } }
285+
]
286+
}
287+
}
288+
"""
289+
)
290+
291+
result = ComputeToSdkConverter.convert_filter(filter_dict)
292+
293+
assert isinstance(result, MetricValueFilter)
294+
assert result.dimensionality is not None
295+
assert result.dimensionality[0] == "attributeLocalId"
296+
assert result.dimensionality[1] == ObjId(type="label", id="label.id")
297+
298+
299+
def test_range_measure_value_filter_with_dimensionality_conversion():
300+
filter_dict = json.loads(
301+
"""
302+
{
303+
"rangeMeasureValueFilter": {
304+
"measure": { "localIdentifier": "measureLocalId" },
305+
"operator": "BETWEEN",
306+
"from": 100,
307+
"to": 200,
308+
"dimensionality": [
309+
{ "localIdentifier": "attributeLocalId" }
310+
]
311+
}
312+
}
313+
"""
314+
)
315+
316+
result = ComputeToSdkConverter.convert_filter(filter_dict)
317+
318+
assert isinstance(result, MetricValueFilter)
319+
assert result.dimensionality == ["attributeLocalId"]
320+
321+
322+
def test_compound_measure_value_filter_with_dimensionality_conversion():
323+
filter_dict = json.loads(
324+
"""
325+
{
326+
"compoundMeasureValueFilter": {
327+
"measure": { "localIdentifier": "measureLocalId" },
328+
"conditions": [
329+
{ "comparison": { "operator": "GREATER_THAN", "value": 100 } }
330+
],
331+
"dimensionality": [
332+
{ "localIdentifier": "attributeLocalId" }
333+
]
334+
}
335+
}
336+
"""
337+
)
338+
339+
result = ComputeToSdkConverter.convert_filter(filter_dict)
340+
341+
assert isinstance(result, CompoundMetricValueFilter)
342+
assert result.dimensionality == ["attributeLocalId"]
343+
344+
273345
def test_ranking_filter_conversion():
274346
filter_dict = json.loads(
275347
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"comparison_measure_value_filter": {
3+
"dimensionality": [
4+
{
5+
"local_identifier": "local_id3"
6+
}
7+
],
8+
"measure": {
9+
"local_identifier": "local_id1"
10+
},
11+
"operator": "GREATER_THAN",
12+
"treat_null_values_as": 0,
13+
"value": 10.0
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"comparison_measure_value_filter": {
3+
"dimensionality": [
4+
{
5+
"local_identifier": "local_id3"
6+
},
7+
{
8+
"local_identifier": "local_id4"
9+
}
10+
],
11+
"measure": {
12+
"local_identifier": "local_id1"
13+
},
14+
"operator": "GREATER_THAN",
15+
"value": 10.0
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"comparison_measure_value_filter": {
3+
"dimensionality": [
4+
{
5+
"local_identifier": "local_id3"
6+
},
7+
{
8+
"identifier": {
9+
"id": "label.id",
10+
"type": "label"
11+
}
12+
}
13+
],
14+
"measure": {
15+
"local_identifier": "local_id1"
16+
},
17+
"operator": "GREATER_THAN",
18+
"value": 10.0
19+
}
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"range_measure_value_filter": {
3+
"_from": 2,
4+
"dimensionality": [
5+
{
6+
"local_identifier": "local_id3"
7+
},
8+
{
9+
"local_identifier": "local_id4"
10+
}
11+
],
12+
"measure": {
13+
"local_identifier": "local_id1"
14+
},
15+
"operator": "BETWEEN",
16+
"to": 3
17+
}
18+
}

packages/gooddata-sdk/tests/compute_model/test_compound_metric_value_filter.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
from gooddata_sdk import (
5+
Attribute,
56
CompoundMetricValueFilter,
67
MetricValueComparisonCondition,
78
MetricValueRangeCondition,
@@ -35,3 +36,64 @@ def test_compound_metric_value_filter_to_api_model():
3536
def test_compound_metric_value_filter_noop_when_no_conditions():
3637
f = CompoundMetricValueFilter(metric=ObjId(type="metric", id="metric.id"), conditions=[])
3738
assert f.is_noop() is True
39+
40+
41+
def test_compound_metric_value_filter_with_dimensionality_to_api_model():
42+
f = CompoundMetricValueFilter(
43+
metric="local_id1",
44+
conditions=[
45+
MetricValueComparisonCondition(operator="GREATER_THAN", value=10),
46+
],
47+
dimensionality=["local_id3", Attribute(local_id="local_id4", label="label.id")],
48+
)
49+
50+
assert f.dimensionality == ["local_id3", "local_id4"]
51+
assert f.as_api_model().to_dict() == {
52+
"compound_measure_value_filter": {
53+
"conditions": [
54+
{"comparison": {"operator": "GREATER_THAN", "value": 10.0}},
55+
],
56+
"dimensionality": [
57+
{"local_identifier": "local_id3"},
58+
{"local_identifier": "local_id4"},
59+
],
60+
"measure": {"local_identifier": "local_id1"},
61+
}
62+
}
63+
64+
65+
def test_compound_metric_value_filter_with_dimensionality_mixed_ids():
66+
f = CompoundMetricValueFilter(
67+
metric="local_id1",
68+
conditions=[
69+
MetricValueComparisonCondition(operator="GREATER_THAN", value=10),
70+
],
71+
dimensionality=["local_id3", ObjId(type="label", id="label.id")],
72+
treat_nulls_as=0,
73+
)
74+
75+
assert f.as_api_model().to_dict() == {
76+
"compound_measure_value_filter": {
77+
"conditions": [
78+
{"comparison": {"operator": "GREATER_THAN", "value": 10.0}},
79+
],
80+
"dimensionality": [
81+
{"local_identifier": "local_id3"},
82+
{"identifier": {"id": "label.id", "type": "label"}},
83+
],
84+
"measure": {"local_identifier": "local_id1"},
85+
"treat_null_values_as": 0,
86+
}
87+
}
88+
89+
90+
def test_compound_metric_value_filter_without_dimensionality_omits_field():
91+
f = CompoundMetricValueFilter(
92+
metric="local_id1",
93+
conditions=[
94+
MetricValueComparisonCondition(operator="GREATER_THAN", value=10),
95+
],
96+
)
97+
98+
assert f.dimensionality is None
99+
assert "dimensionality" not in f.as_api_model().to_dict()["compound_measure_value_filter"]

0 commit comments

Comments
 (0)