From 27f5750f67619f2a0b6355d2f18c8b02a142dc09 Mon Sep 17 00:00:00 2001 From: Auto Implementer Date: Tue, 17 Mar 2026 13:01:08 +0000 Subject: [PATCH 1/2] feat(gooddata-sdk): [AUTO] add knowledge document list/patch SDK wrapper Adds CatalogKnowledgeDocumentMetadata, CatalogListKnowledgeDocumentsResponse, and CatalogPatchKnowledgeDocumentRequest model classes under catalog/knowledge/, plus list_knowledge_documents() and patch_knowledge_document() service methods on ComputeService to expose the new API client endpoints (LX-2095, LX-2103). Co-Authored-By: Claude Sonnet 4.6 --- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 5 + .../catalog/knowledge/__init__.py | 12 ++ .../catalog/knowledge/model/__init__.py | 12 ++ .../knowledge/model/knowledge_document.py | 69 ++++++++ .../src/gooddata_sdk/compute/service.py | 56 +++++++ .../tests/catalog/knowledge/__init__.py | 1 + .../knowledge/test_knowledge_documents.py | 156 ++++++++++++++++++ 7 files changed, 311 insertions(+) create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/__init__.py create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/knowledge_document.py create mode 100644 packages/gooddata-sdk/tests/catalog/knowledge/__init__.py create mode 100644 packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index fba74d7f0..b27239f77 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -7,6 +7,11 @@ import logging from gooddata_sdk._version import __version__ +from gooddata_sdk.catalog.knowledge.model.knowledge_document import ( + CatalogKnowledgeDocumentMetadata, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, +) from gooddata_sdk.catalog.appearance.entity_model.color_palette import ( CatalogColorPalette, CatalogColorPaletteAttributes, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py new file mode 100644 index 000000000..53ea01faa --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/__init__.py @@ -0,0 +1,12 @@ +# (C) 2024 GoodData Corporation +from gooddata_sdk.catalog.knowledge.model.knowledge_document import ( + CatalogKnowledgeDocumentMetadata, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, +) + +__all__ = [ + "CatalogKnowledgeDocumentMetadata", + "CatalogListKnowledgeDocumentsResponse", + "CatalogPatchKnowledgeDocumentRequest", +] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/__init__.py new file mode 100644 index 000000000..53ea01faa --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/__init__.py @@ -0,0 +1,12 @@ +# (C) 2024 GoodData Corporation +from gooddata_sdk.catalog.knowledge.model.knowledge_document import ( + CatalogKnowledgeDocumentMetadata, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, +) + +__all__ = [ + "CatalogKnowledgeDocumentMetadata", + "CatalogListKnowledgeDocumentsResponse", + "CatalogPatchKnowledgeDocumentRequest", +] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/knowledge_document.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/knowledge_document.py new file mode 100644 index 000000000..4c3c5057d --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/knowledge/model/knowledge_document.py @@ -0,0 +1,69 @@ +# (C) 2024 GoodData Corporation +from __future__ import annotations + +from typing import Any + +import attrs +from gooddata_api_client.model.knowledge_document_metadata_dto import KnowledgeDocumentMetadataDto +from gooddata_api_client.model.list_knowledge_documents_response_dto import ListKnowledgeDocumentsResponseDto +from gooddata_api_client.model.patch_knowledge_document_request_dto import PatchKnowledgeDocumentRequestDto + + +@attrs.define(kw_only=True) +class CatalogKnowledgeDocumentMetadata: + filename: str + title: str | None = None + is_disabled: bool | None = None + scopes: list[str] | None = None + + @classmethod + def from_api(cls, dto: KnowledgeDocumentMetadataDto) -> CatalogKnowledgeDocumentMetadata: + return cls( + filename=dto["filename"], + title=dto.get("title"), + is_disabled=dto.get("is_disabled"), + scopes=list(dto["scopes"]) if "scopes" in dto else None, + ) + + def as_api_model(self) -> PatchKnowledgeDocumentRequestDto: + kwargs: dict[str, Any] = {} + if self.title is not None: + kwargs["title"] = self.title + if self.is_disabled is not None: + kwargs["is_disabled"] = self.is_disabled + if self.scopes is not None: + kwargs["scopes"] = self.scopes + return PatchKnowledgeDocumentRequestDto(_check_type=False, **kwargs) + + +@attrs.define(kw_only=True) +class CatalogListKnowledgeDocumentsResponse: + documents: list[CatalogKnowledgeDocumentMetadata] + total_count: int | None = None + next_page_token: str | None = None + + @classmethod + def from_api(cls, dto: ListKnowledgeDocumentsResponseDto) -> CatalogListKnowledgeDocumentsResponse: + documents = [CatalogKnowledgeDocumentMetadata.from_api(d) for d in dto["documents"]] + return cls( + documents=documents, + total_count=dto.get("total_count"), + next_page_token=dto.get("next_page_token"), + ) + + +@attrs.define(kw_only=True) +class CatalogPatchKnowledgeDocumentRequest: + is_disabled: bool | None = None + title: str | None = None + scopes: list[str] | None = None + + def as_api_model(self) -> PatchKnowledgeDocumentRequestDto: + kwargs: dict[str, Any] = {} + if self.is_disabled is not None: + kwargs["is_disabled"] = self.is_disabled + if self.title is not None: + kwargs["title"] = self.title + if self.scopes is not None: + kwargs["scopes"] = self.scopes + return PatchKnowledgeDocumentRequestDto(_check_type=False, **kwargs) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py index 6163798b9..67e2b2bae 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/service.py @@ -16,6 +16,10 @@ from gooddata_api_client.model.search_request import SearchRequest from gooddata_api_client.model.search_result import SearchResult +from gooddata_sdk.catalog.knowledge.model.knowledge_document import ( + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, +) from gooddata_sdk.client import GoodDataApiClient from gooddata_sdk.compute.model.execution import ( Execution, @@ -350,3 +354,55 @@ def sync_metadata(self, workspace_id: str, async_req: bool = False) -> None: None """ self._actions_api.metadata_sync(workspace_id, async_req=async_req, _check_return_type=False) + + def list_knowledge_documents( + self, + workspace_id: str, + *, + size: int | None = None, + page_token: str | None = None, + meta_include: str | None = None, + ) -> CatalogListKnowledgeDocumentsResponse: + """ + List knowledge documents in a GoodData workspace with optional cursor-based pagination. + + Args: + workspace_id (str): workspace identifier + size (int, optional): number of documents per page. Defaults to server default (50). + page_token (str, optional): opaque cursor for fetching the next page. + meta_include (str, optional): set to 'page' to include totalCount in response. + Returns: + CatalogListKnowledgeDocumentsResponse: list response with documents and optional pagination info + """ + kwargs: dict[str, Any] = {} + if size is not None: + kwargs["size"] = size + if page_token is not None: + kwargs["page_token"] = page_token + if meta_include is not None: + kwargs["meta_include"] = meta_include + response = self._actions_api.list_documents(workspace_id, _check_return_type=False, **kwargs) + return CatalogListKnowledgeDocumentsResponse.from_api(response) + + def patch_knowledge_document( + self, + workspace_id: str, + filename: str, + patch_request: CatalogPatchKnowledgeDocumentRequest, + ) -> None: + """ + Patch (partially update) a knowledge document in a GoodData workspace. + + Args: + workspace_id (str): workspace identifier + filename (str): filename of the knowledge document to patch + patch_request (CatalogPatchKnowledgeDocumentRequest): patch fields (is_disabled, title, scopes) + Returns: + None + """ + self._actions_api.patch_document( + workspace_id, + filename, + patch_request.as_api_model(), + _check_return_type=False, + ) diff --git a/packages/gooddata-sdk/tests/catalog/knowledge/__init__.py b/packages/gooddata-sdk/tests/catalog/knowledge/__init__.py new file mode 100644 index 000000000..06549c73b --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/knowledge/__init__.py @@ -0,0 +1 @@ +# (C) 2024 GoodData Corporation diff --git a/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py b/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py new file mode 100644 index 000000000..27c302b9d --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py @@ -0,0 +1,156 @@ +# (C) 2024 GoodData Corporation +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from gooddata_sdk.catalog.knowledge.model.knowledge_document import ( + CatalogKnowledgeDocumentMetadata, + CatalogListKnowledgeDocumentsResponse, + CatalogPatchKnowledgeDocumentRequest, +) + + +class TestCatalogKnowledgeDocumentMetadata: + def test_from_api_required_fields(self): + dto = { + "filename": "test.pdf", + "scopes": ["scope1"], + } + result = CatalogKnowledgeDocumentMetadata.from_api(dto) + assert result.filename == "test.pdf" + assert result.scopes == ["scope1"] + assert result.title is None + assert result.is_disabled is None + + def test_from_api_with_optional_fields(self): + dto = { + "filename": "test.pdf", + "title": "Test Document", + "is_disabled": True, + "scopes": ["scope1", "scope2"], + } + result = CatalogKnowledgeDocumentMetadata.from_api(dto) + assert result.filename == "test.pdf" + assert result.title == "Test Document" + assert result.is_disabled is True + assert result.scopes == ["scope1", "scope2"] + + def test_from_api_is_disabled_false(self): + dto = { + "filename": "test.pdf", + "is_disabled": False, + "scopes": [], + } + result = CatalogKnowledgeDocumentMetadata.from_api(dto) + assert result.is_disabled is False + + +class TestCatalogPatchKnowledgeDocumentRequest: + def test_as_api_model_empty(self): + req = CatalogPatchKnowledgeDocumentRequest() + model = req.as_api_model() + # only _check_type was passed, no other kwargs + assert not hasattr(model, "is_disabled") or model.get("is_disabled") is None + assert not hasattr(model, "title") or model.get("title") is None + + def test_as_api_model_with_is_disabled(self): + req = CatalogPatchKnowledgeDocumentRequest(is_disabled=True) + model = req.as_api_model() + assert model["is_disabled"] is True + + def test_as_api_model_with_title(self): + req = CatalogPatchKnowledgeDocumentRequest(title="New Title") + model = req.as_api_model() + assert model["title"] == "New Title" + + def test_as_api_model_with_scopes(self): + req = CatalogPatchKnowledgeDocumentRequest(scopes=["workspace1"]) + model = req.as_api_model() + assert model["scopes"] == ["workspace1"] + + def test_as_api_model_all_fields(self): + req = CatalogPatchKnowledgeDocumentRequest(is_disabled=False, title="Doc", scopes=["s1"]) + model = req.as_api_model() + assert model["is_disabled"] is False + assert model["title"] == "Doc" + assert model["scopes"] == ["s1"] + + +class TestCatalogListKnowledgeDocumentsResponse: + def test_from_api_minimal(self): + doc_dto = {"filename": "doc.pdf", "scopes": []} + dto = {"documents": [doc_dto]} + result = CatalogListKnowledgeDocumentsResponse.from_api(dto) + assert len(result.documents) == 1 + assert result.documents[0].filename == "doc.pdf" + assert result.total_count is None + assert result.next_page_token is None + + def test_from_api_with_pagination(self): + doc_dto = {"filename": "doc.pdf", "scopes": []} + dto = {"documents": [doc_dto], "total_count": 100, "next_page_token": "cursor123"} + result = CatalogListKnowledgeDocumentsResponse.from_api(dto) + assert result.total_count == 100 + assert result.next_page_token == "cursor123" + + def test_from_api_empty_documents(self): + dto = {"documents": []} + result = CatalogListKnowledgeDocumentsResponse.from_api(dto) + assert result.documents == [] + + +class TestComputeServiceKnowledgeDocuments: + def _make_service(self): + from gooddata_sdk.compute.service import ComputeService + + api_client = MagicMock() + service = ComputeService.__new__(ComputeService) + service._api_client = api_client + service._actions_api = api_client.actions_api + return service + + def test_list_knowledge_documents_no_optional_params(self): + service = self._make_service() + mock_response = {"documents": [], "total_count": 0} + service._actions_api.list_documents.return_value = mock_response + + result = service.list_knowledge_documents("workspace1") + + service._actions_api.list_documents.assert_called_once_with( + "workspace1", _check_return_type=False + ) + assert isinstance(result, CatalogListKnowledgeDocumentsResponse) + + def test_list_knowledge_documents_with_pagination_params(self): + service = self._make_service() + mock_response = {"documents": [], "total_count": 50, "next_page_token": "tok"} + service._actions_api.list_documents.return_value = mock_response + + result = service.list_knowledge_documents( + "workspace1", size=10, page_token="prev_tok", meta_include="page" + ) + + service._actions_api.list_documents.assert_called_once_with( + "workspace1", + _check_return_type=False, + size=10, + page_token="prev_tok", + meta_include="page", + ) + assert result.total_count == 50 + assert result.next_page_token == "tok" + + def test_patch_knowledge_document(self): + service = self._make_service() + service._actions_api.patch_document.return_value = None + + patch_req = CatalogPatchKnowledgeDocumentRequest(is_disabled=True) + service.patch_knowledge_document("workspace1", "doc.pdf", patch_req) + + service._actions_api.patch_document.assert_called_once() + call_args = service._actions_api.patch_document.call_args + assert call_args[0][0] == "workspace1" + assert call_args[0][1] == "doc.pdf" + assert call_args[1]["_check_return_type"] is False From a83fcf023af7f0ac625a0820fc102146a1a9d5d8 Mon Sep 17 00:00:00 2001 From: Auto Implementer Date: Tue, 17 Mar 2026 13:01:34 +0000 Subject: [PATCH 2/2] chore: [AUTO] apply ruff formatting --- .../tests/catalog/knowledge/test_knowledge_documents.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py b/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py index 27c302b9d..8b989ed96 100644 --- a/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py +++ b/packages/gooddata-sdk/tests/catalog/knowledge/test_knowledge_documents.py @@ -118,9 +118,7 @@ def test_list_knowledge_documents_no_optional_params(self): result = service.list_knowledge_documents("workspace1") - service._actions_api.list_documents.assert_called_once_with( - "workspace1", _check_return_type=False - ) + service._actions_api.list_documents.assert_called_once_with("workspace1", _check_return_type=False) assert isinstance(result, CatalogListKnowledgeDocumentsResponse) def test_list_knowledge_documents_with_pagination_params(self): @@ -128,9 +126,7 @@ def test_list_knowledge_documents_with_pagination_params(self): mock_response = {"documents": [], "total_count": 50, "next_page_token": "tok"} service._actions_api.list_documents.return_value = mock_response - result = service.list_knowledge_documents( - "workspace1", size=10, page_token="prev_tok", meta_include="page" - ) + result = service.list_knowledge_documents("workspace1", size=10, page_token="prev_tok", meta_include="page") service._actions_api.list_documents.assert_called_once_with( "workspace1",