From 2432b4a5493b64a628e06cb4c791396df18e75cb Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:45:09 +0000 Subject: [PATCH] feat(gooddata-sdk): [AUTO] Add dashboard arbitrary/match filter schemas; add usesArbitraryValues --- .../compute/compute_to_sdk_converter.py | 12 +++- .../src/gooddata_sdk/compute/model/filter.py | 54 ++++++++++++++- .../src/gooddata_sdk/visualization.py | 12 +++- .../compute/test_compute_to_sdk_converter.py | 68 ++++++++++++++++++ .../compute_model/test_attribute_filters.py | 69 +++++++++++++++++++ 5 files changed, 208 insertions(+), 7 deletions(-) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py index c1e2a0345..f562e8fef 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py @@ -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"] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py index 94171f156..fc2c4a3cd 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py @@ -125,10 +125,26 @@ 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: @@ -136,22 +152,54 @@ def description(self, labels: dict[str, str], format_locale: str | None = None) 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] = { diff --git a/packages/gooddata-sdk/src/gooddata_sdk/visualization.py b/packages/gooddata-sdk/src/gooddata_sdk/visualization.py index 35e5a9c14..c7dd0e451 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/visualization.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/visualization.py @@ -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"]) diff --git a/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py b/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py index d8794285e..d333c7853 100644 --- a/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py +++ b/packages/gooddata-sdk/tests/compute/test_compute_to_sdk_converter.py @@ -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( """ diff --git a/packages/gooddata-sdk/tests/compute_model/test_attribute_filters.py b/packages/gooddata-sdk/tests/compute_model/test_attribute_filters.py index e36fb8dea..1adaf31d5 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_attribute_filters.py +++ b/packages/gooddata-sdk/tests/compute_model/test_attribute_filters.py @@ -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