Skip to content
Merged
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
52 changes: 45 additions & 7 deletions gooddata-sdk/gooddata_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from enum import Enum, auto
from pathlib import Path
from shutil import rmtree
from typing import Any, Callable, NamedTuple, Union, cast, no_type_check
from typing import Any, Callable, NamedTuple, Optional, Union, cast, no_type_check
from warnings import warn
from xml.etree import ElementTree as ET

Expand Down Expand Up @@ -43,6 +43,25 @@ class HttpMethod(Enum):
PATCH = auto()


class ObjRefType(Enum):
"""Enum representing valid tiger object reference types."""

ATTRIBUTE = "attribute"
METRIC = "metric"
LABEL = "label"
DATASET = "dataset"
FACT = "fact"


UI_TO_TIGER_REF_TYPE = {
"attribute": ObjRefType.ATTRIBUTE,
"measure": ObjRefType.METRIC,
"displayForm": ObjRefType.LABEL,
"dataSet": ObjRefType.DATASET,
"fact": ObjRefType.FACT,
}


def id_obj_to_key(id_obj: IdObjType) -> str:
"""
Given an object containing an id+type pair, this function will return a string key.
Expand Down Expand Up @@ -396,28 +415,47 @@ def read_json(path: Union[str, Path]) -> Any:
return json.loads(f.read())


def ref_extract_obj_id(ref: dict[str, Any]) -> ObjId:
def ref_extract_obj_id(ref: dict[str, Any], default_type: Optional[ObjRefType] = None) -> ObjId:
"""
Extracts ObjId from a ref dictionary.

The ref dictionary will most likely conform to one of two forms:
- ui-sdk -> ref: { identifier: str, type: str }
- tiger -> ref: { identifier: { id: str, type: str } }

:param ref: the ref to extract from
:param default_type: the type of the object to fall back to in case of string identifier
:return: the extracted ObjId
:raises ValueError: if the ref is not an identifier
"""
if "identifier" in ref:
return ObjId(id=ref["identifier"]["id"], type=ref["identifier"]["type"])
identifier = ref.get("identifier")
if not identifier:
raise ValueError("invalid ref. must be identifier")

if isinstance(identifier, str):
if default_type:
return ObjId(id=identifier, type=default_type.value)

ui_type = ref.get("type")
if not ui_type or ui_type not in UI_TO_TIGER_REF_TYPE:
raise ValueError("UI objRef type is not recognized and fallback type is not provided")

converted_type = UI_TO_TIGER_REF_TYPE[ui_type].value
return ObjId(id=identifier, type=converted_type)

raise ValueError("invalid ref. must be identifier")
return ObjId(id=identifier["id"], type=identifier["type"])


def ref_extract(ref: dict[str, Any]) -> Union[str, ObjId]:
def ref_extract(ref: dict[str, Any], default_type: Optional[ObjRefType] = None) -> Union[str, ObjId]:
"""
Extracts an object id from a ref dictionary: either an identifier or a localIdentifier.
:param ref: the ref to extract from
:param default_type: ref type to use in case of ui-sdk form of identifier object
:return: thr extracted object id
:raises ValueError: if the ref is not an identifier or localIdentifier
"""
try:
return ref_extract_obj_id(ref)
return ref_extract_obj_id(ref, default_type)
except ValueError:
pass

Expand Down
53 changes: 43 additions & 10 deletions gooddata-sdk/gooddata_sdk/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@
PopDatesetMetric,
SimpleMetric,
)
from gooddata_sdk.utils import IdObjType, SideLoads, load_all_entities, ref_extract, ref_extract_obj_id, safeget
from gooddata_sdk.utils import (
IdObjType,
ObjRefType,
SideLoads,
load_all_entities,
ref_extract,
ref_extract_obj_id,
safeget,
)

#
# Conversion from types stored in visualization into the gooddata_afm_client models.
Expand Down Expand Up @@ -166,23 +174,30 @@ 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"], ObjRefType.LABEL),
values=in_values,
)

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"], ObjRefType.LABEL),
values=not_in_values,
)

elif "relativeDateFilter" in filter_obj:
f = filter_obj["relativeDateFilter"]

# there is filter present, but uses all time
if ("from" not in f) or ("to" not in f):
return AllTimeFilter(ref_extract_obj_id(f["dataSet"]))
return AllTimeFilter(ref_extract_obj_id(f["dataSet"], ObjRefType.DATASET))

return RelativeDateFilter(
dataset=ref_extract_obj_id(f["dataSet"]),
dataset=ref_extract_obj_id(f["dataSet"], ObjRefType.DATASET),
granularity=_GRANULARITY_CONVERSION[f["granularity"]],
from_shift=f["from"],
to_shift=f["to"],
Expand All @@ -191,7 +206,12 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
elif "absoluteDateFilter" in filter_obj:
f = filter_obj["absoluteDateFilter"]

return AbsoluteDateFilter(dataset=ref_extract_obj_id(f["dataSet"]), from_date=f["from"], to_date=f["to"])
return AbsoluteDateFilter(
dataset=ref_extract_obj_id(f["dataSet"], ObjRefType.DATASET),
from_date=f["from"],
to_date=f["to"],
)

elif "measureValueFilter" in filter_obj:
f = filter_obj["measureValueFilter"]

Expand All @@ -211,6 +231,7 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
values=c["value"],
treat_nulls_as=treat_values_as_null,
)

elif "range" in condition:
c = condition["range"]
treat_values_as_null = c.get("treatNullValuesAs")
Expand All @@ -220,6 +241,7 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
values=(c["from"], c["to"]),
treat_nulls_as=treat_values_as_null,
)

elif "rankingFilter" in filter_obj:
f = filter_obj["rankingFilter"]
# mypy is unable to automatically convert Union[str, ObjId] to Union[str, ObjId, Attribute, Metric]
Expand Down Expand Up @@ -254,15 +276,20 @@ def _convert_metric_to_computable(metric: dict[str, Any]) -> Metric:

return SimpleMetric(
local_id=local_id,
item=ref_extract_obj_id(d["item"]),
item=ref_extract_obj_id(d["item"], ObjRefType.FACT),
aggregation=aggregation,
compute_ratio=compute_ratio,
filters=filters,
)

elif "popMeasureDefinition" in measure_def:
d = measure_def["popMeasureDefinition"]
date_attributes = [PopDate(attribute=ref_extract_obj_id(d["popAttribute"]), periods_ago=1)]
date_attributes = [
PopDate(
attribute=ref_extract_obj_id(d["popAttribute"], ObjRefType.ATTRIBUTE),
periods_ago=1,
),
]

return PopDateMetric(
local_id=local_id,
Expand All @@ -273,7 +300,9 @@ def _convert_metric_to_computable(metric: dict[str, Any]) -> Metric:
elif "previousPeriodMeasure" in measure_def:
d = measure_def["previousPeriodMeasure"]

date_datasets = [PopDateDataset(ref_extract(dd["dataSet"]), dd["periodsAgo"]) for dd in d["dateDataSets"]]
date_datasets = [
PopDateDataset(ref_extract(dd["dataSet"], ObjRefType.DATASET), dd["periodsAgo"]) for dd in d["dateDataSets"]
]

return PopDatesetMetric(
local_id=local_id,
Expand Down Expand Up @@ -394,7 +423,11 @@ def show_all_values(self) -> Optional[bool]:
return self._a.get("showAllValues")

def as_computable(self) -> Attribute:
return Attribute(local_id=self.local_id, label=ref_extract(self.label), show_all_values=self.show_all_values)
return Attribute(
local_id=self.local_id,
label=ref_extract(self.label, ObjRefType.LABEL),
show_all_values=self.show_all_values,
)

def __str__(self) -> str:
return self.__repr__()
Expand Down
135 changes: 135 additions & 0 deletions gooddata-sdk/tests/table/fixtures/vis_objs/vis_with_ui_refs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
{
"id": "923fb97a-4eb8-4cd7-b564-e72361addc79",
"type": "visualizationObject",
"attributes": {
"title": "testik",
"description": "",
"areRelationsValid": true,
"content": {
"buckets": [
{
"items": [
{
"attribute": {
"localIdentifier": "bb94cc071f67461f97cc30a8ef54fbb7",
"displayForm": {
"identifier": "Agent.Name",
"type": "displayForm"
}
}
},
{
"attribute": {
"localIdentifier": "e497c4b2398c4f7faaa2212ddb2c5df7",
"displayForm": {
"identifier": "Engaged_Role.Name",
"type": "displayForm"
}
}
},
{
"attribute": {
"localIdentifier": "0baaee3c223f4fd6a707a98beca780ca",
"displayForm": {
"identifier": "Engagement.Engagement",
"type": "displayForm"
}
}
},
{
"attribute": {
"localIdentifier": "44bc41d90b99431cac722b0fae53c7b2",
"displayForm": {
"identifier": "Question.Name",
"type": "displayForm"
}
}
}
],
"localIdentifier": "attribute"
}
],
"filters": [
{
"relativeDateFilter": {
"dataSet": {
"identifier": "Start_Time",
"type": "dataSet"
},
"granularity": "GDC.time.date",
"from": -1,
"to": -1
}
},
{
"negativeAttributeFilter": {
"localIdentifier": "81d4b8d638444fe1a08605e6acff289e",
"displayForm": {
"identifier": "Question.Question",
"type": "displayForm"
},
"notIn": {
"uris": []
}
}
},
{
"negativeAttributeFilter": {
"localIdentifier": "71156c3bcc7846668c3bb3ca2e09945c",
"displayForm": {
"identifier": "Engagement_Type.Type",
"type": "displayForm"
},
"notIn": {
"uris": []
}
}
},
{
"negativeAttributeFilter": {
"localIdentifier": "d4df08d678b645258473a455a5de560b",
"displayForm": {
"identifier": "Review_Type.Type",
"type": "displayForm"
},
"notIn": {
"uris": []
}
}
},
{
"negativeAttributeFilter": {
"localIdentifier": "64c8e3f20f22427c9b0e30a78f522019",
"displayForm": {
"identifier": "Agent.Agent",
"type": "displayForm"
},
"notIn": {
"uris": []
}
}
}
],
"properties": {
"sortItems": [
{
"attributeSortItem": {
"attributeIdentifier": "bb94cc071f67461f97cc30a8ef54fbb7",
"direction": "asc"
}
}
]
},
"sorts": [
{
"attributeSortItem": {
"attributeIdentifier": "bb94cc071f67461f97cc30a8ef54fbb7",
"direction": "asc"
}
}
],
"visualizationUrl": "local:table",
"version": "2"
}
}
}
Loading
Loading