Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,19 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter:
"""
if "positiveAttributeFilter" in filter_dict:
f = filter_dict["positiveAttributeFilter"]
return PositiveAttributeFilter(label=ref_extract(f["label"]), values=f["in"]["values"])
return PositiveAttributeFilter(
label=ref_extract(f["label"]),
values=f["in"]["values"],
uses_arbitrary_values=f.get("usesArbitraryValues"),
)

if "negativeAttributeFilter" in filter_dict:
f = filter_dict["negativeAttributeFilter"]
return NegativeAttributeFilter(label=ref_extract(f["label"]), values=f["notIn"]["values"])
return NegativeAttributeFilter(
label=ref_extract(f["label"]),
values=f["notIn"]["values"],
uses_arbitrary_values=f.get("usesArbitraryValues"),
)

if "matchAttributeFilter" in filter_dict:
f = filter_dict["matchAttributeFilter"]
Expand Down
54 changes: 51 additions & 3 deletions packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,33 +125,81 @@ def __eq__(self, other: object) -> bool:


class PositiveAttributeFilter(AttributeFilter):
def __init__(
self,
label: Union[ObjId, str, Attribute],
values: list[str] | None = None,
uses_arbitrary_values: bool | None = None,
) -> None:
super().__init__(label, values)
self._uses_arbitrary_values = uses_arbitrary_values

@property
def uses_arbitrary_values(self) -> bool | None:
return self._uses_arbitrary_values

def as_api_model(self) -> afm_models.PositiveAttributeFilter:
label_id = _to_identifier(self._label)
elements = afm_models.AttributeFilterElements(values=self.values)
body = PositiveAttributeFilterBody(label=label_id, _in=elements, _check_type=False)
kwargs: dict[str, Any] = {"_check_type": False}
if self._uses_arbitrary_values is not None:
kwargs["uses_arbitrary_values"] = self._uses_arbitrary_values
body = PositiveAttributeFilterBody(label=label_id, _in=elements, **kwargs)
return afm_models.PositiveAttributeFilter(body, _check_type=False)

def description(self, labels: dict[str, str], format_locale: str | None = None) -> str:
label_id = self.label.id if isinstance(self.label, ObjId) else self.label
values = ", ".join(self.values) if len(self.values) else "All"
return f"{labels.get(label_id, label_id)}: {values}"

def __eq__(self, other: object) -> bool:
return (
isinstance(other, PositiveAttributeFilter)
and self._label == other._label
and self._values == other._values
and self._uses_arbitrary_values == other._uses_arbitrary_values
)


class NegativeAttributeFilter(AttributeFilter):
def __init__(
self,
label: Union[ObjId, str, Attribute],
values: list[str] | None = None,
uses_arbitrary_values: bool | None = None,
) -> None:
super().__init__(label, values)
self._uses_arbitrary_values = uses_arbitrary_values

@property
def uses_arbitrary_values(self) -> bool | None:
return self._uses_arbitrary_values

def is_noop(self) -> bool:
return len(self.values) == 0

def as_api_model(self) -> afm_models.NegativeAttributeFilter:
label_id = _to_identifier(self._label)
elements = afm_models.AttributeFilterElements(values=self.values)
body = NegativeAttributeFilterBody(label=label_id, not_in=elements, _check_type=False)
return afm_models.NegativeAttributeFilter(body)
kwargs: dict[str, Any] = {"_check_type": False}
if self._uses_arbitrary_values is not None:
kwargs["uses_arbitrary_values"] = self._uses_arbitrary_values
body = NegativeAttributeFilterBody(label=label_id, not_in=elements, **kwargs)
return afm_models.NegativeAttributeFilter(body, _check_type=False)

def description(self, labels: dict[str, str], format_locale: str | None = None) -> str:
label_id = self.label.id if isinstance(self.label, ObjId) else self.label
values = "All except " + ", ".join(self.values) if len(self.values) else "All"
return f"{labels.get(label_id, label_id)}: {values}"

def __eq__(self, other: object) -> bool:
return (
isinstance(other, NegativeAttributeFilter)
and self._label == other._label
and self._values == other._values
and self._uses_arbitrary_values == other._uses_arbitrary_values
)


# mapping between the allowed match operators and their human-readable descriptions
_ATTRIBUTE_MATCH_OPERATORS: dict[str, str] = {
Expand Down
12 changes: 10 additions & 2 deletions packages/gooddata-sdk/src/gooddata_sdk/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,22 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
# fallback to use URIs; SDK may be able to create filter with attr elements as uris...
in_values = f["in"]["values"] if "values" in f["in"] else f["in"]["uris"]

return PositiveAttributeFilter(label=ref_extract(f["displayForm"]), values=in_values)
return PositiveAttributeFilter(
label=ref_extract(f["displayForm"]),
values=in_values,
uses_arbitrary_values=f.get("usesArbitraryValues"),
)

elif "negativeAttributeFilter" in filter_obj:
f = filter_obj["negativeAttributeFilter"]
# fallback to use URIs; SDK may be able to create filter with attr elements as uris...
not_in_values = f["notIn"]["values"] if "values" in f["notIn"] else f["notIn"]["uris"]

return NegativeAttributeFilter(label=ref_extract(f["displayForm"]), values=not_in_values)
return NegativeAttributeFilter(
label=ref_extract(f["displayForm"]),
values=not_in_values,
uses_arbitrary_values=f.get("usesArbitraryValues"),
)
elif "arbitraryAttributeFilter" in filter_obj:
f = filter_obj["arbitraryAttributeFilter"]
label = ref_extract(f["label"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,74 @@ def test_negative_attribute_filter_conversion():
assert result.values == ["val1", "val2"]


def test_positive_attribute_filter_conversion_with_uses_arbitrary_values():
filter_dict = json.loads(
"""
{
"positiveAttributeFilter": {
"label": {
"identifier": { "id": "attribute1", "type": "label" }
},
"in": {
"values": [ "val1", "val2" ]
},
"usesArbitraryValues": true
}
}
"""
)

result = ComputeToSdkConverter.convert_filter(filter_dict)

assert isinstance(result, PositiveAttributeFilter)
assert result.uses_arbitrary_values is True


def test_negative_attribute_filter_conversion_with_uses_arbitrary_values():
filter_dict = json.loads(
"""
{
"negativeAttributeFilter": {
"label": {
"identifier": { "id": "attribute1", "type": "label" }
},
"notIn": {
"values": [ "val1", "val2" ]
},
"usesArbitraryValues": true
}
}
"""
)

result = ComputeToSdkConverter.convert_filter(filter_dict)

assert isinstance(result, NegativeAttributeFilter)
assert result.uses_arbitrary_values is True


def test_positive_attribute_filter_conversion_no_uses_arbitrary_values():
filter_dict = json.loads(
"""
{
"positiveAttributeFilter": {
"label": {
"identifier": { "id": "attribute1", "type": "label" }
},
"in": {
"values": [ "val1" ]
}
}
}
"""
)

result = ComputeToSdkConverter.convert_filter(filter_dict)

assert isinstance(result, PositiveAttributeFilter)
assert result.uses_arbitrary_values is None


def test_match_attribute_filter_conversion():
filter_dict = json.loads(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,72 @@ def test_match_filter_inequality_different_case_sensitive():
f1 = MatchAttributeFilter(label="test", literal="foo", match_type="CONTAINS")
f2 = MatchAttributeFilter(label="test", literal="foo", match_type="CONTAINS", case_sensitive=True)
assert f1 != f2


# --- uses_arbitrary_values tests ---


def test_positive_filter_uses_arbitrary_values_included_when_set():
f = PositiveAttributeFilter(
label=ObjId(type="label", id="label.id"),
values=["val1"],
uses_arbitrary_values=True,
)
api_dict = f.as_api_model().to_dict()
assert api_dict["positive_attribute_filter"]["uses_arbitrary_values"] is True


def test_positive_filter_uses_arbitrary_values_false():
f = PositiveAttributeFilter(
label=ObjId(type="label", id="label.id"),
values=["val1"],
uses_arbitrary_values=False,
)
api_dict = f.as_api_model().to_dict()
assert api_dict["positive_attribute_filter"]["uses_arbitrary_values"] is False


def test_positive_filter_uses_arbitrary_values_absent_by_default():
f = PositiveAttributeFilter(label=ObjId(type="label", id="label.id"), values=["val1"])
api_dict = f.as_api_model().to_dict()
assert "uses_arbitrary_values" not in api_dict["positive_attribute_filter"]


def test_negative_filter_uses_arbitrary_values_included_when_set():
f = NegativeAttributeFilter(
label=ObjId(type="label", id="label.id"),
values=["val1"],
uses_arbitrary_values=True,
)
api_dict = f.as_api_model().to_dict()
assert api_dict["negative_attribute_filter"]["uses_arbitrary_values"] is True


def test_negative_filter_uses_arbitrary_values_absent_by_default():
f = NegativeAttributeFilter(label=ObjId(type="label", id="label.id"), values=["val1"])
api_dict = f.as_api_model().to_dict()
assert "uses_arbitrary_values" not in api_dict["negative_attribute_filter"]


def test_positive_filter_equality_with_uses_arbitrary_values():
f1 = PositiveAttributeFilter(label="test", values=["a"], uses_arbitrary_values=True)
f2 = PositiveAttributeFilter(label="test", values=["a"], uses_arbitrary_values=True)
assert f1 == f2


def test_positive_filter_inequality_different_uses_arbitrary_values():
f1 = PositiveAttributeFilter(label="test", values=["a"], uses_arbitrary_values=True)
f2 = PositiveAttributeFilter(label="test", values=["a"], uses_arbitrary_values=None)
assert f1 != f2


def test_negative_filter_equality_with_uses_arbitrary_values():
f1 = NegativeAttributeFilter(label="test", values=["a"], uses_arbitrary_values=True)
f2 = NegativeAttributeFilter(label="test", values=["a"], uses_arbitrary_values=True)
assert f1 == f2


def test_negative_filter_inequality_different_uses_arbitrary_values():
f1 = NegativeAttributeFilter(label="test", values=["a"], uses_arbitrary_values=True)
f2 = NegativeAttributeFilter(label="test", values=["a"])
assert f1 != f2
Loading