Skip to content

Commit a0a4442

Browse files
committed
fix(sdk): support ui identifier ref conversion
Conversion logic adjusted to follow ui-sdk counterpart. Hence somewhat chaotic usage of defaultType values in some cases. risk: low JIRA: CQ-1080
1 parent b1e24a7 commit a0a4442

File tree

4 files changed

+306
-17
lines changed

4 files changed

+306
-17
lines changed

gooddata-sdk/gooddata_sdk/utils.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from enum import Enum, auto
1010
from pathlib import Path
1111
from shutil import rmtree
12-
from typing import Any, Callable, NamedTuple, Union, cast, no_type_check
12+
from typing import Any, Callable, NamedTuple, Optional, Union, cast, no_type_check
1313
from warnings import warn
1414
from xml.etree import ElementTree as ET
1515

@@ -43,6 +43,25 @@ class HttpMethod(Enum):
4343
PATCH = auto()
4444

4545

46+
class ObjRefType(Enum):
47+
"""Enum representing valid tiger object reference types."""
48+
49+
ATTRIBUTE = "attribute"
50+
METRIC = "metric"
51+
LABEL = "label"
52+
DATASET = "dataset"
53+
FACT = "fact"
54+
55+
56+
UI_TO_TIGER_REF_TYPE = {
57+
"attribute": ObjRefType.ATTRIBUTE,
58+
"measure": ObjRefType.METRIC,
59+
"displayForm": ObjRefType.LABEL,
60+
"dataSet": ObjRefType.DATASET,
61+
"fact": ObjRefType.FACT,
62+
}
63+
64+
4665
def id_obj_to_key(id_obj: IdObjType) -> str:
4766
"""
4867
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:
396415
return json.loads(f.read())
397416

398417

399-
def ref_extract_obj_id(ref: dict[str, Any]) -> ObjId:
418+
def ref_extract_obj_id(ref: dict[str, Any], default_type: Optional[ObjRefType] = None) -> ObjId:
400419
"""
401420
Extracts ObjId from a ref dictionary.
421+
422+
The ref dictionary will most likely conform to one of two forms:
423+
- ui-sdk -> ref: { identifier: str, type: str }
424+
- tiger -> ref: { identifier: { id: str, type: str } }
425+
402426
:param ref: the ref to extract from
427+
:param default_type: the type of the object to fall back to in case of string identifier
403428
:return: the extracted ObjId
404429
:raises ValueError: if the ref is not an identifier
405430
"""
406-
if "identifier" in ref:
407-
return ObjId(id=ref["identifier"]["id"], type=ref["identifier"]["type"])
431+
identifier = ref.get("identifier")
432+
if not identifier:
433+
raise ValueError("invalid ref. must be identifier")
434+
435+
if isinstance(identifier, str):
436+
if default_type:
437+
return ObjId(id=identifier, type=default_type.value)
438+
439+
ui_type = ref.get("type")
440+
if not ui_type or ui_type not in UI_TO_TIGER_REF_TYPE:
441+
raise ValueError("UI objRef type is not recognized and fallback type is not provided")
442+
443+
converted_type = UI_TO_TIGER_REF_TYPE[ui_type].value
444+
return ObjId(id=identifier, type=converted_type)
408445

409-
raise ValueError("invalid ref. must be identifier")
446+
return ObjId(id=identifier["id"], type=identifier["type"])
410447

411448

412-
def ref_extract(ref: dict[str, Any]) -> Union[str, ObjId]:
449+
def ref_extract(ref: dict[str, Any], default_type: Optional[ObjRefType] = None) -> Union[str, ObjId]:
413450
"""
414451
Extracts an object id from a ref dictionary: either an identifier or a localIdentifier.
415452
:param ref: the ref to extract from
453+
:param default_type: ref type to use in case of ui-sdk form of identifier object
416454
:return: thr extracted object id
417455
:raises ValueError: if the ref is not an identifier or localIdentifier
418456
"""
419457
try:
420-
return ref_extract_obj_id(ref)
458+
return ref_extract_obj_id(ref, default_type)
421459
except ValueError:
422460
pass
423461

gooddata-sdk/gooddata_sdk/visualization.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@
2929
PopDatesetMetric,
3030
SimpleMetric,
3131
)
32-
from gooddata_sdk.utils import IdObjType, SideLoads, load_all_entities, ref_extract, ref_extract_obj_id, safeget
32+
from gooddata_sdk.utils import (
33+
IdObjType,
34+
ObjRefType,
35+
SideLoads,
36+
load_all_entities,
37+
ref_extract,
38+
ref_extract_obj_id,
39+
safeget,
40+
)
3341

3442
#
3543
# 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:
166174
# fallback to use URIs; SDK may be able to create filter with attr elements as uris...
167175
in_values = f["in"]["values"] if "values" in f["in"] else f["in"]["uris"]
168176

169-
return PositiveAttributeFilter(label=ref_extract(f["displayForm"]), values=in_values)
177+
return PositiveAttributeFilter(
178+
label=ref_extract(f["displayForm"], ObjRefType.LABEL),
179+
values=in_values,
180+
)
170181

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

176-
return NegativeAttributeFilter(label=ref_extract(f["displayForm"]), values=not_in_values)
187+
return NegativeAttributeFilter(
188+
label=ref_extract(f["displayForm"], ObjRefType.LABEL),
189+
values=not_in_values,
190+
)
191+
177192
elif "relativeDateFilter" in filter_obj:
178193
f = filter_obj["relativeDateFilter"]
179194

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

184199
return RelativeDateFilter(
185-
dataset=ref_extract_obj_id(f["dataSet"]),
200+
dataset=ref_extract_obj_id(f["dataSet"], ObjRefType.DATASET),
186201
granularity=_GRANULARITY_CONVERSION[f["granularity"]],
187202
from_shift=f["from"],
188203
to_shift=f["to"],
@@ -191,7 +206,12 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
191206
elif "absoluteDateFilter" in filter_obj:
192207
f = filter_obj["absoluteDateFilter"]
193208

194-
return AbsoluteDateFilter(dataset=ref_extract_obj_id(f["dataSet"]), from_date=f["from"], to_date=f["to"])
209+
return AbsoluteDateFilter(
210+
dataset=ref_extract_obj_id(f["dataSet"], ObjRefType.DATASET),
211+
from_date=f["from"],
212+
to_date=f["to"],
213+
)
214+
195215
elif "measureValueFilter" in filter_obj:
196216
f = filter_obj["measureValueFilter"]
197217

@@ -211,6 +231,7 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
211231
values=c["value"],
212232
treat_nulls_as=treat_values_as_null,
213233
)
234+
214235
elif "range" in condition:
215236
c = condition["range"]
216237
treat_values_as_null = c.get("treatNullValuesAs")
@@ -220,6 +241,7 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter:
220241
values=(c["from"], c["to"]),
221242
treat_nulls_as=treat_values_as_null,
222243
)
244+
223245
elif "rankingFilter" in filter_obj:
224246
f = filter_obj["rankingFilter"]
225247
# mypy is unable to automatically convert Union[str, ObjId] to Union[str, ObjId, Attribute, Metric]
@@ -254,15 +276,20 @@ def _convert_metric_to_computable(metric: dict[str, Any]) -> Metric:
254276

255277
return SimpleMetric(
256278
local_id=local_id,
257-
item=ref_extract_obj_id(d["item"]),
279+
item=ref_extract_obj_id(d["item"], ObjRefType.FACT),
258280
aggregation=aggregation,
259281
compute_ratio=compute_ratio,
260282
filters=filters,
261283
)
262284

263285
elif "popMeasureDefinition" in measure_def:
264286
d = measure_def["popMeasureDefinition"]
265-
date_attributes = [PopDate(attribute=ref_extract_obj_id(d["popAttribute"]), periods_ago=1)]
287+
date_attributes = [
288+
PopDate(
289+
attribute=ref_extract_obj_id(d["popAttribute"], ObjRefType.ATTRIBUTE),
290+
periods_ago=1,
291+
),
292+
]
266293

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

276-
date_datasets = [PopDateDataset(ref_extract(dd["dataSet"]), dd["periodsAgo"]) for dd in d["dateDataSets"]]
303+
date_datasets = [
304+
PopDateDataset(ref_extract(dd["dataSet"], ObjRefType.DATASET), dd["periodsAgo"]) for dd in d["dateDataSets"]
305+
]
277306

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

396425
def as_computable(self) -> Attribute:
397-
return Attribute(local_id=self.local_id, label=ref_extract(self.label), show_all_values=self.show_all_values)
426+
return Attribute(
427+
local_id=self.local_id,
428+
label=ref_extract(self.label, ObjRefType.LABEL),
429+
show_all_values=self.show_all_values,
430+
)
398431

399432
def __str__(self) -> str:
400433
return self.__repr__()
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
{
2+
"id": "923fb97a-4eb8-4cd7-b564-e72361addc79",
3+
"type": "visualizationObject",
4+
"attributes": {
5+
"title": "testik",
6+
"description": "",
7+
"areRelationsValid": true,
8+
"content": {
9+
"buckets": [
10+
{
11+
"items": [
12+
{
13+
"attribute": {
14+
"localIdentifier": "bb94cc071f67461f97cc30a8ef54fbb7",
15+
"displayForm": {
16+
"identifier": "Agent.Name",
17+
"type": "displayForm"
18+
}
19+
}
20+
},
21+
{
22+
"attribute": {
23+
"localIdentifier": "e497c4b2398c4f7faaa2212ddb2c5df7",
24+
"displayForm": {
25+
"identifier": "Engaged_Role.Name",
26+
"type": "displayForm"
27+
}
28+
}
29+
},
30+
{
31+
"attribute": {
32+
"localIdentifier": "0baaee3c223f4fd6a707a98beca780ca",
33+
"displayForm": {
34+
"identifier": "Engagement.Engagement",
35+
"type": "displayForm"
36+
}
37+
}
38+
},
39+
{
40+
"attribute": {
41+
"localIdentifier": "44bc41d90b99431cac722b0fae53c7b2",
42+
"displayForm": {
43+
"identifier": "Question.Name",
44+
"type": "displayForm"
45+
}
46+
}
47+
}
48+
],
49+
"localIdentifier": "attribute"
50+
}
51+
],
52+
"filters": [
53+
{
54+
"relativeDateFilter": {
55+
"dataSet": {
56+
"identifier": "Start_Time",
57+
"type": "dataSet"
58+
},
59+
"granularity": "GDC.time.date",
60+
"from": -1,
61+
"to": -1
62+
}
63+
},
64+
{
65+
"negativeAttributeFilter": {
66+
"localIdentifier": "81d4b8d638444fe1a08605e6acff289e",
67+
"displayForm": {
68+
"identifier": "Question.Question",
69+
"type": "displayForm"
70+
},
71+
"notIn": {
72+
"uris": []
73+
}
74+
}
75+
},
76+
{
77+
"negativeAttributeFilter": {
78+
"localIdentifier": "71156c3bcc7846668c3bb3ca2e09945c",
79+
"displayForm": {
80+
"identifier": "Engagement_Type.Type",
81+
"type": "displayForm"
82+
},
83+
"notIn": {
84+
"uris": []
85+
}
86+
}
87+
},
88+
{
89+
"negativeAttributeFilter": {
90+
"localIdentifier": "d4df08d678b645258473a455a5de560b",
91+
"displayForm": {
92+
"identifier": "Review_Type.Type",
93+
"type": "displayForm"
94+
},
95+
"notIn": {
96+
"uris": []
97+
}
98+
}
99+
},
100+
{
101+
"negativeAttributeFilter": {
102+
"localIdentifier": "64c8e3f20f22427c9b0e30a78f522019",
103+
"displayForm": {
104+
"identifier": "Agent.Agent",
105+
"type": "displayForm"
106+
},
107+
"notIn": {
108+
"uris": []
109+
}
110+
}
111+
}
112+
],
113+
"properties": {
114+
"sortItems": [
115+
{
116+
"attributeSortItem": {
117+
"attributeIdentifier": "bb94cc071f67461f97cc30a8ef54fbb7",
118+
"direction": "asc"
119+
}
120+
}
121+
]
122+
},
123+
"sorts": [
124+
{
125+
"attributeSortItem": {
126+
"attributeIdentifier": "bb94cc071f67461f97cc30a8ef54fbb7",
127+
"direction": "asc"
128+
}
129+
}
130+
],
131+
"visualizationUrl": "local:table",
132+
"version": "2"
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)