Skip to content

Commit

Permalink
Merge pull request #878 from gooddata/cbon-lx-428-saved-filter-views-…
Browse files Browse the repository at this point in the history
…mgmt-in-pythonsdk

feat: FilterView management in pythonSDK
  • Loading branch information
hkad98 authored Nov 21, 2024
2 parents aa6b50c + a70d67b commit eda1f3c
Show file tree
Hide file tree
Showing 36 changed files with 1,005 additions and 18 deletions.
2 changes: 2 additions & 0 deletions gooddata-sdk/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from gooddata_sdk.catalog.identifier import (
CatalogAssigneeIdentifier,
CatalogDatasetWorkspaceDataFilterIdentifier,
CatalogDeclarativeAnalyticalDashboardIdentifier,
CatalogExportDefinitionIdentifier,
CatalogNotificationChannelIdentifier,
CatalogUserIdentifier,
Expand Down Expand Up @@ -175,6 +176,7 @@
CatalogDeclarativeModel,
)
from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import (
CatalogDeclarativeFilterView,
CatalogDeclarativeUserDataFilter,
CatalogDeclarativeUserDataFilters,
CatalogDeclarativeWorkspace,
Expand Down
13 changes: 13 additions & 0 deletions gooddata-sdk/gooddata_sdk/catalog/identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from attrs import define
from gooddata_api_client.model.assignee_identifier import AssigneeIdentifier
from gooddata_api_client.model.dataset_workspace_data_filter_identifier import DatasetWorkspaceDataFilterIdentifier
from gooddata_api_client.model.declarative_analytical_dashboard_identifier import (
DeclarativeAnalyticalDashboardIdentifier,
)
from gooddata_api_client.model.declarative_export_definition_identifier import DeclarativeExportDefinitionIdentifier
from gooddata_api_client.model.declarative_notification_channel_identifier import (
DeclarativeNotificationChannelIdentifier,
Expand Down Expand Up @@ -114,3 +117,13 @@ class CatalogNotificationChannelIdentifier(Base):
@staticmethod
def client_class() -> builtins.type[DeclarativeNotificationChannelIdentifier]:
return DeclarativeNotificationChannelIdentifier


@attr.s(auto_attribs=True, kw_only=True)
class CatalogDeclarativeAnalyticalDashboardIdentifier(Base):
id: str
type: str = attr.field(validator=value_in_allowed)

@staticmethod
def client_class() -> builtins.type[DeclarativeAnalyticalDashboardIdentifier]:
return DeclarativeAnalyticalDashboardIdentifier
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any, Optional

import attr
from gooddata_api_client.model.declarative_filter_view import DeclarativeFilterView
from gooddata_api_client.model.declarative_user_data_filter import DeclarativeUserDataFilter
from gooddata_api_client.model.declarative_user_data_filters import DeclarativeUserDataFilters
from gooddata_api_client.model.declarative_workspace import DeclarativeWorkspace
Expand All @@ -17,6 +18,7 @@

from gooddata_sdk.catalog.base import Base
from gooddata_sdk.catalog.identifier import (
CatalogDeclarativeAnalyticalDashboardIdentifier,
CatalogDeclarativeUserGroupIdentifier,
CatalogUserIdentifier,
CatalogWorkspaceIdentifier,
Expand All @@ -36,6 +38,7 @@
LAYOUT_WORKSPACES_DIR = "workspaces"
LAYOUT_WORKSPACES_DATA_FILTERS_DIR = "workspaces_data_filters"
LAYOUT_USER_DATA_FILTERS_DIR = "user_data_filters"
LAYOUT_FILTER_VIEWS_DIR = "filter_views"


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

@staticmethod
def client_class() -> type[DeclarativeWorkspace]:
Expand Down Expand Up @@ -211,12 +215,13 @@ def load_from_disk(cls, workspaces_data_filter_file: Path) -> CatalogDeclarative
@classmethod
def from_dict(cls, data: dict[str, Any], camel_case: bool = True) -> CatalogDeclarativeWorkspaceDataFilter:
"""
:param data: Data loaded for example from the file.
:param camel_case: True if the variable names in the input
data are serialized names as specified in the OpenAPI document.
False if the variables names in the input data are python
variable names in PEP-8 snake case.
:return: CatalogDeclarativeWorkspaceDataFilter object.
Args:
data (dict[str, Any]): Data loaded, for example, from a file.
camel_case (bool): True if the variable names in the input data are serialized names as specified in the OpenAPI document.
False if the variable names in the input data are Python variable names in PEP-8 snake case.
Returns:
CatalogDeclarativeWorkspaceDataFilter: CatalogDeclarativeWorkspaceDataFilter object.
"""
declarative_workspace_data_filter = DeclarativeWorkspaceDataFilter.from_dict(data, camel_case)
return cls.from_api(declarative_workspace_data_filter)
Expand Down Expand Up @@ -273,17 +278,61 @@ def load_from_disk(cls, user_data_filter_file: Path) -> CatalogDeclarativeUserDa
@classmethod
def from_dict(cls, data: dict[str, Any], camel_case: bool = True) -> CatalogDeclarativeUserDataFilter:
"""
:param data: Data loaded for example from the file.
:param camel_case: True if the variable names in the input
data are serialized names as specified in the OpenAPI document.
False if the variables names in the input data are python
variable names in PEP-8 snake case.
:return: CatalogDeclarativeUserDataFilter object.
Args:
data (dict[str, Any]): Data loaded, for example, from a file.
camel_case (bool): True if the variable names in the input data are serialized names as specified in the OpenAPI document.
False if the variable names in the input data are Python variable names in PEP-8 snake case.
Returns:
CatalogDeclarativeUserDataFilter: CatalogDeclarativeUserDataFilter object.
"""
declarative_user_data_filter = DeclarativeUserDataFilter.from_dict(data, camel_case)
return cls.from_api(declarative_user_data_filter)


@attr.s(auto_attribs=True, kw_only=True)
class CatalogDeclarativeFilterView(Base):
id: str
title: str
analytical_dashboard: Optional[CatalogDeclarativeAnalyticalDashboardIdentifier] = None
content: Optional[dict[str, Any]] = None
description: Optional[str] = None
is_default: Optional[bool] = None
tags: Optional[list[str]] = None
user: Optional[CatalogUserIdentifier] = None

@staticmethod
def client_class() -> type[DeclarativeFilterView]:
return DeclarativeFilterView

def store_to_disk(self, filter_views_folder: Path) -> None:
filter_view_file = filter_views_folder / f"{self.id}.yaml"
write_layout_to_file(filter_view_file, self.to_api().to_dict(camel_case=True))

@classmethod
def load_from_disk(cls, filter_view_file: Path) -> CatalogDeclarativeFilterView:
filter_view = read_layout_from_file(filter_view_file)
return CatalogDeclarativeFilterView.from_dict(filter_view, camel_case=True)

@classmethod
def store_filter_views_to_disk(
cls, filter_views: list[CatalogDeclarativeFilterView], layout_organization_folder: Path
) -> None:
filter_views_folder = CatalogDeclarativeWorkspaces.filter_views_folder(layout_organization_folder)
create_directory(filter_views_folder)
for filter_view in filter_views:
filter_view.store_to_disk(filter_views_folder)

@classmethod
def load_filter_views_from_disk(cls, layout_organization_folder: Path) -> list[CatalogDeclarativeFilterView]:
filter_views_files = get_sorted_yaml_files(
CatalogDeclarativeWorkspaces.filter_views_folder(layout_organization_folder)
)
return [
CatalogDeclarativeFilterView.load_from_disk(filter_views_file) for filter_views_file in filter_views_files
]


@attr.s(auto_attribs=True, kw_only=True)
class CatalogDeclarativeWorkspaces(Base):
workspaces: list[CatalogDeclarativeWorkspace]
Expand All @@ -305,6 +354,10 @@ def workspace_data_filters_folder(layout_organization_folder: Path) -> Path:
def user_data_filters_folder(layout_organization_folder: Path) -> Path:
return layout_organization_folder / LAYOUT_USER_DATA_FILTERS_DIR

@staticmethod
def filter_views_folder(layout_organization_folder: Path) -> Path:
return layout_organization_folder / LAYOUT_FILTER_VIEWS_DIR

def store_to_disk(self, layout_organization_folder: Path) -> None:
workspaces_folder = self.workspaces_folder(layout_organization_folder)
workspaces_data_filters_folder = self.workspace_data_filters_folder(layout_organization_folder)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# (C) 2024 GoodData Corporation
from __future__ import annotations

from typing import Any, Optional, Union

import attr
from gooddata_api_client.model.json_api_filter_view_in import JsonApiFilterViewIn
from gooddata_api_client.model.json_api_filter_view_in_attributes import JsonApiFilterViewInAttributes
from gooddata_api_client.model.json_api_filter_view_in_document import JsonApiFilterViewInDocument
from gooddata_api_client.model.json_api_filter_view_in_relationships import JsonApiFilterViewInRelationships

from gooddata_sdk.catalog.base import Base
from gooddata_sdk.catalog.identifier import CatalogDeclarativeAnalyticalDashboardIdentifier, CatalogUserIdentifier


@attr.s(auto_attribs=True, kw_only=True)
class CatalogFilterViewDocument(Base):
data: CatalogFilterView

@staticmethod
def client_class() -> type[JsonApiFilterViewInDocument]:
return JsonApiFilterViewInDocument

def to_api(self) -> JsonApiFilterViewInDocument:
return JsonApiFilterViewInDocument(data=self.data.to_api())


def _data_entity(value: Any) -> dict[str, Any]:
return {"data": value}


@attr.s(auto_attribs=True, kw_only=True)
class CatalogFilterView(Base):
id: Optional[str] = None
attributes: CatalogFilterViewAttributes
relationships: Optional[CatalogFilterViewRelationships] = None

@staticmethod
def client_class() -> type[JsonApiFilterViewIn]:
return JsonApiFilterViewIn

@classmethod
def init(
cls,
filter_view_id: str,
content: dict[str, Any],
title: str,
are_relations_valid: Optional[bool] = None,
description: Optional[str] = None,
is_default: Optional[bool] = None,
tags: Optional[list[str]] = None,
user_id: Optional[str] = None,
analytical_dashboard_id: Optional[str] = None,
) -> CatalogFilterView:
attributes = CatalogFilterViewAttributes(
content=content,
title=title,
are_relations_valid=are_relations_valid,
description=description,
is_default=is_default,
tags=tags,
)
relationships = CatalogFilterViewRelationships.create_user_analytical_dashboard_relationship(
user_id=user_id, analytical_dashboard_id=analytical_dashboard_id
)
return cls(id=filter_view_id, attributes=attributes, relationships=relationships)

def to_api(self) -> JsonApiFilterViewIn:
attributes = self.attributes.to_api()
relationships = self.relationships.to_api() if self.relationships is not None else None
return JsonApiFilterViewIn(id=self.id, attributes=attributes, relationships=relationships)

@property
def user_id(self) -> Union[str, None]:
if self.relationships and self.relationships.user:
return self.relationships.user["data"].id
return None

@property
def analytical_dashboard_id(self) -> Union[str, None]:
if self.relationships and self.relationships.analytical_dashboard:
return self.relationships.analytical_dashboard["data"].id
return None

def assign_user(self, user_id: str) -> None:
if self.relationships is None:
self.relationships = CatalogFilterViewRelationships.create_user_analytical_dashboard_relationship(
user_id=user_id
)
else:
self.relationships.user = _data_entity(CatalogUserIdentifier(id=user_id, type="user"))

def assign_analytical_dashboard(self, analytical_dashboard_id: str) -> None:
if self.relationships is None:
self.relationships = CatalogFilterViewRelationships.create_user_analytical_dashboard_relationship(
analytical_dashboard_id=analytical_dashboard_id
)
else:
self.relationships.analytical_dashboard = _data_entity(
CatalogDeclarativeAnalyticalDashboardIdentifier(id=analytical_dashboard_id, type="analyticalDashboard")
)

def clean_relationships(self) -> None:
if self.relationships is not None:
self.relationships.user = None
self.relationships.analytical_dashboard = None


@attr.s(auto_attribs=True, kw_only=True)
class CatalogFilterViewAttributes(Base):
content: dict[str, Any]
title: str
are_relations_valid: Optional[bool] = None
description: Optional[str] = None
is_default: Optional[bool] = None
tags: Optional[list[str]] = None

@staticmethod
def client_class() -> type[JsonApiFilterViewInAttributes]:
return JsonApiFilterViewInAttributes


@attr.s(auto_attribs=True, kw_only=True)
class CatalogFilterViewRelationships(Base):
user: Optional[dict[str, CatalogUserIdentifier]] = None
analytical_dashboard: Optional[dict[str, CatalogDeclarativeAnalyticalDashboardIdentifier]] = None

@staticmethod
def client_class() -> type[JsonApiFilterViewInRelationships]:
return JsonApiFilterViewInRelationships

@classmethod
def create_user_analytical_dashboard_relationship(
cls, user_id: Optional[str] = None, analytical_dashboard_id: Optional[str] = None
) -> CatalogFilterViewRelationships | None:
if user_id is None and analytical_dashboard_id is None:
return None
assignee_user = _data_entity(CatalogUserIdentifier(id=user_id, type="user")) if user_id else None
assignee_analytical_dashboard = (
_data_entity(
CatalogDeclarativeAnalyticalDashboardIdentifier(id=analytical_dashboard_id, type="analyticalDashboard")
)
if analytical_dashboard_id
else None
)
return cls(user=assignee_user, analytical_dashboard=assignee_analytical_dashboard)
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ def to_api(self, post: bool = False) -> Union[JsonApiUserDataFilterPostOptionalI
return JsonApiUserDataFilterIn(id=self.id, attributes=attributes, relationships=relationships)

@property
def user_id(self) -> str | None:
def user_id(self) -> Union[str, None]:
if self.relationships and self.relationships.user:
return self.relationships.user["data"].id
return None

@property
def user_group_id(self) -> str | None:
def user_group_id(self) -> Union[str, None]:
if self.relationships and self.relationships.user_group:
return self.relationships.user_group["data"].id
return None
Expand Down
Loading

0 comments on commit eda1f3c

Please sign in to comment.