diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 91f87c918..02d254626 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -181,6 +181,10 @@ CatalogDeclarativeUserGroupPermission, CatalogDeclarativeUserGroups, ) +from gooddata_sdk.catalog.user.entity_model.custom_user_application_setting import ( + CatalogCustomUserApplicationSetting, + CatalogCustomUserApplicationSettingAttributes, +) from gooddata_sdk.catalog.user.entity_model.user import CatalogUser from gooddata_sdk.catalog.user.entity_model.user_group import CatalogUserGroup from gooddata_sdk.catalog.user.management_model.management import ( diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/entity_model/custom_user_application_setting.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/entity_model/custom_user_application_setting.py new file mode 100644 index 000000000..c9cbbd68c --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/entity_model/custom_user_application_setting.py @@ -0,0 +1,115 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from typing import Any + +import attrs +from gooddata_api_client.model.json_api_custom_user_application_setting_in import ( + JsonApiCustomUserApplicationSettingIn, +) +from gooddata_api_client.model.json_api_custom_user_application_setting_in_attributes import ( + JsonApiCustomUserApplicationSettingInAttributes, +) +from gooddata_api_client.model.json_api_custom_user_application_setting_in_document import ( + JsonApiCustomUserApplicationSettingInDocument, +) +from gooddata_api_client.model.json_api_custom_user_application_setting_post_optional_id import ( + JsonApiCustomUserApplicationSettingPostOptionalId, +) +from gooddata_api_client.model.json_api_custom_user_application_setting_post_optional_id_document import ( + JsonApiCustomUserApplicationSettingPostOptionalIdDocument, +) + +from gooddata_sdk.catalog.base import Base + + +@attrs.define(kw_only=True) +class CatalogCustomUserApplicationSettingAttributes(Base): + """Attributes of a custom user application setting.""" + + application_name: str + content: dict[str, Any] + workspace_id: str | None = None + + @staticmethod + def client_class() -> type[JsonApiCustomUserApplicationSettingInAttributes]: + return JsonApiCustomUserApplicationSettingInAttributes + + +@attrs.define(kw_only=True) +class CatalogCustomUserApplicationSetting(Base): + """Entity representing a per-user application setting.""" + + id: str + attributes: CatalogCustomUserApplicationSettingAttributes + + @staticmethod + def client_class() -> type[JsonApiCustomUserApplicationSettingIn]: + return JsonApiCustomUserApplicationSettingIn + + @classmethod + def init( + cls, + setting_id: str, + application_name: str, + content: dict[str, Any], + *, + workspace_id: str | None = None, + ) -> CatalogCustomUserApplicationSetting: + """Convenience factory to create a setting entity without constructing attributes manually.""" + return cls( + id=setting_id, + attributes=CatalogCustomUserApplicationSettingAttributes( + application_name=application_name, + content=content, + workspace_id=workspace_id, + ), + ) + + @classmethod + def from_api(cls, entity: dict[str, Any]) -> CatalogCustomUserApplicationSetting: + ea = entity.get("attributes", {}) + return cls( + id=entity["id"], + attributes=CatalogCustomUserApplicationSettingAttributes( + application_name=ea["applicationName"], + content=ea["content"], + workspace_id=ea.get("workspaceId"), + ), + ) + + def as_post_document(self) -> JsonApiCustomUserApplicationSettingPostOptionalIdDocument: + """Serialize to the document form used by the POST (create) endpoint.""" + kwargs: dict[str, Any] = {} + if self.attributes.workspace_id is not None: + kwargs["workspace_id"] = self.attributes.workspace_id + api_attrs = JsonApiCustomUserApplicationSettingInAttributes( + application_name=self.attributes.application_name, + content=self.attributes.content, + _check_type=False, + **kwargs, + ) + data = JsonApiCustomUserApplicationSettingPostOptionalId( + attributes=api_attrs, + id=self.id, + _check_type=False, + ) + return JsonApiCustomUserApplicationSettingPostOptionalIdDocument(data=data, _check_type=False) + + def as_patch_document(self) -> JsonApiCustomUserApplicationSettingInDocument: + """Serialize to the document form used by the PATCH (update) endpoint.""" + kwargs: dict[str, Any] = {} + if self.attributes.workspace_id is not None: + kwargs["workspace_id"] = self.attributes.workspace_id + api_attrs = JsonApiCustomUserApplicationSettingInAttributes( + application_name=self.attributes.application_name, + content=self.attributes.content, + _check_type=False, + **kwargs, + ) + data = JsonApiCustomUserApplicationSettingIn( + attributes=api_attrs, + id=self.id, + _check_type=False, + ) + return JsonApiCustomUserApplicationSettingInDocument(data=data, _check_type=False) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/service.py index 1e1f4ad33..c706affdd 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/user/service.py @@ -13,6 +13,9 @@ from gooddata_sdk.catalog.user.declarative_model.user_and_user_groups import CatalogDeclarativeUsersUserGroups from gooddata_sdk.catalog.user.declarative_model.user_group import CatalogDeclarativeUserGroups from gooddata_sdk.catalog.user.entity_model.api_token import CatalogApiToken +from gooddata_sdk.catalog.user.entity_model.custom_user_application_setting import ( + CatalogCustomUserApplicationSetting, +) from gooddata_sdk.catalog.user.entity_model.user import CatalogUser, CatalogUserDocument from gooddata_sdk.catalog.user.entity_model.user_group import CatalogUserGroup, CatalogUserGroupDocument from gooddata_sdk.catalog.user.management_model.management import ( @@ -458,3 +461,98 @@ def get_user_api_token(self, user_id: str, api_token_id: str) -> CatalogApiToken def delete_user_api_token(self, user_id: str, api_token_id: str) -> None: self._entities_api.delete_entity_api_tokens(user_id, api_token_id) + + # Custom user application settings + + def list_custom_user_application_settings(self, user_id: str) -> list[CatalogCustomUserApplicationSetting]: + """List all custom application settings for a user. + + Args: + user_id (str): + User identification string. + + Returns: + list[CatalogCustomUserApplicationSetting]: + List of custom user application setting entities. + """ + get_settings = functools.partial( + self._entities_api.get_all_entities_custom_user_application_settings, + user_id, + _check_return_type=False, + ) + settings = load_all_entities(get_settings) + return [CatalogCustomUserApplicationSetting.from_api(v) for v in settings.data] + + def get_custom_user_application_setting(self, user_id: str, setting_id: str) -> CatalogCustomUserApplicationSetting: + """Get a single custom application setting for a user. + + Args: + user_id (str): + User identification string. + setting_id (str): + Setting identification string. + + Returns: + CatalogCustomUserApplicationSetting: + Custom user application setting entity. + """ + result = self._entities_api.get_entity_custom_user_application_settings( + user_id, setting_id, _check_return_type=False + ) + return CatalogCustomUserApplicationSetting.from_api(result.data) + + def create_custom_user_application_setting( + self, user_id: str, setting: CatalogCustomUserApplicationSetting + ) -> CatalogCustomUserApplicationSetting: + """Create a custom application setting for a user. + + Args: + user_id (str): + User identification string. + setting (CatalogCustomUserApplicationSetting): + Setting entity to create. + + Returns: + CatalogCustomUserApplicationSetting: + The newly created custom user application setting entity. + """ + document = setting.as_post_document() + result = self._entities_api.create_entity_custom_user_application_settings( + user_id, document, _check_return_type=False + ) + return CatalogCustomUserApplicationSetting.from_api(result.data) + + def update_custom_user_application_setting( + self, user_id: str, setting: CatalogCustomUserApplicationSetting + ) -> CatalogCustomUserApplicationSetting: + """Update (patch) a custom application setting for a user. + + Args: + user_id (str): + User identification string. + setting (CatalogCustomUserApplicationSetting): + Setting entity with updated values. + + Returns: + CatalogCustomUserApplicationSetting: + The updated custom user application setting entity. + """ + document = setting.as_patch_document() + result = self._entities_api.update_entity_custom_user_application_settings( + user_id, setting.id, document, _check_return_type=False + ) + return CatalogCustomUserApplicationSetting.from_api(result.data) + + def delete_custom_user_application_setting(self, user_id: str, setting_id: str) -> None: + """Delete a custom application setting for a user. + + Args: + user_id (str): + User identification string. + setting_id (str): + Setting identification string. + + Returns: + None + """ + self._entities_api.delete_entity_custom_user_application_settings(user_id, setting_id) diff --git a/packages/gooddata-sdk/tests/catalog/fixtures/users/test_custom_user_application_settings.yaml b/packages/gooddata-sdk/tests/catalog/fixtures/users/test_custom_user_application_settings.yaml new file mode 100644 index 000000000..787fc80e5 --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/fixtures/users/test_custom_user_application_settings.yaml @@ -0,0 +1,288 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings?page=0&size=500 + response: + body: + string: + data: [] + links: + next: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings?page=1&size=500 + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings?page=0&size=500 + headers: + Content-Type: + - application/json + DATE: &id001 + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK + - request: + body: + data: + attributes: + applicationName: my-app + content: + language: en + theme: dark + id: test-setting-1 + type: customUserApplicationSetting + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + Content-Type: + - application/json + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: POST + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings + response: + body: + string: + data: + attributes: + applicationName: my-app + content: + language: en + theme: dark + id: test-setting-1 + type: customUserApplicationSetting + links: + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + headers: + Content-Type: + - application/json + DATE: *id001 + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 201 + message: Created + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + response: + body: + string: + data: + attributes: + applicationName: my-app + content: + language: en + theme: dark + id: test-setting-1 + type: customUserApplicationSetting + links: + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + headers: + Content-Type: + - application/json + DATE: *id001 + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings?page=0&size=500 + response: + body: + string: + data: + - attributes: + applicationName: my-app + content: + language: en + theme: dark + id: test-setting-1 + links: + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + type: customUserApplicationSetting + links: + next: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings?page=1&size=500 + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings?page=0&size=500 + headers: + Content-Type: + - application/json + DATE: *id001 + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK + - request: + body: + data: + attributes: + applicationName: my-app + content: + language: fr + theme: light + id: test-setting-1 + type: customUserApplicationSetting + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + Content-Type: + - application/json + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: PUT + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + response: + body: + string: + data: + attributes: + applicationName: my-app + content: + language: fr + theme: light + id: test-setting-1 + type: customUserApplicationSetting + links: + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + headers: + Content-Type: + - application/json + DATE: *id001 + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + response: + body: + string: + data: + attributes: + applicationName: my-app + content: + language: fr + theme: light + id: test-setting-1 + type: customUserApplicationSetting + links: + self: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + headers: + Content-Type: + - application/json + DATE: *id001 + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: DELETE + uri: http://localhost:3000/api/v1/entities/users/demo/customUserApplicationSettings/test-setting-1 + response: + body: + string: '' + headers: + Content-Type: + - application/vnd.gooddata.api+json + DATE: *id001 + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 204 + message: No Content +version: 1 diff --git a/packages/gooddata-sdk/tests/catalog/test_catalog_user_service.py b/packages/gooddata-sdk/tests/catalog/test_catalog_user_service.py index d29302a23..f05ae2ce7 100644 --- a/packages/gooddata-sdk/tests/catalog/test_catalog_user_service.py +++ b/packages/gooddata-sdk/tests/catalog/test_catalog_user_service.py @@ -27,6 +27,7 @@ import pytest from gooddata_sdk import ( CatalogAssigneeIdentifier, + CatalogCustomUserApplicationSetting, CatalogDeclarativeUser, CatalogDeclarativeUserGroup, CatalogDeclarativeUserGroups, @@ -776,6 +777,63 @@ def test_api_tokens(test_config): safe_delete(sdk.catalog_user.delete_user_api_token, test_config["demo_user"], token_id) +# Custom user application settings + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_custom_user_application_settings.yaml")) +def test_custom_user_application_settings(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + user_id = test_config["demo_user"] + setting_id = "test-setting-1" + application_name = "my-app" + content = {"theme": "dark", "language": "en"} + updated_content = {"theme": "light", "language": "fr"} + + initial_settings = sdk.catalog_user.list_custom_user_application_settings(user_id) + initial_count = len(initial_settings) + + try: + # Create + setting = CatalogCustomUserApplicationSetting.init( + setting_id=setting_id, + application_name=application_name, + content=content, + ) + created = sdk.catalog_user.create_custom_user_application_setting(user_id, setting) + assert created.id == setting_id + assert created.attributes.application_name == application_name + assert created.attributes.content == content + assert created.attributes.workspace_id is None + + # Get + fetched = sdk.catalog_user.get_custom_user_application_setting(user_id, setting_id) + assert fetched.id == setting_id + assert fetched.attributes.application_name == application_name + assert fetched.attributes.content == content + + # List + all_settings = sdk.catalog_user.list_custom_user_application_settings(user_id) + assert len(all_settings) == initial_count + 1 + assert any(s.id == setting_id for s in all_settings) + + # Update + updated_setting = CatalogCustomUserApplicationSetting.init( + setting_id=setting_id, + application_name=application_name, + content=updated_content, + ) + updated = sdk.catalog_user.update_custom_user_application_setting(user_id, updated_setting) + assert updated.id == setting_id + assert updated.attributes.content == updated_content + + # Verify update + re_fetched = sdk.catalog_user.get_custom_user_application_setting(user_id, setting_id) + assert re_fetched.attributes.content == updated_content + + finally: + safe_delete(sdk.catalog_user.delete_custom_user_application_setting, user_id, setting_id) + + # Help functions