Skip to content

Commit 2964993

Browse files
feat: add support for FilterView management
Enabling management of FilterViews via pythonSDK JIRA: LX-428 risk: low
1 parent d20db45 commit 2964993

35 files changed

+598
-15
lines changed

gooddata-sdk/gooddata_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
from gooddata_sdk.catalog.identifier import (
7272
CatalogAssigneeIdentifier,
7373
CatalogDatasetWorkspaceDataFilterIdentifier,
74+
CatalogDeclarativeAnalyticalDashboardIdentifier,
7475
CatalogExportDefinitionIdentifier,
7576
CatalogNotificationChannelIdentifier,
7677
CatalogUserIdentifier,
@@ -175,6 +176,7 @@
175176
CatalogDeclarativeModel,
176177
)
177178
from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import (
179+
CatalogDeclarativeFilterView,
178180
CatalogDeclarativeUserDataFilter,
179181
CatalogDeclarativeUserDataFilters,
180182
CatalogDeclarativeWorkspace,

gooddata-sdk/gooddata_sdk/catalog/identifier.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from attrs import define
88
from gooddata_api_client.model.assignee_identifier import AssigneeIdentifier
99
from gooddata_api_client.model.dataset_workspace_data_filter_identifier import DatasetWorkspaceDataFilterIdentifier
10+
from gooddata_api_client.model.declarative_analytical_dashboard_identifier import (
11+
DeclarativeAnalyticalDashboardIdentifier,
12+
)
1013
from gooddata_api_client.model.declarative_export_definition_identifier import DeclarativeExportDefinitionIdentifier
1114
from gooddata_api_client.model.declarative_notification_channel_identifier import (
1215
DeclarativeNotificationChannelIdentifier,
@@ -114,3 +117,13 @@ class CatalogNotificationChannelIdentifier(Base):
114117
@staticmethod
115118
def client_class() -> builtins.type[DeclarativeNotificationChannelIdentifier]:
116119
return DeclarativeNotificationChannelIdentifier
120+
121+
122+
@attr.s(auto_attribs=True, kw_only=True)
123+
class CatalogDeclarativeAnalyticalDashboardIdentifier(Base):
124+
id: str
125+
type: str = attr.field(validator=value_in_allowed)
126+
127+
@staticmethod
128+
def client_class() -> builtins.type[DeclarativeAnalyticalDashboardIdentifier]:
129+
return DeclarativeAnalyticalDashboardIdentifier

gooddata-sdk/gooddata_sdk/catalog/workspace/declarative_model/workspace/workspace.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Optional
77

88
import attr
9+
from gooddata_api_client.model.declarative_filter_view import DeclarativeFilterView
910
from gooddata_api_client.model.declarative_user_data_filter import DeclarativeUserDataFilter
1011
from gooddata_api_client.model.declarative_user_data_filters import DeclarativeUserDataFilters
1112
from gooddata_api_client.model.declarative_workspace import DeclarativeWorkspace
@@ -17,6 +18,7 @@
1718

1819
from gooddata_sdk.catalog.base import Base
1920
from gooddata_sdk.catalog.identifier import (
21+
CatalogDeclarativeAnalyticalDashboardIdentifier,
2022
CatalogDeclarativeUserGroupIdentifier,
2123
CatalogUserIdentifier,
2224
CatalogWorkspaceIdentifier,
@@ -36,6 +38,7 @@
3638
LAYOUT_WORKSPACES_DIR = "workspaces"
3739
LAYOUT_WORKSPACES_DATA_FILTERS_DIR = "workspaces_data_filters"
3840
LAYOUT_USER_DATA_FILTERS_DIR = "user_data_filters"
41+
LAYOUT_FILTER_VIEWS_DIR = "filter_views"
3942

4043

4144
def get_workspace_folder(workspace_id: str, layout_organization_folder: Path) -> Path:
@@ -85,6 +88,7 @@ class CatalogDeclarativeWorkspace(Base):
8588
user_data_filters: list[CatalogDeclarativeUserDataFilter] = attr.field(factory=list)
8689
custom_application_settings: list[CatalogDeclarativeCustomApplicationSetting] = attr.field(factory=list)
8790
automations: list[CatalogDeclarativeAutomation] = attr.field(factory=list)
91+
filter_views: list[CatalogDeclarativeFilterView] = attr.field(factory=list)
8892

8993
@staticmethod
9094
def client_class() -> type[DeclarativeWorkspace]:
@@ -211,12 +215,13 @@ def load_from_disk(cls, workspaces_data_filter_file: Path) -> CatalogDeclarative
211215
@classmethod
212216
def from_dict(cls, data: dict[str, Any], camel_case: bool = True) -> CatalogDeclarativeWorkspaceDataFilter:
213217
"""
214-
:param data: Data loaded for example from the file.
215-
:param camel_case: True if the variable names in the input
216-
data are serialized names as specified in the OpenAPI document.
217-
False if the variables names in the input data are python
218-
variable names in PEP-8 snake case.
219-
:return: CatalogDeclarativeWorkspaceDataFilter object.
218+
Args:
219+
data (dict[str, Any]): Data loaded, for example, from a file.
220+
camel_case (bool): True if the variable names in the input data are serialized names as specified in the OpenAPI document.
221+
False if the variable names in the input data are Python variable names in PEP-8 snake case.
222+
223+
Returns:
224+
CatalogDeclarativeWorkspaceDataFilter: CatalogDeclarativeWorkspaceDataFilter object.
220225
"""
221226
declarative_workspace_data_filter = DeclarativeWorkspaceDataFilter.from_dict(data, camel_case)
222227
return cls.from_api(declarative_workspace_data_filter)
@@ -272,16 +277,73 @@ def load_from_disk(cls, user_data_filter_file: Path) -> CatalogDeclarativeUserDa
272277

273278
@classmethod
274279
def from_dict(cls, data: dict[str, Any], camel_case: bool = True) -> CatalogDeclarativeUserDataFilter:
280+
"""
281+
Args:
282+
data (dict[str, Any]): Data loaded, for example, from a file.
283+
camel_case (bool): True if the variable names in the input data are serialized names as specified in the OpenAPI document.
284+
False if the variable names in the input data are Python variable names in PEP-8 snake case.
285+
286+
Returns:
287+
CatalogDeclarativeUserDataFilter: CatalogDeclarativeUserDataFilter object.
288+
"""
289+
declarative_user_data_filter = DeclarativeUserDataFilter.from_dict(data, camel_case)
290+
return cls.from_api(declarative_user_data_filter)
291+
292+
293+
@attr.s(auto_attribs=True, kw_only=True)
294+
class CatalogDeclarativeFilterView(Base):
295+
id: str
296+
title: str
297+
analytical_dashboard: Optional[CatalogDeclarativeAnalyticalDashboardIdentifier] = None
298+
content: Optional[dict[str, Any]] = None
299+
description: Optional[str] = None
300+
is_default: Optional[bool] = None
301+
tags: Optional[list[str]] = None
302+
user: Optional[CatalogUserIdentifier] = None
303+
304+
@staticmethod
305+
def client_class() -> type[DeclarativeFilterView]:
306+
return DeclarativeFilterView
307+
308+
def store_to_disk(self, filter_views_folder: Path) -> None:
309+
filter_view_file = filter_views_folder / f"{self.id}.yaml"
310+
write_layout_to_file(filter_view_file, self.to_api().to_dict(camel_case=True))
311+
312+
@classmethod
313+
def load_from_disk(cls, filter_view_file: Path) -> CatalogDeclarativeFilterView:
314+
filter_view = read_layout_from_file(filter_view_file)
315+
return CatalogDeclarativeFilterView.from_dict(filter_view, camel_case=True)
316+
317+
@classmethod
318+
def store_filter_views_to_disk(
319+
cls, filter_views: list[CatalogDeclarativeFilterView], layout_organization_folder: Path
320+
) -> None:
321+
filter_views_folder = CatalogDeclarativeWorkspaces.filter_views_folder(layout_organization_folder)
322+
create_directory(filter_views_folder)
323+
for filter_view in filter_views:
324+
filter_view.store_to_disk(filter_views_folder)
325+
326+
@classmethod
327+
def load_filter_views_from_disk(cls, layout_organization_folder: Path) -> list[CatalogDeclarativeFilterView]:
328+
filter_views_files = get_sorted_yaml_files(
329+
CatalogDeclarativeWorkspaces.filter_views_folder(layout_organization_folder)
330+
)
331+
return [
332+
CatalogDeclarativeFilterView.load_from_disk(filter_views_file) for filter_views_file in filter_views_files
333+
]
334+
335+
@classmethod
336+
def from_dict(cls, data: dict[str, Any], camel_case: bool = True) -> CatalogDeclarativeFilterView:
275337
"""
276338
:param data: Data loaded for example from the file.
277339
:param camel_case: True if the variable names in the input
278340
data are serialized names as specified in the OpenAPI document.
279341
False if the variables names in the input data are python
280342
variable names in PEP-8 snake case.
281-
:return: CatalogDeclarativeUserDataFilter object.
343+
:return: CatalogDeclarativeFilterView object.
282344
"""
283-
declarative_user_data_filter = DeclarativeUserDataFilter.from_dict(data, camel_case)
284-
return cls.from_api(declarative_user_data_filter)
345+
declarative_filter_view = DeclarativeFilterView.from_dict(data, camel_case)
346+
return cls.from_api(declarative_filter_view)
285347

286348

287349
@attr.s(auto_attribs=True, kw_only=True)
@@ -305,6 +367,10 @@ def workspace_data_filters_folder(layout_organization_folder: Path) -> Path:
305367
def user_data_filters_folder(layout_organization_folder: Path) -> Path:
306368
return layout_organization_folder / LAYOUT_USER_DATA_FILTERS_DIR
307369

370+
@staticmethod
371+
def filter_views_folder(layout_organization_folder: Path) -> Path:
372+
return layout_organization_folder / LAYOUT_FILTER_VIEWS_DIR
373+
308374
def store_to_disk(self, layout_organization_folder: Path) -> None:
309375
workspaces_folder = self.workspaces_folder(layout_organization_folder)
310376
workspaces_data_filters_folder = self.workspace_data_filters_folder(layout_organization_folder)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# (C) 2024 GoodData Corporation
2+
from __future__ import annotations
3+
4+
from typing import Any, Optional, Union
5+
6+
import attr
7+
from gooddata_api_client.model.json_api_filter_view_in import JsonApiFilterViewIn
8+
from gooddata_api_client.model.json_api_filter_view_in_attributes import JsonApiFilterViewInAttributes
9+
from gooddata_api_client.model.json_api_filter_view_in_document import JsonApiFilterViewInDocument
10+
from gooddata_api_client.model.json_api_filter_view_in_relationships import JsonApiFilterViewInRelationships
11+
12+
from gooddata_sdk.catalog.base import Base
13+
from gooddata_sdk.catalog.identifier import CatalogDeclarativeAnalyticalDashboardIdentifier, CatalogUserIdentifier
14+
15+
16+
@attr.s(auto_attribs=True, kw_only=True)
17+
class CatalogFilterViewDocument(Base):
18+
data: CatalogFilterView
19+
20+
@staticmethod
21+
def client_class() -> type[JsonApiFilterViewInDocument]:
22+
return JsonApiFilterViewInDocument
23+
24+
def to_api(self) -> JsonApiFilterViewInDocument:
25+
return JsonApiFilterViewInDocument(data=self.data.to_api())
26+
27+
28+
def _data_entity(value: Any) -> dict[str, Any]:
29+
return {"data": value}
30+
31+
32+
@attr.s(auto_attribs=True, kw_only=True)
33+
class CatalogFilterView(Base):
34+
id: Optional[str] = None
35+
attributes: CatalogFilterViewAttributes
36+
relationships: Optional[CatalogFilterViewRelationships] = None
37+
38+
@staticmethod
39+
def client_class() -> type[JsonApiFilterViewIn]:
40+
return JsonApiFilterViewIn
41+
42+
@classmethod
43+
def init(
44+
cls,
45+
filter_view_id: str,
46+
content: dict[str, Any],
47+
title: str,
48+
are_relations_valid: Optional[bool] = None,
49+
description: Optional[str] = None,
50+
is_default: Optional[bool] = None,
51+
tags: Optional[list[str]] = None,
52+
user_id: Optional[str] = None,
53+
analytical_dashboard_id: Optional[str] = None,
54+
) -> CatalogFilterView:
55+
attributes = CatalogFilterViewAttributes(
56+
content=content,
57+
title=title,
58+
are_relations_valid=are_relations_valid,
59+
description=description,
60+
is_default=is_default,
61+
tags=tags,
62+
)
63+
relationships = CatalogFilterViewRelationships.create_user_analytical_dashboard_relationship(
64+
user_id=user_id, analytical_dashboard_id=analytical_dashboard_id
65+
)
66+
return cls(id=filter_view_id, attributes=attributes, relationships=relationships)
67+
68+
def to_api(self) -> JsonApiFilterViewIn:
69+
attributes = self.attributes.to_api()
70+
relationships = self.relationships.to_api() if self.relationships is not None else None
71+
return JsonApiFilterViewIn(id=self.id, attributes=attributes, relationships=relationships)
72+
73+
@property
74+
def user_id(self) -> Union[str, None]:
75+
if self.relationships and self.relationships.user:
76+
return self.relationships.user["data"].id
77+
return None
78+
79+
@property
80+
def analytical_dashboard_id(self) -> Union[str, None]:
81+
if self.relationships and self.relationships.analytical_dashboard:
82+
return self.relationships.analytical_dashboard["data"].id
83+
return None
84+
85+
def assign_user(self, user_id: str) -> None:
86+
if self.relationships is None:
87+
self.relationships = CatalogFilterViewRelationships.create_user_analytical_dashboard_relationship(
88+
user_id=user_id
89+
)
90+
else:
91+
self.relationships.user = _data_entity(CatalogUserIdentifier(id=user_id, type="user"))
92+
93+
def assign_analytical_dashboard(self, analytical_dashboard_id: str) -> None:
94+
if self.relationships is None:
95+
self.relationships = CatalogFilterViewRelationships.create_user_analytical_dashboard_relationship(
96+
analytical_dashboard_id=analytical_dashboard_id
97+
)
98+
else:
99+
self.relationships.analytical_dashboard = _data_entity(
100+
CatalogDeclarativeAnalyticalDashboardIdentifier(id=analytical_dashboard_id, type="analyticalDashboard")
101+
)
102+
103+
def clean_relationships(self) -> None:
104+
if self.relationships is not None:
105+
self.relationships.user = None
106+
self.relationships.analytical_dashboard = None
107+
108+
109+
@attr.s(auto_attribs=True, kw_only=True)
110+
class CatalogFilterViewAttributes(Base):
111+
content: dict[str, Any]
112+
title: str
113+
are_relations_valid: Optional[bool] = None
114+
description: Optional[str] = None
115+
is_default: Optional[bool] = None
116+
tags: Optional[list[str]] = None
117+
118+
@staticmethod
119+
def client_class() -> type[JsonApiFilterViewInAttributes]:
120+
return JsonApiFilterViewInAttributes
121+
122+
123+
@attr.s(auto_attribs=True, kw_only=True)
124+
class CatalogFilterViewRelationships(Base):
125+
user: Optional[dict[str, CatalogUserIdentifier]] = None
126+
analytical_dashboard: Optional[dict[str, CatalogDeclarativeAnalyticalDashboardIdentifier]] = None
127+
128+
@staticmethod
129+
def client_class() -> type[JsonApiFilterViewInRelationships]:
130+
return JsonApiFilterViewInRelationships
131+
132+
@classmethod
133+
def create_user_analytical_dashboard_relationship(
134+
cls, user_id: Optional[str] = None, analytical_dashboard_id: Optional[str] = None
135+
) -> CatalogFilterViewRelationships | None:
136+
if user_id is None and analytical_dashboard_id is None:
137+
return None
138+
assignee_user = _data_entity(CatalogUserIdentifier(id=user_id, type="user")) if user_id else None
139+
assignee_analytical_dashboard = (
140+
_data_entity(
141+
CatalogDeclarativeAnalyticalDashboardIdentifier(id=analytical_dashboard_id, type="analyticalDashboard")
142+
)
143+
if analytical_dashboard_id
144+
else None
145+
)
146+
return cls(user=assignee_user, analytical_dashboard=assignee_analytical_dashboard)

gooddata-sdk/gooddata_sdk/catalog/workspace/entity_model/user_data_filter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,13 @@ def to_api(self, post: bool = False) -> Union[JsonApiUserDataFilterPostOptionalI
9191
return JsonApiUserDataFilterIn(id=self.id, attributes=attributes, relationships=relationships)
9292

9393
@property
94-
def user_id(self) -> str | None:
94+
def user_id(self) -> Union[str, None]:
9595
if self.relationships and self.relationships.user:
9696
return self.relationships.user["data"].id
9797
return None
9898

9999
@property
100-
def user_group_id(self) -> str | None:
100+
def user_group_id(self) -> Union[str, None]:
101101
if self.relationships and self.relationships.user_group:
102102
return self.relationships.user_group["data"].id
103103
return None

0 commit comments

Comments
 (0)