diff --git a/crowdin_api/api_resources/__init__.py b/crowdin_api/api_resources/__init__.py index 11a5888..8a729bb 100644 --- a/crowdin_api/api_resources/__init__.py +++ b/crowdin_api/api_resources/__init__.py @@ -10,7 +10,7 @@ from .languages.resource import LanguagesResource from .machine_translation_engines.resource import MachineTranslationEnginesResource from .projects.resource import ProjectsResource -from .reports.resource import ReportsResource, EnterpriseReportsResource +from .reports.resource import EnterpriseReportsResource, ReportsResource from .screenshots.resource import ScreenshotsResource from .security_logs.resource import SecurityLogsResource from .source_files.resource import SourceFilesResource @@ -18,12 +18,13 @@ from .storages.resource import StoragesResource from .string_comments.resource import StringCommentsResource from .string_translations.resource import StringTranslationsResource -from .tasks.resource import TasksResource, EnterpriseTasksResource +from .style_guides.resource import StyleGuidesResource +from .tasks.resource import EnterpriseTasksResource, TasksResource from .teams.resource import TeamsResource from .translation_memory.resource import TranslationMemoryResource from .translation_status.resource import TranslationStatusResource from .translations.resource import TranslationsResource -from .users.resource import UsersResource, EnterpriseUsersResource +from .users.resource import EnterpriseUsersResource, UsersResource from .vendors.resource import VendorsResource from .webhooks.resource import WebhooksResource from .workflows.resource import WorkflowsResource @@ -50,6 +51,7 @@ "SourceStringsResource", "StoragesResource", "StringCommentsResource", + "StyleGuidesResource", "StringTranslationsResource", "TasksResource", "EnterpriseTasksResource", diff --git a/crowdin_api/api_resources/style_guides/__init__.py b/crowdin_api/api_resources/style_guides/__init__.py new file mode 100644 index 0000000..f540619 --- /dev/null +++ b/crowdin_api/api_resources/style_guides/__init__.py @@ -0,0 +1 @@ +__pdoc__ = {"tests": False} diff --git a/crowdin_api/api_resources/style_guides/enums.py b/crowdin_api/api_resources/style_guides/enums.py new file mode 100644 index 0000000..a789c7b --- /dev/null +++ b/crowdin_api/api_resources/style_guides/enums.py @@ -0,0 +1,17 @@ +from enum import Enum + + +class ListStyleGuidesOrderBy(Enum): + ID = "id" + NAME = "name" + USER_ID = "userId" + CREATED_AT = "createdAt" + + +class StyleGuidePatchPath(Enum): + NAME = "/name" + AI_INSTRUCTIONS = "/aiInstructions" + LANGUAGE_IDS = "/languageIds" + PROJECT_IDS = "/projectIds" + IS_SHARED = "/isShared" + STORAGE_ID = "/storageId" diff --git a/crowdin_api/api_resources/style_guides/resource.py b/crowdin_api/api_resources/style_guides/resource.py new file mode 100644 index 0000000..a270d92 --- /dev/null +++ b/crowdin_api/api_resources/style_guides/resource.py @@ -0,0 +1,101 @@ +from typing import Iterable, Optional + +from crowdin_api.api_resources.abstract.resources import BaseResource +from crowdin_api.api_resources.style_guides.types import ( + AddStyleGuideRequest, + StyleGuidePatchRequest, +) +from crowdin_api.sorting import Sorting + + +class StyleGuidesResource(BaseResource): + """ + Resource for Style Guides. + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#tag/Style-Guides + """ + + def get_style_guides_path(self, style_guide_id: Optional[int] = None): + if style_guide_id is not None: + return f"style-guides/{style_guide_id}" + return "style-guides" + + def list_style_guides( + self, + order_by: Optional[Sorting] = None, + user_id: Optional[int] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, + ): + """ + List Style Guides + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.getMany + """ + params = { + "orderBy": order_by, + "userId": user_id, + } + params.update(self.get_page_params(limit=limit, offset=offset)) + + return self.requester.request( + method="get", + path=self.get_style_guides_path(), + params=params, + ) + + def add_style_guide(self, request_data: AddStyleGuideRequest): + """ + Create Style Guide + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.post + """ + return self.requester.request( + method="post", + path=self.get_style_guides_path(), + request_data=request_data, + ) + + def get_style_guide(self, style_guide_id: int): + """ + Get Style Guide + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.get + """ + return self.requester.request( + method="get", + path=self.get_style_guides_path(style_guide_id), + ) + + def delete_style_guide(self, style_guide_id: int): + """ + Delete Style Guide + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.delete + """ + return self.requester.request( + method="delete", + path=self.get_style_guides_path(style_guide_id), + ) + + def edit_style_guide( + self, + style_guide_id: int, + request_data: Iterable[StyleGuidePatchRequest], + ): + """ + Edit Style Guide + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.patch + """ + return self.requester.request( + method="patch", + path=self.get_style_guides_path(style_guide_id), + request_data=request_data, + ) diff --git a/crowdin_api/api_resources/style_guides/tests/test_style_guides_resources.py b/crowdin_api/api_resources/style_guides/tests/test_style_guides_resources.py new file mode 100644 index 0000000..d88ac60 --- /dev/null +++ b/crowdin_api/api_resources/style_guides/tests/test_style_guides_resources.py @@ -0,0 +1,166 @@ +from unittest import mock + +import pytest +from crowdin_api.api_resources.enums import PatchOperation +from crowdin_api.api_resources.style_guides.enums import ( + ListStyleGuidesOrderBy, + StyleGuidePatchPath, +) +from crowdin_api.api_resources.style_guides.resource import StyleGuidesResource +from crowdin_api.api_resources.style_guides.types import AddStyleGuideRequest +from crowdin_api.requester import APIRequester +from crowdin_api.sorting import Sorting, SortingOrder, SortingRule + + +class TestStyleGuidesResource: + resource_class = StyleGuidesResource + + def get_resource(self, base_absolut_url): + return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) + + @pytest.mark.parametrize( + "in_params, path", + ( + ({}, "style-guides"), + ({"style_guide_id": 2}, "style-guides/2"), + ), + ) + def test_get_style_guides_path(self, in_params, path, base_absolut_url): + resource = self.get_resource(base_absolut_url) + assert resource.get_style_guides_path(**in_params) == path + + @pytest.mark.parametrize( + "incoming_data, request_params", + ( + ( + {}, + { + "orderBy": None, + "userId": None, + "limit": 25, + "offset": 0, + }, + ), + ( + { + "order_by": Sorting( + [SortingRule(ListStyleGuidesOrderBy.CREATED_AT, SortingOrder.DESC)] + ), + "user_id": 2, + "limit": 25, + "offset": 0, + }, + { + "orderBy": Sorting( + [SortingRule(ListStyleGuidesOrderBy.CREATED_AT, SortingOrder.DESC)] + ), + "userId": 2, + "limit": 25, + "offset": 0, + }, + ), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_list_style_guides(self, m_request, incoming_data, request_params, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.list_style_guides(**incoming_data) == "response" + + m_request.assert_called_once_with( + method="get", + path="style-guides", + params=request_params, + ) + + @pytest.mark.parametrize( + "incoming_data, request_data", + ( + ( + AddStyleGuideRequest( + name="Be My Eyes iOS's Style Guide", + storageId=1, + ), + { + "name": "Be My Eyes iOS's Style Guide", + "storageId": 1, + }, + ), + ( + AddStyleGuideRequest( + name="Be My Eyes iOS's Style Guide", + storageId=1, + aiInstructions="Rules to be used by AI models", + languageIds=["uk", "fr", "de"], + projectIds=[1, 2, 3], + isShared=False, + ), + { + "name": "Be My Eyes iOS's Style Guide", + "storageId": 1, + "aiInstructions": "Rules to be used by AI models", + "languageIds": ["uk", "fr", "de"], + "projectIds": [1, 2, 3], + "isShared": False, + }, + ), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_add_style_guide(self, m_request, incoming_data, request_data, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.add_style_guide(incoming_data) == "response" + + m_request.assert_called_once_with( + method="post", + path="style-guides", + request_data=request_data, + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_get_style_guide(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.get_style_guide(style_guide_id=2) == "response" + + m_request.assert_called_once_with( + method="get", + path="style-guides/2", + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_delete_style_guide(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.delete_style_guide(style_guide_id=2) == "response" + + m_request.assert_called_once_with( + method="delete", + path="style-guides/2", + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_edit_style_guide(self, m_request, base_absolut_url): + m_request.return_value = "response" + + request_data = [ + { + "op": PatchOperation.REPLACE, + "path": StyleGuidePatchPath.NAME, + "value": "Be My Eyes iOS's Style Guide", + } + ] + + resource = self.get_resource(base_absolut_url) + assert resource.edit_style_guide(style_guide_id=2, request_data=request_data) == "response" + + m_request.assert_called_once_with( + method="patch", + path="style-guides/2", + request_data=request_data, + ) diff --git a/crowdin_api/api_resources/style_guides/types.py b/crowdin_api/api_resources/style_guides/types.py new file mode 100644 index 0000000..a3078ef --- /dev/null +++ b/crowdin_api/api_resources/style_guides/types.py @@ -0,0 +1,20 @@ +from typing import Any, Iterable, Optional + +from crowdin_api.api_resources.enums import PatchOperation +from crowdin_api.api_resources.style_guides.enums import StyleGuidePatchPath +from crowdin_api.typing import TypedDict + + +class AddStyleGuideRequest(TypedDict): + name: str + storageId: int + aiInstructions: Optional[str] + languageIds: Optional[Iterable[str]] + projectIds: Optional[Iterable[int]] + isShared: Optional[bool] + + +class StyleGuidePatchRequest(TypedDict): + op: PatchOperation + path: StyleGuidePatchPath + value: Any diff --git a/crowdin_api/client.py b/crowdin_api/client.py index d5693da..8194945 100644 --- a/crowdin_api/client.py +++ b/crowdin_api/client.py @@ -360,6 +360,12 @@ def string_translations(self) -> api_resources.StringTranslationsResource: requester=self.get_api_requestor(), page_size=self.PAGE_SIZE ) + @property + def style_guides(self) -> api_resources.StyleGuidesResource: + return api_resources.StyleGuidesResource( + requester=self.get_api_requestor(), page_size=self.PAGE_SIZE + ) + @property def tasks(self) -> Union[api_resources.TasksResource, api_resources.EnterpriseTasksResource]: if self._is_enterprise_platform: