diff --git a/gooddata-sdk/gooddata_sdk/utils.py b/gooddata-sdk/gooddata_sdk/utils.py index 17bf59045..d4f010225 100644 --- a/gooddata-sdk/gooddata_sdk/utils.py +++ b/gooddata-sdk/gooddata_sdk/utils.py @@ -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 @@ -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. @@ -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 diff --git a/gooddata-sdk/gooddata_sdk/visualization.py b/gooddata-sdk/gooddata_sdk/visualization.py index 8b46d3ec3..7882846f8 100644 --- a/gooddata-sdk/gooddata_sdk/visualization.py +++ b/gooddata-sdk/gooddata_sdk/visualization.py @@ -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. @@ -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"], @@ -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"] @@ -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") @@ -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] @@ -254,7 +276,7 @@ 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, @@ -262,7 +284,12 @@ def _convert_metric_to_computable(metric: dict[str, Any]) -> Metric: 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, @@ -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, @@ -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__() diff --git a/gooddata-sdk/tests/table/fixtures/vis_objs/vis_with_ui_refs.json b/gooddata-sdk/tests/table/fixtures/vis_objs/vis_with_ui_refs.json new file mode 100644 index 000000000..a65528173 --- /dev/null +++ b/gooddata-sdk/tests/table/fixtures/vis_objs/vis_with_ui_refs.json @@ -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" + } + } +} diff --git a/gooddata-sdk/tests/table/snapshots/vis_with_ui_refs.snapshot.json b/gooddata-sdk/tests/table/snapshots/vis_with_ui_refs.snapshot.json new file mode 100644 index 000000000..f04eac1fc --- /dev/null +++ b/gooddata-sdk/tests/table/snapshots/vis_with_ui_refs.snapshot.json @@ -0,0 +1,83 @@ +{ + "execution": { + "attributes": [ + { + "label": { + "identifier": { + "id": "Agent.Name", + "type": "label" + } + }, + "local_identifier": "bb94cc071f67461f97cc30a8ef54fbb7" + }, + { + "label": { + "identifier": { + "id": "Engaged_Role.Name", + "type": "label" + } + }, + "local_identifier": "e497c4b2398c4f7faaa2212ddb2c5df7" + }, + { + "label": { + "identifier": { + "id": "Engagement.Engagement", + "type": "label" + } + }, + "local_identifier": "0baaee3c223f4fd6a707a98beca780ca" + }, + { + "label": { + "identifier": { + "id": "Question.Name", + "type": "label" + } + }, + "local_identifier": "44bc41d90b99431cac722b0fae53c7b2" + } + ], + "filters": [ + { + "relative_date_filter": { + "dataset": { + "identifier": { + "id": "Start_Time", + "type": "dataset" + } + }, + "_from": -1, + "granularity": "DAY", + "to": -1 + } + } + ], + "measures": [] + }, + "result_spec": { + "dimensions": [ + { + "item_identifiers": [ + "bb94cc071f67461f97cc30a8ef54fbb7", + "e497c4b2398c4f7faaa2212ddb2c5df7", + "0baaee3c223f4fd6a707a98beca780ca", + "44bc41d90b99431cac722b0fae53c7b2" + ], + "local_identifier": "dim_0", + "sorting": [ + { + "attribute": { + "attribute_identifier": "bb94cc071f67461f97cc30a8ef54fbb7", + "sort_type": "DEFAULT" + } + } + ] + }, + { + "item_identifiers": [], + "local_identifier": "dim_1" + } + ] + } +} \ No newline at end of file