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
2 changes: 2 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,12 @@
AllMetricValueFilter,
AllTimeDateFilter,
AttributeFilter,
AttributeFilterMatchType,
BoundedFilter,
CompoundMetricValueFilter,
Filter,
InlineFilter,
MatchAttributeFilter,
MetricValueComparisonCondition,
MetricValueFilter,
MetricValueRangeCondition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CompoundMetricValueFilter,
Filter,
InlineFilter,
MatchAttributeFilter,
MetricValueComparisonCondition,
MetricValueFilter,
MetricValueRangeCondition,
Expand Down Expand Up @@ -75,6 +76,18 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter:
f = filter_dict["negativeAttributeFilter"]
return NegativeAttributeFilter(label=ref_extract(f["label"]), values=f["notIn"]["values"])

if "matchAttributeFilter" in filter_dict:
f = filter_dict["matchAttributeFilter"]
return MatchAttributeFilter(
label=ref_extract(f["label"]),
literal=f["literal"],
match_type=f["matchType"],
negate=f.get("negate", False),
case_sensitive=f.get("caseSensitive", True),
local_identifier=f.get("localIdentifier"),
apply_on_result=f.get("applyOnResult"),
)

if "relativeDateFilter" in filter_dict:
f = filter_dict["relativeDateFilter"]

Expand Down
84 changes: 84 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from gooddata_api_client.models import NegativeAttributeFilterNegativeAttributeFilter as NegativeAttributeFilterBody
from gooddata_api_client.models import PositiveAttributeFilterPositiveAttributeFilter as PositiveAttributeFilterBody
from gooddata_api_client.models import RangeMeasureValueFilterRangeMeasureValueFilter as RangeMeasureValueFilterBody
from gooddata_api_client.models import MatchAttributeFilterMatchAttributeFilter as MatchAttributeFilterBody
from gooddata_api_client.models import RankingFilterRankingFilter as RankingFilterBody
from gooddata_api_client.models import RelativeDateFilterRelativeDateFilter as RelativeDateFilterBody

Expand Down Expand Up @@ -78,6 +79,8 @@

EmptyValueHandling: TypeAlias = Literal["INCLUDE", "EXCLUDE", "ONLY"]

AttributeFilterMatchType: TypeAlias = Literal["STARTS_WITH", "ENDS_WITH", "CONTAINS"]


def _extract_id_or_local_id(val: Union[ObjId, Attribute, Metric, str]) -> Union[ObjId, str]:
if isinstance(val, (str, ObjId)):
Expand Down Expand Up @@ -152,6 +155,87 @@ def description(self, labels: dict[str, str], format_locale: str | None = None)
return f"{labels.get(label_id, label_id)}: {values}"


class MatchAttributeFilter(Filter):
def __init__(
self,
label: Union[ObjId, str, Attribute],
literal: str,
match_type: AttributeFilterMatchType,
negate: bool = False,
case_sensitive: bool = True,
local_identifier: str | None = None,
apply_on_result: bool | None = None,
) -> None:
super().__init__()

if match_type not in ("STARTS_WITH", "ENDS_WITH", "CONTAINS"):
raise ValueError(
f"Invalid match attribute filter match type '{match_type}'. "
"It is expected to be one of: STARTS_WITH, ENDS_WITH, CONTAINS"
)

self._label = _extract_id_or_local_id(label)
self._literal = literal
self._match_type = match_type
self._negate = negate
self._case_sensitive = case_sensitive
self._local_identifier = local_identifier
self._apply_on_result = apply_on_result

@property
def label(self) -> Union[ObjId, str]:
return self._label

@property
def literal(self) -> str:
return self._literal

@property
def match_type(self) -> AttributeFilterMatchType:
return self._match_type

@property
def negate(self) -> bool:
return self._negate

@property
def case_sensitive(self) -> bool:
return self._case_sensitive

@property
def local_identifier(self) -> str | None:
return self._local_identifier

@property
def apply_on_result(self) -> bool | None:
return self._apply_on_result

def is_noop(self) -> bool:
return False

