From bb75483a8d2fbb8344a04ec98c76ac83d25e72ec Mon Sep 17 00:00:00 2001 From: Auto Implementation Date: Tue, 17 Mar 2026 13:12:09 +0000 Subject: [PATCH 1/2] feat(gooddata-sdk): [AUTO] add CatalogSmartFunctionsService with get_trending_objects for GDAI-1418 Wraps the new GET /api/v1/actions/workspaces/{workspaceId}/ai/analyticsCatalog/trendingObjects endpoint (already present in gooddata-api-client) by adding CatalogTrendingObjectItem and CatalogTrendingObjectsResult model classes, CatalogSmartFunctionsService, and exposing them through the GoodDataSdk facade and public __init__ exports. Co-Authored-By: Claude Sonnet 4.6 --- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 5 + .../catalog/analytics_catalog/__init__.py | 10 ++ .../analytics_catalog/model/__init__.py | 1 + .../model/trending_objects.py | 66 +++++++++++ .../catalog/smart_functions/__init__.py | 6 + .../catalog/smart_functions/service.py | 32 ++++++ .../gooddata-sdk/src/gooddata_sdk/client.py | 5 + packages/gooddata-sdk/src/gooddata_sdk/sdk.py | 6 + .../tests/catalog/smart_functions/__init__.py | 1 + .../smart_functions/test_trending_objects.py | 104 ++++++++++++++++++ 10 files changed, 236 insertions(+) create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/__init__.py create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/__init__.py create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/analytics_catalog/model/trending_objects.py create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/__init__.py create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/smart_functions/service.py create mode 100644 packages/gooddata-sdk/tests/catalog/smart_functions/__init__.py create mode 100644 packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py 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..c76bd0305 --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py @@ -0,0 +1,104 @@ +# (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 From ad2d563e2cab39e9e6fc7ac23453286fad23ca41 Mon Sep 17 00:00:00 2001 From: Auto Implementation Date: Tue, 17 Mar 2026 13:12:34 +0000 Subject: [PATCH 2/2] chore: [AUTO] apply ruff formatting --- .../smart_functions/test_trending_objects.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) 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 index c76bd0305..4112abdf7 100644 --- a/packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py +++ b/packages/gooddata-sdk/tests/catalog/smart_functions/test_trending_objects.py @@ -29,11 +29,25 @@ def _make_mock_item( 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", - ]}) + 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