diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index fba74d7f0..92b56f76c 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -7,6 +7,10 @@ import logging from gooddata_sdk._version import __version__ +from gooddata_sdk.catalog.analytics_catalog.model.trending_objects import ( + CatalogTrendingObjectItem, + CatalogTrendingObjectsResult, +) from gooddata_sdk.catalog.appearance.entity_model.color_palette import ( CatalogColorPalette, CatalogColorPaletteAttributes, @@ -16,6 +20,7 @@ CatalogThemeAttributes, ) from gooddata_sdk.catalog.appearance.service import CatalogAppearanceService +from gooddata_sdk.catalog.smart_functions.service import CatalogSmartFunctionsService from gooddata_sdk.catalog.data_source.action_model.requests.ldm_request import ( CatalogGenerateLdmRequest, CatalogPdmLdmRequest, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/__init__.py new file mode 100644 index 000000000..ab7ad768f --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/__init__.py @@ -0,0 +1,10 @@ +# (C) 2024 GoodData Corporation +from gooddata_sdk.catalog.analytics_catalog.model.trending_objects import ( + CatalogTrendingObjectItem, + CatalogTrendingObjectsResult, +) + +__all__ = [ + "CatalogTrendingObjectItem", + "CatalogTrendingObjectsResult", +] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/__init__.py new file mode 100644 index 000000000..06549c73b --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/__init__.py @@ -0,0 +1 @@ +# (C) 2024 GoodData Corporation diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/trending_objects.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/trending_objects.py new file mode 100644 index 000000000..8b615d405 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/trending_objects.py @@ -0,0 +1,66 @@ +# (C) 2024 GoodData Corporation +from __future__ import annotations + +from datetime import datetime + +import attrs + + +@attrs.define(kw_only=True) +class CatalogTrendingObjectItem: + """Represents a trending object in the Analytics Catalog.""" + + id: str + tags: list[str] + title: str + type: str + usage_count: int + workspace_id: str + created_at: datetime | None = None + created_by: str | None = None + dataset_id: str | None = None + dataset_title: str | None = None + dataset_type: str | None = None + description: str | None = None + is_hidden: bool | None = None + is_hidden_from_kda: bool | None = None + metric_type: str | None = None + modified_at: datetime | None = None + modified_by: str | None = None + visualization_url: str | None = None + + @classmethod + def from_api_model(cls, api_model: object) -> CatalogTrendingObjectItem: + return cls( + id=api_model.id, + tags=list(api_model.tags), + title=api_model.title, + type=api_model.type, + usage_count=api_model.usage_count, + workspace_id=api_model.workspace_id, + created_at=getattr(api_model, "created_at", None), + created_by=getattr(api_model, "created_by", None), + dataset_id=getattr(api_model, "dataset_id", None), + dataset_title=getattr(api_model, "dataset_title", None), + dataset_type=getattr(api_model, "dataset_type", None), + description=getattr(api_model, "description", None), + is_hidden=getattr(api_model, "is_hidden", None), + is_hidden_from_kda=getattr(api_model, "is_hidden_from_kda", None), + metric_type=getattr(api_model, "metric_type", None), + modified_at=getattr(api_model, "modified_at", None), + modified_by=getattr(api_model, "modified_by", None), + visualization_url=getattr(api_model, "visualization_url", None), + ) + + +@attrs.define(kw_only=True) +class CatalogTrendingObjectsResult: + """Represents the result of a trending objects query.""" + + objects: list[CatalogTrendingObjectItem] = attrs.field(factory=list) + + @classmethod + def from_api_model(cls, api_model: object) -> CatalogTrendingObjectsResult: + return cls( + objects=[CatalogTrendingObjectItem.from_api_model(item) for item in api_model.objects], + ) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/__init__.py new file mode 100644 index 000000000..fe4a1bead --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/__init__.py @@ -0,0 +1,6 @@ +# (C) 2024 GoodData Corporation +from gooddata_sdk.catalog.smart_functions.service import CatalogSmartFunctionsService + +__all__ = [ + "CatalogSmartFunctionsService", +] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/service.py new file mode 100644 index 000000000..e2f84b84d --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/service.py @@ -0,0 +1,32 @@ +# (C) 2024 GoodData Corporation +from __future__ import annotations + +from gooddata_api_client import apis + +from gooddata_sdk.catalog.analytics_catalog.model.trending_objects import ( + CatalogTrendingObjectsResult, +) +from gooddata_sdk.client import GoodDataApiClient + + +class CatalogSmartFunctionsService: + """Service for accessing AI smart functions in the Analytics Catalog.""" + + def __init__(self, api_client: GoodDataApiClient) -> None: + self._client = api_client + self._smart_functions_api: apis.SmartFunctionsApi = api_client.smart_functions_api + + def get_trending_objects(self, workspace_id: str) -> CatalogTrendingObjectsResult: + """Return trending objects for the given workspace. + + Args: + workspace_id: Workspace identifier. + + Returns: + CatalogTrendingObjectsResult containing a list of trending objects. + """ + result = self._smart_functions_api.trending_objects( + workspace_id=workspace_id, + _check_return_type=False, + ) + return CatalogTrendingObjectsResult.from_api_model(result) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/client.py b/packages/gooddata-sdk/src/gooddata_sdk/client.py index 80ff83925..52400df88 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/client.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/client.py @@ -71,6 +71,7 @@ def __init__( self._actions_api = apis.ActionsApi(self._api_client) self._user_management_api = apis.UserManagementApi(self._api_client) self._appearance_api = apis.AppearanceApi(self._api_client) + self._smart_functions_api = apis.SmartFunctionsApi(self._api_client) self._executions_cancellable = executions_cancellable def _do_post_request( @@ -158,6 +159,10 @@ def user_management_api(self) -> apis.UserManagementApi: def appearance_api(self) -> apis.AppearanceApi: return self._appearance_api + @property + def smart_functions_api(self) -> apis.SmartFunctionsApi: + return self._smart_functions_api + @property def executions_cancellable(self) -> bool: return self._executions_cancellable diff --git a/packages/gooddata-sdk/src/gooddata_sdk/sdk.py b/packages/gooddata-sdk/src/gooddata_sdk/sdk.py index 003840083..9f51b39a5 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/sdk.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/sdk.py @@ -4,6 +4,7 @@ from pathlib import Path from gooddata_sdk.catalog.appearance.service import CatalogAppearanceService +from gooddata_sdk.catalog.smart_functions.service import CatalogSmartFunctionsService from gooddata_sdk.catalog.data_source.service import CatalogDataSourceService from gooddata_sdk.catalog.export.service import ExportService from gooddata_sdk.catalog.organization.service import CatalogOrganizationService @@ -89,6 +90,7 @@ def __init__(self, client: GoodDataApiClient) -> None: self._support = SupportService(self._client) self._catalog_permission = CatalogPermissionService(self._client) self._export = ExportService(self._client) + self._catalog_smart_functions = CatalogSmartFunctionsService(self._client) @property def catalog_appearance(self) -> CatalogAppearanceService: @@ -138,6 +140,10 @@ def catalog_permission(self) -> CatalogPermissionService: def export(self) -> ExportService: return self._export + @property + def catalog_smart_functions(self) -> CatalogSmartFunctionsService: + return self._catalog_smart_functions + @property def client(self) -> GoodDataApiClient: return self._client diff --git a/packages/gooddata-sdk/tests/catalog/smart_functions/__init__.py b/packages/gooddata-sdk/tests/catalog/smart_functions/__init__.py new file mode 100644 index 000000000..06549c73b --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/smart_functions/__init__.py @@ -0,0 +1 @@ +# (C) 2024 GoodData Corporation diff --git a/packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py b/packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py new file mode 100644 index 000000000..4112abdf7 --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py @@ -0,0 +1,118 @@ +# (C) 2024 GoodData Corporation +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from gooddata_sdk import CatalogTrendingObjectItem, CatalogTrendingObjectsResult, GoodDataSdk +from tests_support.vcrpy_utils import get_vcr + +gd_vcr = get_vcr() + +_fixtures_dir = Path(__file__).parent / "fixtures" + + +def _make_mock_item( + id: str = "obj-1", + title: str = "Revenue Dashboard", + type: str = "dashboard", + usage_count: int = 42, + workspace_id: str = "ws-1", + tags: list | None = None, +) -> MagicMock: + item = MagicMock() + item.id = id + item.title = title + item.type = type + item.usage_count = usage_count + item.workspace_id = workspace_id + item.tags = tags or [] + # optional fields not set + item.configure_mock( + **{ + attr: None + for attr in [ + "created_at", + "created_by", + "dataset_id", + "dataset_title", + "dataset_type", + "description", + "is_hidden", + "is_hidden_from_kda", + "metric_type", + "modified_at", + "modified_by", + "visualization_url", + ] + } + ) + return item + + +def test_catalog_trending_object_item_from_api_model(): + """Unit test: CatalogTrendingObjectItem.from_api_model converts required fields correctly.""" + mock_item = _make_mock_item( + id="dash-1", + title="My Dashboard", + type="dashboard", + usage_count=10, + workspace_id="workspace-123", + tags=["finance", "monthly"], + ) + + result = CatalogTrendingObjectItem.from_api_model(mock_item) + + assert result.id == "dash-1" + assert result.title == "My Dashboard" + assert result.type == "dashboard" + assert result.usage_count == 10 + assert result.workspace_id == "workspace-123" + assert result.tags == ["finance", "monthly"] + assert result.created_at is None + assert result.description is None + + +def test_catalog_trending_objects_result_from_api_model(): + """Unit test: CatalogTrendingObjectsResult.from_api_model converts a list of items correctly.""" + item1 = _make_mock_item(id="obj-1", title="Dashboard A", type="dashboard", usage_count=5, workspace_id="ws-1") + item2 = _make_mock_item(id="obj-2", title="Metric B", type="metric", usage_count=3, workspace_id="ws-1") + + mock_result = MagicMock() + mock_result.objects = [item1, item2] + + result = CatalogTrendingObjectsResult.from_api_model(mock_result) + + assert len(result.objects) == 2 + assert result.objects[0].id == "obj-1" + assert result.objects[1].id == "obj-2" + assert result.objects[0].type == "dashboard" + assert result.objects[1].type == "metric" + + +def test_catalog_trending_objects_result_empty(): + """Unit test: CatalogTrendingObjectsResult.from_api_model handles empty objects list.""" + mock_result = MagicMock() + mock_result.objects = [] + + result = CatalogTrendingObjectsResult.from_api_model(mock_result) + + assert result.objects == [] + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_get_trending_objects.yaml")) +def test_get_trending_objects(test_config): + """Integration test: get_trending_objects returns CatalogTrendingObjectsResult.""" + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + workspace_id = test_config["workspace"] + + result = sdk.catalog_smart_functions.get_trending_objects(workspace_id=workspace_id) + + assert isinstance(result, CatalogTrendingObjectsResult) + assert isinstance(result.objects, list) + for item in result.objects: + assert isinstance(item, CatalogTrendingObjectItem) + assert item.id + assert item.workspace_id + assert item.usage_count >= 0