def as_api_model(self) -> afm_models.MatchAttributeFilter:
label_id = _to_identifier(self._label)
body_params: dict[str, Any] = {
"label": label_id,
"literal": self._literal,
"match_type": self._match_type,
"negate": self._negate,
"case_sensitive": self._case_sensitive,
"_check_type": False,
}
if self._local_identifier is not None:
body_params["local_identifier"] = self._local_identifier
if self._apply_on_result is not None:
body_params["apply_on_result"] = self._apply_on_result
body = MatchAttributeFilterBody(**body_params)
return afm_models.MatchAttributeFilter(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
negate_str = "not " if self._negate else ""
return f"{labels.get(label_id, label_id)}: {negate_str}{self._match_type.lower()} {self._literal}"


_GRANULARITY: set[str] = {
"YEAR",
"QUARTER",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CompoundMetricValueFilter,
ComputeToSdkConverter,
InlineFilter,
MatchAttributeFilter,
MetricValueComparisonCondition,
MetricValueFilter,
MetricValueRangeCondition,
Expand Down Expand Up @@ -265,6 +266,66 @@ def test_ranking_filter_with_dimensionality_conversion():
assert result.value == 5


def test_match_attribute_filter_conversion():
filter_dict = json.loads(
"""
{
"matchAttributeFilter": {
"label": {
"identifier": { "id": "attribute1", "type": "label" }
},
"literal": "foo",
"matchType": "STARTS_WITH",
"negate": false,
"caseSensitive": true
}
}
"""
)

result = ComputeToSdkConverter.convert_filter(filter_dict)

assert isinstance(result, MatchAttributeFilter)
assert result.label.id == "attribute1"
assert result.literal == "foo"
assert result.match_type == "STARTS_WITH"
assert result.negate is False
assert result.case_sensitive is True
assert result.local_identifier is None
assert result.apply_on_result is None


def test_match_attribute_filter_conversion_with_optional_fields():
filter_dict = json.loads(
"""
{
"matchAttributeFilter": {
"label": {
"identifier": { "id": "attribute2", "type": "label" }
},
"literal": "bar",
"matchType": "CONTAINS",
"negate": true,
"caseSensitive": false,
"localIdentifier": "my_filter",
"applyOnResult": true
}
}
"""
)

result = ComputeToSdkConverter.convert_filter(filter_dict)

assert isinstance(result, MatchAttributeFilter)
assert result.label.id == "attribute2"
assert result.literal == "bar"
assert result.match_type == "CONTAINS"
assert result.negate is True
assert result.case_sensitive is False
assert result.local_identifier == "my_filter"
assert result.apply_on_result is True


def test_inline_filter():
filter_dict = json.loads(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os

import pytest
from gooddata_sdk import NegativeAttributeFilter, ObjId, PositiveAttributeFilter
from gooddata_sdk import MatchAttributeFilter, NegativeAttributeFilter, ObjId, PositiveAttributeFilter

_current_dir = os.path.dirname(os.path.abspath(__file__))

Expand Down Expand Up @@ -76,3 +76,55 @@ def test_empty_positive_filter_is_not_noop():
f = PositiveAttributeFilter(label="test")

assert f.is_noop() is False


def test_match_attribute_filter_starts_with():
f = MatchAttributeFilter(label="local_id", literal="foo", match_type="STARTS_WITH")
model = f.as_api_model()
d = model.to_dict()
inner = d["match_attribute_filter"]
assert inner["label"] == {"local_identifier": "local_id"}
assert inner["literal"] == "foo"
assert inner["match_type"] == "STARTS_WITH"
assert inner["negate"] is False
assert inner["case_sensitive"] is True


def test_match_attribute_filter_ends_with_obj_id():
f = MatchAttributeFilter(
label=ObjId(type="label", id="label.id"),
literal="bar",
match_type="ENDS_WITH",
)
model = f.as_api_model()
d = model.to_dict()
inner = d["match_attribute_filter"]
assert inner["label"] == {"identifier": {"id": "label.id", "type": "label"}}
assert inner["literal"] == "bar"
assert inner["match_type"] == "ENDS_WITH"


def test_match_attribute_filter_contains_negate_case_insensitive():
f = MatchAttributeFilter(
label="local_id",
literal="baz",
match_type="CONTAINS",
negate=True,
case_sensitive=False,
)
model = f.as_api_model()
d = model.to_dict()
inner = d["match_attribute_filter"]
assert inner["match_type"] == "CONTAINS"
assert inner["negate"] is True
assert inner["case_sensitive"] is False


def test_match_attribute_filter_is_not_noop():
f = MatchAttributeFilter(label="local_id", literal="x", match_type="STARTS_WITH")
assert f.is_noop() is False


def test_match_attribute_filter_invalid_match_type():
with pytest.raises(ValueError, match="STARTS_WITH"):
MatchAttributeFilter(label="local_id", literal="x", match_type="INVALID")
Loading