diff --git a/AGENTS.md b/AGENTS.md
index bf0e6a45..7dab863a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -29,7 +29,7 @@ Fetch the endpoint spec first (see Crowdin API reference below). Then:
- List endpoints call `self._get_entire_data(method="get", path=..., params=...)` so `with_fetch_all()` pagination works; everything else calls `self.requester.request(...)`.
- Project-scoped methods take `projectId: Optional[int] = None` and resolve it via `projectId or self.get_project_id()`.
- Request body shapes go in `types.py` as TypedDicts; enum values in `enums.py`. Enums and `Sorting` objects can be passed straight into `params`/`request_data` — the custom JSON encoder serializes them, and `None` values are stripped before sending.
- - End the docstring with `Link to documentation:` and the developer.crowdin.com operation URL (pdoc publishes these).
+ - End the docstring with `Link to documentation:` and the support.crowdin.com operation URL, e.g. `https://support.crowdin.com/developer/api/v2/#operation/api.projects.getMany` (Enterprise: `https://support.crowdin.com/developer/enterprise/api/v2/#operation/...`). pdoc publishes these.
2. For a new resource, register it in three places: an import plus `__all__` entry in `crowdin_api/api_resources/__init__.py` (alphabetical), a `@property` on `CrowdinClient` in `client.py` (copy an existing property; use the enterprise-guard or per-platform variant when the API is Enterprise-only or differs by platform), and one tuple in each of the two parametrize lists in `crowdin_api/tests/test_client.py`. Some resource classes exist but were never registered (e.g. `BranchesResource`, `StringCorrectionsResource`) — "adding" one of those is exactly this registration work.
3. Test in the resource's `tests/` dir: patch the requester with `@mock.patch("crowdin_api.requester.APIRequester.request")`, call the method, then `m_request.assert_called_once_with(method=..., path=..., ...)` with the exact kwargs. The `base_absolut_url` fixture (spelled without the second "e") provides the base URL. No test performs real HTTP.
diff --git a/README.md b/README.md
index 2ddfebc1..5e09fa73 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,8 @@ Crowdin API is a full-featured RESTful API that helps you to integrate localizat
[**`API Client Docs`**](https://crowdin.github.io/crowdin-api-client-python/) |
-[**`Crowdin API`**](https://developer.crowdin.com/api/v2/) |
-[**`Crowdin Enterprise API`**](https://developer.crowdin.com/enterprise/api/v2/)
+[**`Crowdin API`**](https://support.crowdin.com/developer/api/v2/) |
+[**`Crowdin Enterprise API`**](https://support.crowdin.com/developer/enterprise/api/v2/)
[](https://pypi.org/project/crowdin-api-client/)
[](https://pepy.tech/project/crowdin-api-client)
@@ -185,7 +185,7 @@ class FirstCrowdinClient(CrowdinClient):
### GraphQL API
-This library also provides the possibility to use [GraphQL API](https://developer.crowdin.com/graphql-api/):
+This library also provides the possibility to use [GraphQL API](https://support.crowdin.com/developer/graphql-api/):
```python
from crowdin_api import CrowdinClient
diff --git a/crowdin_api/api_resources/__init__.py b/crowdin_api/api_resources/__init__.py
index 9d164792..008bee04 100644
--- a/crowdin_api/api_resources/__init__.py
+++ b/crowdin_api/api_resources/__init__.py
@@ -1,24 +1,38 @@
+from .advisors.resource import AdvisorsResource
from .ai.resource import AIResource, EnterpriseAIResource
from .application.resource import ApplicationResource
+from .branches.resource import BranchesResource
from .bundles.resource import BundlesResource
+from .clients.resource import ClientsResource
+from .custom_placeholders.resource import CustomPlaceholdersResource
+from .custom_spellcheckers.resource import CustomSpellcheckersResource
from .dictionaries.resource import DictionariesResource
from .distributions.resource import DistributionsResource
+from .external_qa_checks.resource import ExternalQaChecksResource
from .fields.resource import FieldsResource
from .glossaries.resource import GlossariesResource
from .groups.resource import GroupsResource
from .labels.resource import LabelsResource
from .languages.resource import LanguagesResource
-from .machine_translation_engines.resource import MachineTranslationEnginesResource
+from .machine_translation_engines.resource import (
+ EnterpriseMachineTranslationEnginesResource,
+ MachineTranslationEnginesResource,
+)
+from .notifications.resource import NotificationResource
+from .organization.resource import OrganizationResource
+from .project_placeholders.resource import ProjectPlaceholdersResource
from .projects.resource import ProjectsResource
from .reports.resource import EnterpriseReportsResource, ReportsResource
from .screenshots.resource import ScreenshotsResource
from .security_logs.resource import SecurityLogsResource
from .source_files.resource import SourceFilesResource
-from .source_strings.resource import SourceStringsResource
+from .source_strings.resource import EnterpriseSourceStringsResource, SourceStringsResource
from .storages.resource import StoragesResource
from .string_comments.resource import StringCommentsResource
+from .string_corrections.resource import StringCorrectionsResource
from .string_translations.resource import StringTranslationsResource
from .style_guides.resource import StyleGuidesResource
+from .system_placeholders.resource import SystemPlaceholdersResource
from .tasks.resource import EnterpriseTasksResource, TasksResource
from .teams.resource import TeamsResource
from .translation_memory.resource import TranslationMemoryResource
@@ -26,22 +40,34 @@
from .translations.resource import TranslationsResource
from .users.resource import EnterpriseUsersResource, UsersResource
from .vendors.resource import VendorsResource
+from .webhooks.organization.resource import OrganizationWebhooksResource
from .webhooks.resource import WebhooksResource
from .workflows.resource import WorkflowsResource
__all__ = [
+ "AdvisorsResource",
"AIResource",
"EnterpriseAIResource",
"ApplicationResource",
+ "BranchesResource",
"BundlesResource",
+ "ClientsResource",
+ "CustomPlaceholdersResource",
+ "CustomSpellcheckersResource",
"DictionariesResource",
"DistributionsResource",
+ "ExternalQaChecksResource",
"FieldsResource",
"GlossariesResource",
"GroupsResource",
"LabelsResource",
"LanguagesResource",
"MachineTranslationEnginesResource",
+ "EnterpriseMachineTranslationEnginesResource",
+ "NotificationResource",
+ "OrganizationResource",
+ "OrganizationWebhooksResource",
+ "ProjectPlaceholdersResource",
"ProjectsResource",
"ReportsResource",
"EnterpriseReportsResource",
@@ -49,16 +75,19 @@
"SecurityLogsResource",
"SourceFilesResource",
"SourceStringsResource",
+ "EnterpriseSourceStringsResource",
"StoragesResource",
"StringCommentsResource",
+ "StringCorrectionsResource",
"StringTranslationsResource",
"StyleGuidesResource",
+ "SystemPlaceholdersResource",
"TasksResource",
"EnterpriseTasksResource",
"TeamsResource",
"TranslationMemoryResource",
- "TranslationStatusResource",
"TranslationsResource",
+ "TranslationStatusResource",
"UsersResource",
"EnterpriseUsersResource",
"VendorsResource",
diff --git a/crowdin_api/api_resources/advisors/__init__.py b/crowdin_api/api_resources/advisors/__init__.py
new file mode 100644
index 00000000..9f77d639
--- /dev/null
+++ b/crowdin_api/api_resources/advisors/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {'tests': False}
diff --git a/crowdin_api/api_resources/advisors/enums.py b/crowdin_api/api_resources/advisors/enums.py
new file mode 100644
index 00000000..f6ef9057
--- /dev/null
+++ b/crowdin_api/api_resources/advisors/enums.py
@@ -0,0 +1,40 @@
+from enum import Enum
+
+
+class AdvisorInspectorMode(Enum):
+ AUTO = "auto"
+ ALL = "all"
+
+
+class AdvisorInsightStatus(Enum):
+ PENDING = "pending"
+ CHECKING = "checking"
+ OUTDATED = "outdated"
+ DONE = "done"
+
+
+class AdvisorInsightOutcome(Enum):
+ FLAGGED = "flagged"
+ CLEAR = "clear"
+ NOT_APPLICABLE = "not_applicable"
+
+
+class AdvisorInsightPatchPath(Enum):
+ IS_DISMISSED = "/isDismissed"
+
+
+class AdvisorInsightMetricUnit(Enum):
+ PERCENT = "percent"
+ COUNT = "count"
+
+
+class AdvisorInsightMetricTone(Enum):
+ DEFAULT = "default"
+ SUCCESS = "success"
+ DANGER = "danger"
+
+
+class AdvisorInsightMetricSource(Enum):
+ DETERMINISTIC = "deterministic"
+ AI = "ai"
+ APP = "app"
diff --git a/crowdin_api/api_resources/advisors/resource.py b/crowdin_api/api_resources/advisors/resource.py
new file mode 100644
index 00000000..8d4078d8
--- /dev/null
+++ b/crowdin_api/api_resources/advisors/resource.py
@@ -0,0 +1,187 @@
+from datetime import datetime
+from typing import Any, Dict, Iterable, Optional, Union
+
+from crowdin_api.api_resources.abstract.resources import BaseResource
+from crowdin_api.api_resources.advisors.enums import (
+ AdvisorInsightOutcome,
+ AdvisorInsightStatus,
+)
+from crowdin_api.api_resources.advisors.types import (
+ AdvisorCheckInspector,
+ AdvisorInsightMetric,
+ AdvisorInsightPatchRequest,
+ AdvisorInsightRecommendation,
+)
+from crowdin_api.utils import convert_to_query_list
+
+
+class AdvisorsResource(BaseResource):
+ """
+ Resource for Advisors.
+
+ Advisors run inspectors against a project and report insights about its localization health.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Advisors
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Advisors
+ """
+
+ def get_advisor_checks_path(self, projectId: int, checkId: Optional[str] = None):
+ if checkId is not None:
+ return f"projects/{projectId}/advisors/checks/{checkId}"
+ return f"projects/{projectId}/advisors/checks"
+
+ def get_advisor_insights_path(self, projectId: int, insightId: Optional[int] = None):
+ if insightId is not None:
+ return f"projects/{projectId}/advisors/insights/{insightId}"
+ return f"projects/{projectId}/advisors/insights"
+
+ def create_advisor_check(
+ self,
+ category: Optional[str] = None,
+ inspectors: Optional[Iterable[AdvisorCheckInspector]] = None,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Create Advisor Check.
+
+ At most one of `category` or `inspectors` may be set; if neither is set, every
+ inspector is re-checked.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.advisors.checks.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.advisors.checks.post
+ """
+ if category is not None and inspectors is not None:
+ raise ValueError("You can set only one of category or inspectors.")
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_advisor_checks_path(projectId=projectId),
+ request_data={"category": category, "inspectors": inspectors},
+ )
+
+ def get_advisor_check_status(self, checkId: str, projectId: Optional[int] = None):
+ """
+ Get Advisor Check Status.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.advisors.checks.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.advisors.checks.get
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_advisor_checks_path(projectId=projectId, checkId=checkId),
+ )
+
+ def list_advisor_insights(
+ self,
+ projectId: Optional[int] = None,
+ isDismissed: Optional[bool] = None,
+ status: Optional[
+ Union[str, AdvisorInsightStatus, Iterable[Union[str, AdvisorInsightStatus]]]
+ ] = None,
+ outcome: Optional[
+ Union[str, AdvisorInsightOutcome, Iterable[Union[str, AdvisorInsightOutcome]]]
+ ] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List Advisor Insights.
+
+ `status` and `outcome` accept a single value or a list of values (sent comma-separated).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.advisors.insights.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.advisors.insights.getMany
+ """
+ projectId = projectId or self.get_project_id()
+ params = {
+ "isDismissed": isDismissed,
+ "status": convert_to_query_list(status),
+ "outcome": convert_to_query_list(outcome),
+ }
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_advisor_insights_path(projectId=projectId),
+ params=params,
+ )
+
+ def edit_advisor_insight(
+ self,
+ insightId: int,
+ data: Iterable[AdvisorInsightPatchRequest],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Edit Advisor Insight.
+
+ Currently only `/isDismissed` is patchable.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.advisors.insights.patch
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.advisors.insights.patch
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="patch",
+ path=self.get_advisor_insights_path(projectId=projectId, insightId=insightId),
+ request_data=data,
+ )
+
+ def create_or_update_application_advisor_insight(
+ self,
+ applicationIdentifier: str,
+ moduleKey: str,
+ outcome: AdvisorInsightOutcome,
+ checkedAt: Optional[Union[datetime, str]] = None,
+ metrics: Optional[Iterable[AdvisorInsightMetric]] = None,
+ recommendations: Optional[Iterable[AdvisorInsightRecommendation]] = None,
+ payload: Optional[Dict[str, Any]] = None,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Create or Update Application Advisor Insight.
+
+ Called by an installed application's `advisor-inspector` module to publish its check result.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.applications.modules.advisors.insights.put
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.applications.modules.advisors.insights.put
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="put",
+ path=(
+ f"projects/{projectId}/applications/{applicationIdentifier}"
+ f"/modules/{moduleKey}/advisors/insights"
+ ),
+ request_data={
+ "outcome": outcome,
+ "checkedAt": checkedAt,
+ "metrics": metrics,
+ "recommendations": recommendations,
+ "payload": payload,
+ },
+ )
diff --git a/crowdin_api/api_resources/advisors/tests/test_advisors_resources.py b/crowdin_api/api_resources/advisors/tests/test_advisors_resources.py
new file mode 100644
index 00000000..b6f9ad39
--- /dev/null
+++ b/crowdin_api/api_resources/advisors/tests/test_advisors_resources.py
@@ -0,0 +1,254 @@
+from unittest import mock
+
+import pytest
+
+from crowdin_api.api_resources.advisors.enums import (
+ AdvisorInspectorMode,
+ AdvisorInsightMetricSource,
+ AdvisorInsightMetricTone,
+ AdvisorInsightMetricUnit,
+ AdvisorInsightOutcome,
+ AdvisorInsightPatchPath,
+ AdvisorInsightStatus,
+)
+from crowdin_api.api_resources.advisors.resource import AdvisorsResource
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.requester import APIRequester
+
+
+class TestAdvisorsResource:
+ resource_class = AdvisorsResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"projectId": 1}, "projects/1/advisors/checks"),
+ ({"projectId": 1, "checkId": "abc"}, "projects/1/advisors/checks/abc"),
+ ),
+ )
+ def test_get_advisor_checks_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_advisor_checks_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"projectId": 1}, "projects/1/advisors/insights"),
+ ({"projectId": 1, "insightId": 2}, "projects/1/advisors/insights/2"),
+ ),
+ )
+ def test_get_advisor_insights_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_advisor_insights_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ ({}, {"category": None, "inspectors": None}),
+ ({"category": "strings"}, {"category": "strings", "inspectors": None}),
+ (
+ {
+ "inspectors": [
+ {
+ "key": "string_context_relevance",
+ "options": {"mode": AdvisorInspectorMode.AUTO, "promptId": 3},
+ }
+ ]
+ },
+ {
+ "category": None,
+ "inspectors": [
+ {
+ "key": "string_context_relevance",
+ "options": {"mode": AdvisorInspectorMode.AUTO, "promptId": 3},
+ }
+ ],
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_create_advisor_check(self, m_request, in_params, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.create_advisor_check(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/advisors/checks",
+ request_data=request_data,
+ )
+
+ def test_create_advisor_check_invalid(self, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ with pytest.raises(ValueError):
+ resource.create_advisor_check(
+ category="strings", inspectors=[{"key": "k"}], projectId=1
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_advisor_check_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_advisor_check_status("abc", projectId=1) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/advisors/checks/abc",
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ (
+ {},
+ {
+ "isDismissed": None,
+ "status": None,
+ "outcome": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ ),
+ (
+ {
+ "isDismissed": False,
+ "status": [AdvisorInsightStatus.DONE, "outdated"],
+ "outcome": AdvisorInsightOutcome.FLAGGED,
+ "offset": 10,
+ "limit": 5,
+ },
+ {
+ "isDismissed": False,
+ "status": "done,outdated",
+ "outcome": AdvisorInsightOutcome.FLAGGED,
+ "offset": 10,
+ "limit": 5,
+ },
+ ),
+ (
+ {"status": "pending,checking"},
+ {
+ "isDismissed": None,
+ "status": "pending,checking",
+ "outcome": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_advisor_insights(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_advisor_insights(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/advisors/insights",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_advisor_insight(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": AdvisorInsightPatchPath.IS_DISMISSED,
+ "value": True,
+ }
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_advisor_insight(2, data, projectId=1) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="projects/1/advisors/insights/2",
+ request_data=data,
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {"outcome": AdvisorInsightOutcome.CLEAR},
+ {
+ "outcome": AdvisorInsightOutcome.CLEAR,
+ "checkedAt": None,
+ "metrics": None,
+ "recommendations": None,
+ "payload": None,
+ },
+ ),
+ (
+ {
+ "outcome": AdvisorInsightOutcome.FLAGGED,
+ "checkedAt": "2026-01-01T00:00:00+00:00",
+ "metrics": [
+ {
+ "key": "quality_score",
+ "value": 12,
+ "unit": AdvisorInsightMetricUnit.PERCENT,
+ "tone": AdvisorInsightMetricTone.DANGER,
+ "source": AdvisorInsightMetricSource.APP,
+ }
+ ],
+ "recommendations": [{"id": "r1", "primary": True, "params": {"a": 1}}],
+ "payload": {"x": "y"},
+ },
+ {
+ "outcome": AdvisorInsightOutcome.FLAGGED,
+ "checkedAt": "2026-01-01T00:00:00+00:00",
+ "metrics": [
+ {
+ "key": "quality_score",
+ "value": 12,
+ "unit": AdvisorInsightMetricUnit.PERCENT,
+ "tone": AdvisorInsightMetricTone.DANGER,
+ "source": AdvisorInsightMetricSource.APP,
+ }
+ ],
+ "recommendations": [{"id": "r1", "primary": True, "params": {"a": 1}}],
+ "payload": {"x": "y"},
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_create_or_update_application_advisor_insight(
+ self, m_request, in_params, request_data, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.create_or_update_application_advisor_insight(
+ "my-app", "my-module", projectId=1, **in_params
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="put",
+ path="projects/1/applications/my-app/modules/my-module/advisors/insights",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_default_project_id(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.resource_class(
+ requester=APIRequester(base_url=base_absolut_url), project_id=5
+ )
+ assert resource.get_advisor_check_status("abc") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/5/advisors/checks/abc",
+ )
diff --git a/crowdin_api/api_resources/advisors/types.py b/crowdin_api/api_resources/advisors/types.py
new file mode 100644
index 00000000..c7202e7e
--- /dev/null
+++ b/crowdin_api/api_resources/advisors/types.py
@@ -0,0 +1,47 @@
+from datetime import datetime
+from typing import Any, Dict, Optional, Union
+
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.typing import TypedDict
+from crowdin_api.api_resources.advisors.enums import (
+ AdvisorInspectorMode,
+ AdvisorInsightMetricSource,
+ AdvisorInsightMetricTone,
+ AdvisorInsightMetricUnit,
+ AdvisorInsightPatchPath,
+)
+
+
+class AdvisorInspectorOptions(TypedDict, total=False):
+ mode: AdvisorInspectorMode
+ promptId: int
+
+
+class _AdvisorCheckInspectorRequired(TypedDict):
+ key: str
+
+
+class AdvisorCheckInspector(_AdvisorCheckInspectorRequired, total=False):
+ options: Optional[AdvisorInspectorOptions]
+
+
+class AdvisorInsightPatchRequest(TypedDict):
+ op: PatchOperation
+ path: AdvisorInsightPatchPath
+ value: bool
+
+
+class AdvisorInsightMetric(TypedDict, total=False):
+ key: str
+ value: Union[int, float]
+ unit: AdvisorInsightMetricUnit
+ threshold: Optional[Union[int, float]]
+ tone: AdvisorInsightMetricTone
+ source: AdvisorInsightMetricSource
+ checkedAt: Optional[Union[datetime, str]]
+
+
+class AdvisorInsightRecommendation(TypedDict, total=False):
+ id: str
+ primary: bool
+ params: Dict[str, Any]
diff --git a/crowdin_api/api_resources/ai/enums.py b/crowdin_api/api_resources/ai/enums.py
index 920c9d03..e3d18ff4 100644
--- a/crowdin_api/api_resources/ai/enums.py
+++ b/crowdin_api/api_resources/ai/enums.py
@@ -17,6 +17,7 @@ class EditAIPromptPath(Enum):
ACTION = "/action"
AI_PROVIDER_ID = "/aiProviderId"
AI_MODEL_ID = "/aiModelId"
+ # Deprecated in Crowdin API (still supported by Crowdin Enterprise API).
IS_ENABLED = "/isEnabled"
ENABLED_PROJECT_IDS = "/enabledProjectIds"
CONFIG = "/config"
@@ -26,8 +27,13 @@ class AIProviderType(Enum):
OPEN_AI = "open_ai"
AZURE_OPEN_AI = "azure_open_ai"
GOOGLE_GEMINI = "google_gemini"
+ GOOGLE_GEMINI_AI_STUDIO = "google_gemini_ai_studio"
MISTRAL_AI = "mistral_ai"
ANTHROPIC = "anthropic"
+ X_AI = "x_ai"
+ WATSONX = "watsonx"
+ DEEPSEEK = "deepseek"
+ MICROSOFT_FOUNDRY = "microsoft_foundry"
CUSTOM_AI = "custom_ai"
@@ -46,6 +52,14 @@ class DatasetPurpose(Enum):
class EditAiCustomPlaceholderPatchPath(Enum):
+ """Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `EditAiSnippetPatchPath`."""
+
+ DESCRIPTION = "/description"
+ PLACEHOLDER = "/placeholder"
+ VALUE = "/value"
+
+
+class EditAiSnippetPatchPath(Enum):
DESCRIPTION = "/description"
PLACEHOLDER = "/placeholder"
VALUE = "/value"
@@ -65,10 +79,30 @@ class AiToolType(Enum):
class AiReportType(Enum):
TOKENS_USAGE_RAW_DATA = "tokens-usage-raw-data"
+ COSTS_BY_USERS = "costs-by-users"
class EditAiSettingsPatchPath(Enum):
+ """
+ Paths containing `{userId}` are templates: replace `{userId}` with the actual user identifier,
+ e.g. `EditAiSettingsPatchPath.PER_USER_OVERRIDE_DAILY_COST_LIMIT.value.format(userId=12)`.
+ """
+
+ PRE_TRANSLATION_AI_PROMPT_ID = "/preTranslationAiPromptId"
EDITOR_SUGGESTION_AI_PROMPT_ID = "/editorSuggestionAiPromptId"
+ # Crowdin Enterprise only.
+ ALIGNMENT_ACTION_AI_PROMPT_ID = "/alignmentActionAiPromptId"
+ QA_CHECK_ACTION_AI_PROMPT_ID = "/qaCheckActionAiPromptId"
+ CONTEXT_REVIEW_AI_PROMPT_ID = "/contextReviewAiPromptId"
+ DAILY_COST_LIMIT = "/dailyCostLimit"
+ MONTHLY_COST_LIMIT = "/monthlyCostLimit"
+ USER_DAILY_COST_LIMIT = "/userDailyCostLimit"
+ USER_MONTHLY_COST_LIMIT = "/userMonthlyCostLimit"
+ PER_USER_OVERRIDE = "/perUserOverrides/{userId}"
+ PER_USER_OVERRIDE_COST_LIMIT_MODE = "/perUserOverrides/{userId}/costLimitMode"
+ PER_USER_OVERRIDE_DAILY_COST_LIMIT = "/perUserOverrides/{userId}/dailyCostLimit"
+ PER_USER_OVERRIDE_MONTHLY_COST_LIMIT = "/perUserOverrides/{userId}/monthlyCostLimit"
+ # Deprecated: no longer supported by the API.
SHORTCUTS = "/shortcuts"
@@ -108,3 +142,7 @@ class AiRequestLogSourceAction(Enum):
QA_CHECK = "qa_check"
AI_SUGGESTION = "ai_suggestion"
ADVISOR = "advisor"
+
+
+class AiRequestLogExportFormat(Enum):
+ CSV = "csv"
diff --git a/crowdin_api/api_resources/ai/resource.py b/crowdin_api/api_resources/ai/resource.py
index 92ce2f34..f4fd6142 100644
--- a/crowdin_api/api_resources/ai/resource.py
+++ b/crowdin_api/api_resources/ai/resource.py
@@ -1,10 +1,13 @@
from datetime import datetime
from typing import Dict, Iterable, Optional, Union
+from deprecated import deprecated
+
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.ai.enums import (
AIPromptAction,
AiPromptFineTuningJobStatus,
+ AiRequestLogExportFormat,
AiRequestLogSourceAction,
AiRequestLogStatus,
AIProviderType,
@@ -13,6 +16,7 @@
AddAiCustomPlaceholderRequest,
AddAIPromptRequestScheme,
AddAIProviderReqeustScheme,
+ AddAiSnippetRequest,
AiFileTranslationRequest,
AiTranslateStringsRequest,
CreateAIPromptFineTuningJobRequest,
@@ -20,6 +24,7 @@
EditAIPromptScheme,
EditAIProviderRequestScheme,
EditAiSettingsPatch,
+ EditAiSnippetPatch,
GenerateAiPromptCompletionRequest,
GenerateAIPromptFineTuningDatasetRequest,
GenerateAiReportRequest,
@@ -30,6 +35,7 @@
from crowdin_api.utils import (
convert_enum_collection_to_string_if_exists,
convert_enum_to_string_if_exists,
+ convert_to_query_string,
)
@@ -38,7 +44,7 @@ class AIResource(BaseResource):
Resource for AI.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/AI
+ https://support.crowdin.com/developer/api/v2/#tag/AI
"""
def get_ai_path(self, userId: int, aiPromptId: Optional[int] = None):
@@ -63,7 +69,7 @@ def list_ai_prompts(
List AI Prompts
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.ai.prompts.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.prompts.getMany
"""
params = {"projectId": projectId, "action": action}
params.update(self.get_page_params(limit=limit, offset=offset))
@@ -77,7 +83,7 @@ def add_ai_prompt(self, userId: int, request_data: AddAIPromptRequestScheme):
Add AI Prompt
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.prompts.post
"""
return self.requester.request(
@@ -91,7 +97,7 @@ def get_ai_prompt(self, userId: int, aiPromptId: int):
Get AI Prompt
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.prompts.get
"""
return self.requester.request(
@@ -104,7 +110,7 @@ def delete_ai_prompt(self, userId: int, aiPromptId: int):
Delete AI Prompt
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.prompts.delete
"""
return self.requester.request(
@@ -119,7 +125,7 @@ def edit_ai_prompt(
Edit AI Prompt
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.prompts.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.prompts.patch
"""
return self.requester.request(
@@ -138,7 +144,7 @@ def list_ai_providers(
List AI Providers
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.ai.providers.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.providers.getMany
"""
params = self.get_page_params(limit=limit, offset=offset)
return self.requester.request(
@@ -150,7 +156,7 @@ def add_ai_provider(self, userId: int, request_data: AddAIProviderReqeustScheme)
Add AI Provider
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.providers.post
"""
return self.requester.request(
method="post",
@@ -163,7 +169,7 @@ def get_ai_provider(self, userId: int, aiProviderId: int):
Get AI Provider
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.providers.get
"""
return self.requester.request(
method="get",
@@ -175,7 +181,7 @@ def delete_ai_provider(self, userId: int, aiProviderId: int):
Delete AI Provider
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.providers.delete
"""
return self.requester.request(
method="delete",
@@ -189,7 +195,7 @@ def edit_ai_provider(
Edit AI Provider
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.providers.patch
"""
return self.requester.request(
method="patch",
@@ -202,7 +208,7 @@ def list_ai_provider_models(self, userId: int, aiProviderId: int):
List AI Provider Models
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.ai.providers.models.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.providers.models.getMany
"""
return self.requester.request(
method="get",
@@ -210,6 +216,21 @@ def list_ai_provider_models(self, userId: int, aiProviderId: int):
+ "/models",
)
+ def list_all_ai_provider_models(self, user_id: int):
+ """
+ List AI Provider Models
+
+ Lists the models of all AI providers available to the user.
+ To list the models of a single AI provider, use `list_ai_provider_models`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.providers.models.crowdin.getMany
+ """
+ return self.requester.request(
+ method="get",
+ path=f"users/{user_id}/ai/providers/models",
+ )
+
def create_ai_proxy_chat_completion(
self,
userId: int,
@@ -223,7 +244,7 @@ def create_ai_proxy_chat_completion(
Please refer to the documentation for the specific provider you use to determine the required payload format.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.ai.providers.chat.completions.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.ai.providers.chat.completions.post
"""
return self.requester.request(
method="post",
@@ -396,10 +417,13 @@ def get_ai_custom_placeholders_path(self, user_id: int, custom_placeholder_id: O
return f"users/{user_id}/ai/settings/custom-placeholders"
+ @deprecated("Use `list_ai_snippets` instead")
def list_ai_custom_placeholders(self, user_id: int):
"""
List AI Custom Placeholders
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `list_ai_snippets` instead.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.ai.prompt.custom.placeholders.getMany
"""
@@ -409,10 +433,13 @@ def list_ai_custom_placeholders(self, user_id: int):
path=self.get_ai_custom_placeholders_path(user_id)
)
+ @deprecated("Use `add_ai_snippet` instead")
def add_ai_custom_placeholder(self, user_id: int, body: AddAiCustomPlaceholderRequest):
"""
Add AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `add_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.settings.custom-placeholders.post
"""
@@ -423,10 +450,13 @@ def add_ai_custom_placeholder(self, user_id: int, body: AddAiCustomPlaceholderRe
request_data=body,
)
+ @deprecated("Use `get_ai_snippet` instead")
def get_ai_custom_placeholder(self, user_id: int, ai_custom_placeholder_id: int):
"""
Get AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `get_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.settings.custom-placeholders.get
"""
@@ -436,10 +466,13 @@ def get_ai_custom_placeholder(self, user_id: int, ai_custom_placeholder_id: int)
path=self.get_ai_custom_placeholders_path(user_id, ai_custom_placeholder_id),
)
+ @deprecated("Use `delete_ai_snippet` instead")
def delete_ai_custom_placeholder(self, user_id: int, ai_custom_placeholder_id: int):
"""
Delete AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `delete_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.settings.custom-placeholders.delete
"""
@@ -449,6 +482,7 @@ def delete_ai_custom_placeholder(self, user_id: int, ai_custom_placeholder_id: i
path=self.get_ai_custom_placeholders_path(user_id, ai_custom_placeholder_id),
)
+ @deprecated("Use `edit_ai_snippet` instead")
def edit_ai_custom_placeholder(
self,
user_id: int,
@@ -458,6 +492,8 @@ def edit_ai_custom_placeholder(
"""
Edit AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `edit_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.settings.custom-placeholders.patch
"""
@@ -468,6 +504,84 @@ def edit_ai_custom_placeholder(
request_data=patches,
)
+ def get_ai_snippets_path(self, user_id: int, ai_snippet_id: Optional[int] = None):
+ if ai_snippet_id is not None:
+ return f"users/{user_id}/ai/settings/snippets/{ai_snippet_id}"
+ return f"users/{user_id}/ai/settings/snippets"
+
+ def list_ai_snippets(
+ self,
+ user_id: int,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List AI Snippets
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.prompts.snippets.getMany
+ """
+ return self._get_entire_data(
+ method="get",
+ path=self.get_ai_snippets_path(user_id),
+ params=self.get_page_params(limit=limit, offset=offset),
+ )
+
+ def add_ai_snippet(self, user_id: int, request_data: AddAiSnippetRequest):
+ """
+ Add AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.prompts.snippets.post
+ """
+ return self.requester.request(
+ method="post",
+ path=self.get_ai_snippets_path(user_id),
+ request_data=request_data,
+ )
+
+ def get_ai_snippet(self, user_id: int, ai_snippet_id: int):
+ """
+ Get AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.prompts.snippets.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_snippets_path(user_id, ai_snippet_id=ai_snippet_id),
+ )
+
+ def delete_ai_snippet(self, user_id: int, ai_snippet_id: int):
+ """
+ Delete AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.prompts.snippets.delete
+ """
+ return self.requester.request(
+ method="delete",
+ path=self.get_ai_snippets_path(user_id, ai_snippet_id=ai_snippet_id),
+ )
+
+ def edit_ai_snippet(
+ self,
+ user_id: int,
+ ai_snippet_id: int,
+ patches: Iterable[EditAiSnippetPatch],
+ ):
+ """
+ Edit AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.prompts.snippets.patch
+ """
+ return self.requester.request(
+ method="patch",
+ path=self.get_ai_snippets_path(user_id, ai_snippet_id=ai_snippet_id),
+ request_data=patches,
+ )
+
def clone_ai_prompt(
self,
user_id: int,
@@ -656,7 +770,7 @@ def list_ai_request_logs(
List AI Request Logs
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.requestLogs.getMany
"""
params = {
@@ -683,6 +797,86 @@ def list_ai_request_logs(
params=params,
)
+ def export_ai_request_logs(
+ self,
+ user_id: int,
+ export_format: Optional[AiRequestLogExportFormat] = None,
+ request_id: Optional[str] = None,
+ project_id: Optional[int] = None,
+ request_user_id: Optional[int] = None,
+ ai_provider_id: Optional[int] = None,
+ model: Optional[str] = None,
+ source_action: Optional[AiRequestLogSourceAction] = None,
+ prompt_action: Optional[str] = None,
+ statuses: Optional[Iterable[AiRequestLogStatus]] = None,
+ system_credentials: Optional[bool] = None,
+ is_auto_triggered: Optional[bool] = None,
+ token_name: Optional[str] = None,
+ oauth_client_id: Optional[str] = None,
+ created_after: Optional[datetime] = None,
+ created_before: Optional[datetime] = None,
+ ):
+ """
+ Export AI Request Logs
+
+ Starts an asynchronous export of the AI request logs matching the filters.
+ Poll `check_ai_request_logs_export_status` until the status is `finished`,
+ then call `download_ai_request_logs_export`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.requestLogs.exports.post
+ """
+
+ request_data = {
+ "format": export_format,
+ "requestId": request_id,
+ "projectId": project_id,
+ "userId": request_user_id,
+ "aiProviderId": ai_provider_id,
+ "model": model,
+ "sourceAction": source_action,
+ "promptAction": prompt_action,
+ "statuses": statuses,
+ "systemCredentials": system_credentials,
+ "isAutoTriggered": is_auto_triggered,
+ "tokenName": token_name,
+ "oauthClientId": oauth_client_id,
+ "createdAfter": created_after,
+ "createdBefore": created_before,
+ }
+
+ return self.requester.request(
+ method="post",
+ path=self.get_ai_request_logs_path(user_id) + "/exports",
+ request_data=request_data,
+ )
+
+ def check_ai_request_logs_export_status(self, user_id: int, export_id: str):
+ """
+ Check AI Request Logs Export Status
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.requestLogs.exports.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_request_logs_path(user_id) + f"/exports/{export_id}",
+ )
+
+ def download_ai_request_logs_export(self, user_id: int, export_id: str):
+ """
+ Download AI Request Logs Export
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.requestLogs.exports.download
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_request_logs_path(user_id) + f"/exports/{export_id}/download",
+ )
+
def get_ai_settings_path(self, user_id: int):
return f"users/{user_id}/ai/settings"
@@ -720,6 +914,68 @@ def edit_ai_settings(
request_data=patches,
)
+ def get_ai_usage_members_path(self, user_id: int, member_id: Optional[int] = None):
+ if member_id is not None:
+ return f"users/{user_id}/ai/usage/members/{member_id}"
+ return f"users/{user_id}/ai/usage/members"
+
+ def list_ai_usage_members(
+ self,
+ user_id: int,
+ user_ids: Optional[Iterable[int]] = None,
+ order_by: Optional[Sorting] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List AI Usage Members
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.usage.members.getMany
+ """
+
+ params = {
+ "userIds": convert_to_query_string(user_ids),
+ "orderBy": order_by,
+ }
+ params.update(self.get_page_params(limit=limit, offset=offset))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_ai_usage_members_path(user_id),
+ params=params,
+ )
+
+ def get_ai_usage_member(self, user_id: int, member_id: int):
+ """
+ Get AI Usage Member
+
+ `member_id` is the identifier of the project member (or your own user id for own usage).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.ai.usage.members.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_usage_members_path(user_id, member_id=member_id),
+ )
+
+ def get_project_ai_settings(self, projectId: Optional[int] = None):
+ """
+ Get Project AI Settings
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.ai.settings.get
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=f"projects/{projectId}/ai/settings",
+ )
+
def list_supported_ai_provider_models(
self,
user_id: int,
@@ -957,7 +1213,7 @@ class EnterpriseAIResource(BaseResource):
Enterprise Resource for AI.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/AI
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI
"""
def get_ai_path(self, aiPromptId: Optional[int] = None):
@@ -981,7 +1237,7 @@ def list_ai_prompts(
List AI Prompts
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.getMany
"""
params = {"projectId": projectId, "action": action}
params.update(self.get_page_params(limit=limit, offset=offset))
@@ -995,7 +1251,7 @@ def add_ai_prompt(self, request_data: AddAIPromptRequestScheme):
Add AI Prompt
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.post
"""
return self.requester.request(
@@ -1009,7 +1265,7 @@ def get_ai_prompt(self, aiPromptId: int):
Get AI Prompt
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.get
"""
return self.requester.request(
@@ -1022,7 +1278,7 @@ def delete_ai_prompt(self, aiPromptId: int):
Delete AI Prompt
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.delete
"""
return self.requester.request(
@@ -1037,7 +1293,7 @@ def edit_ai_prompt(
Edit AI Prompt
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.prompts.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.patch
"""
return self.requester.request(
@@ -1055,7 +1311,7 @@ def list_ai_providers(
List AI Providers
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.getMany
"""
params = self.get_page_params(limit=limit, offset=offset)
return self.requester.request(
@@ -1067,7 +1323,7 @@ def add_ai_provider(self, request_data: AddAIProviderReqeustScheme):
Add AI Provider
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.post
"""
return self.requester.request(
method="post",
@@ -1080,7 +1336,7 @@ def get_ai_provider(self, aiProviderId: int):
Get AI Provider
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.get
"""
return self.requester.request(
method="get",
@@ -1092,7 +1348,7 @@ def delete_ai_provider(self, aiProviderId: int):
Delete AI Provider
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.delete
"""
return self.requester.request(
method="delete",
@@ -1106,7 +1362,7 @@ def edit_ai_provider(
Edit AI Provider
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.patch
"""
return self.requester.request(
method="patch",
@@ -1119,13 +1375,28 @@ def list_ai_provider_models(self, aiProviderId: int):
List AI Provider Models
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.ai.providers.models.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.models.getMany
"""
return self.requester.request(
method="get",
path=self.get_ai_provider_path(aiProviderId=aiProviderId) + "/models",
)
+ def list_all_ai_provider_models(self):
+ """
+ List AI Provider Models
+
+ Lists the models of all AI providers available in the organization.
+ To list the models of a single AI provider, use `list_ai_provider_models`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.models.enterprise.getMany
+ """
+ return self.requester.request(
+ method="get",
+ path="ai/providers/models",
+ )
+
def create_ai_proxy_chat_completion(
self,
aiProviderId: int,
@@ -1138,7 +1409,7 @@ def create_ai_proxy_chat_completion(
Please refer to the documentation for the specific provider you use to determine the required payload format.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.providers.chat.completions.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.providers.chat.completions.post
"""
return self.requester.request(
method="post",
@@ -1152,10 +1423,13 @@ def get_ai_custom_placeholders_path(self, ai_custom_placeholder_id: Optional[int
return f"ai/settings/custom-placeholders/{ai_custom_placeholder_id}"
return "ai/settings/custom-placeholders"
+ @deprecated("Use `list_ai_snippets` instead")
def list_ai_custom_placeholders(self):
"""
List AI Custom Placeholders
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `list_ai_snippets` instead.
+
Link to documentation:
https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.prompts.custom.placeholders.getMany
"""
@@ -1165,10 +1439,13 @@ def list_ai_custom_placeholders(self):
path=self.get_ai_custom_placeholders_path()
)
+ @deprecated("Use `add_ai_snippet` instead")
def add_ai_custom_placeholder(self, body: AddAiCustomPlaceholderRequest):
"""
Add AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `add_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.settings.custom-placeholders.post
"""
@@ -1179,10 +1456,13 @@ def add_ai_custom_placeholder(self, body: AddAiCustomPlaceholderRequest):
request_data=body,
)
+ @deprecated("Use `get_ai_snippet` instead")
def get_ai_custom_placeholder(self, ai_custom_placeholder_id: int):
"""
Get AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `get_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.settings.custom-placeholders.get
"""
@@ -1192,10 +1472,13 @@ def get_ai_custom_placeholder(self, ai_custom_placeholder_id: int):
path=self.get_ai_custom_placeholders_path(ai_custom_placeholder_id),
)
+ @deprecated("Use `delete_ai_snippet` instead")
def delete_ai_custom_placeholder(self, ai_custom_placeholder_id: int):
"""
Delete AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `delete_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.settings.custom-placeholders.delete
"""
@@ -1205,6 +1488,7 @@ def delete_ai_custom_placeholder(self, ai_custom_placeholder_id: int):
path=self.get_ai_custom_placeholders_path(ai_custom_placeholder_id),
)
+ @deprecated("Use `edit_ai_snippet` instead")
def edit_ai_custom_placeholder(
self,
ai_custom_placeholder_id: int,
@@ -1213,6 +1497,8 @@ def edit_ai_custom_placeholder(
"""
Edit AI Custom Placeholder
+ Deprecated: AI Custom Placeholders were replaced by AI Snippets, use `edit_ai_snippet` instead.
+
Link to documentation:
https://support.crowdin.com/developer/enterprise/api/v2/#tag/AI/operation/api.ai.settings.custom-placeholders.patch
"""
@@ -1223,6 +1509,82 @@ def edit_ai_custom_placeholder(
request_data=patches,
)
+ def get_ai_snippets_path(self, ai_snippet_id: Optional[int] = None):
+ if ai_snippet_id is not None:
+ return f"ai/settings/snippets/{ai_snippet_id}"
+ return "ai/settings/snippets"
+
+ def list_ai_snippets(
+ self,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List AI Snippets
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.snippets.getMany
+ """
+ return self._get_entire_data(
+ method="get",
+ path=self.get_ai_snippets_path(),
+ params=self.get_page_params(limit=limit, offset=offset),
+ )
+
+ def add_ai_snippet(self, request_data: AddAiSnippetRequest):
+ """
+ Add AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.snippets.post
+ """
+ return self.requester.request(
+ method="post",
+ path=self.get_ai_snippets_path(),
+ request_data=request_data,
+ )
+
+ def get_ai_snippet(self, ai_snippet_id: int):
+ """
+ Get AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.snippets.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_snippets_path(ai_snippet_id=ai_snippet_id),
+ )
+
+ def delete_ai_snippet(self, ai_snippet_id: int):
+ """
+ Delete AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.snippets.delete
+ """
+ return self.requester.request(
+ method="delete",
+ path=self.get_ai_snippets_path(ai_snippet_id=ai_snippet_id),
+ )
+
+ def edit_ai_snippet(
+ self,
+ ai_snippet_id: int,
+ patches: Iterable[EditAiSnippetPatch],
+ ):
+ """
+ Edit AI Snippet
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.prompts.snippets.patch
+ """
+ return self.requester.request(
+ method="patch",
+ path=self.get_ai_snippets_path(ai_snippet_id=ai_snippet_id),
+ request_data=patches,
+ )
+
def get_ai_prompt_fine_tuning_datasets_path(
self,
ai_prompt_id: int,
@@ -1550,7 +1912,7 @@ def list_ai_request_logs(
List AI Request Logs
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.requestLogs.getMany
"""
params = {
@@ -1577,6 +1939,85 @@ def list_ai_request_logs(
params=params,
)
+ def export_ai_request_logs(
+ self,
+ export_format: Optional[AiRequestLogExportFormat] = None,
+ request_id: Optional[str] = None,
+ project_id: Optional[int] = None,
+ request_user_id: Optional[int] = None,
+ ai_provider_id: Optional[int] = None,
+ model: Optional[str] = None,
+ source_action: Optional[AiRequestLogSourceAction] = None,
+ prompt_action: Optional[str] = None,
+ statuses: Optional[Iterable[AiRequestLogStatus]] = None,
+ system_credentials: Optional[bool] = None,
+ is_auto_triggered: Optional[bool] = None,
+ token_name: Optional[str] = None,
+ oauth_client_id: Optional[str] = None,
+ created_after: Optional[datetime] = None,
+ created_before: Optional[datetime] = None,
+ ):
+ """
+ Export AI Request Logs
+
+ Starts an asynchronous export of the AI request logs matching the filters.
+ Poll `check_ai_request_logs_export_status` until the status is `finished`,
+ then call `download_ai_request_logs_export`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.requestLogs.exports.post
+ """
+
+ request_data = {
+ "format": export_format,
+ "requestId": request_id,
+ "projectId": project_id,
+ "userId": request_user_id,
+ "aiProviderId": ai_provider_id,
+ "model": model,
+ "sourceAction": source_action,
+ "promptAction": prompt_action,
+ "statuses": statuses,
+ "systemCredentials": system_credentials,
+ "isAutoTriggered": is_auto_triggered,
+ "tokenName": token_name,
+ "oauthClientId": oauth_client_id,
+ "createdAfter": created_after,
+ "createdBefore": created_before,
+ }
+
+ return self.requester.request(
+ method="post",
+ path=self.get_ai_request_logs_path() + "/exports",
+ request_data=request_data,
+ )
+
+ def check_ai_request_logs_export_status(self, export_id: str):
+ """
+ Check AI Request Logs Export Status
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.requestLogs.exports.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_request_logs_path() + f"/exports/{export_id}",
+ )
+
+ def download_ai_request_logs_export(self, export_id: str):
+ """
+ Download AI Request Logs Export
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.requestLogs.exports.download
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_request_logs_path() + f"/exports/{export_id}/download",
+ )
+
def get_ai_settings(self):
"""
Get AI Settings
@@ -1607,6 +2048,67 @@ def edit_ai_settings(
request_data=patches,
)
+ def get_ai_usage_members_path(self, member_id: Optional[int] = None):
+ if member_id is not None:
+ return f"ai/usage/members/{member_id}"
+ return "ai/usage/members"
+
+ def list_ai_usage_members(
+ self,
+ user_ids: Optional[Iterable[int]] = None,
+ order_by: Optional[Sorting] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List AI Usage Members
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.usage.members.getMany
+ """
+
+ params = {
+ "userIds": convert_to_query_string(user_ids),
+ "orderBy": order_by,
+ }
+ params.update(self.get_page_params(limit=limit, offset=offset))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_ai_usage_members_path(),
+ params=params,
+ )
+
+ def get_ai_usage_member(self, member_id: int):
+ """
+ Get AI Usage Member
+
+ `member_id` is the identifier of the organization user.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.ai.usage.members.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_ai_usage_members_path(member_id=member_id),
+ )
+
+ def get_project_ai_settings(self, projectId: Optional[int] = None):
+ """
+ Get Project AI Settings
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.ai.settings.get
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=f"projects/{projectId}/ai/settings",
+ )
+
def list_supported_ai_provider_models(
self,
limit: Optional[int] = None,
diff --git a/crowdin_api/api_resources/ai/tests/test_ai_resources.py b/crowdin_api/api_resources/ai/tests/test_ai_resources.py
index 5c467b82..71978252 100644
--- a/crowdin_api/api_resources/ai/tests/test_ai_resources.py
+++ b/crowdin_api/api_resources/ai/tests/test_ai_resources.py
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
+from enum import Enum
from unittest import mock
import pytest
@@ -10,9 +11,11 @@
AIProviderType,
AiReportFormat,
AiToolType,
+ AiRequestLogExportFormat,
DatasetPurpose,
EditAiCustomPlaceholderPatchPath,
EditAiSettingsPatchPath,
+ EditAiSnippetPatchPath,
ListAiPromptFineTuningJobsOrderBy,
ListSupportedAiModelsOrderBy,
)
@@ -42,6 +45,13 @@
from crowdin_api.sorting import Sorting, SortingOrder, SortingRule
+class _UsageMembersOrderBy(Enum):
+ MONTHLY_COST_SPENT = "monthlyCostSpent"
+
+
+ORDER_BY = Sorting([SortingRule(_UsageMembersOrderBy.MONTHLY_COST_SPENT, SortingOrder.DESC)])
+
+
class TestAIResources:
resource_class = AIResource
@@ -742,7 +752,8 @@ def test_list_ai_custom_placeholders(self, m_request, base_absolut_url):
user_id = 1
resource = self.get_resource(base_absolut_url)
- assert resource.list_ai_custom_placeholders(user_id) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.list_ai_custom_placeholders(user_id) == "response"
m_request.assert_called_once_with(
method="get",
@@ -773,7 +784,8 @@ def test_add_ai_custom_placeholder(self, m_request, incoming_data, request_param
user_id = 1
resource = self.get_resource(base_absolut_url)
- assert resource.add_ai_custom_placeholder(user_id, incoming_data) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.add_ai_custom_placeholder(user_id, incoming_data) == "response"
m_request.assert_called_once_with(
method="post",
@@ -789,7 +801,8 @@ def test_get_ai_custom_placeholder(self, m_request, base_absolut_url):
ai_custom_placeholder_id = 2
resource = self.get_resource(base_absolut_url)
- assert resource.get_ai_custom_placeholder(user_id, ai_custom_placeholder_id) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.get_ai_custom_placeholder(user_id, ai_custom_placeholder_id) == "response"
m_request.assert_called_once_with(
method="get",
@@ -804,7 +817,8 @@ def test_delete_ai_custom_placeholder(self, m_request, base_absolut_url):
ai_custom_placeholder_id = 2
resource = self.get_resource(base_absolut_url)
- assert resource.delete_ai_custom_placeholder(user_id, ai_custom_placeholder_id) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.delete_ai_custom_placeholder(user_id, ai_custom_placeholder_id) == "response"
m_request.assert_called_once_with(
method="delete",
@@ -850,7 +864,8 @@ def test_edit_ai_custom_placeholder(self, m_request, incoming_data, request_para
ai_custom_placeholder_id = 2
resource = self.get_resource(base_absolut_url)
- assert resource.edit_ai_custom_placeholder(user_id, ai_custom_placeholder_id, incoming_data) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.edit_ai_custom_placeholder(user_id, ai_custom_placeholder_id, incoming_data) == "response"
m_request.assert_called_once_with(
method="patch",
@@ -1251,6 +1266,40 @@ def test_get_ai_settings(self, m_request, base_absolut_url):
}
],
),
+ (
+ [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSettingsPatchPath.MONTHLY_COST_LIMIT,
+ "value": 100,
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSettingsPatchPath.PER_USER_OVERRIDE_DAILY_COST_LIMIT.value.format(userId=12),
+ "value": 5,
+ },
+ {
+ "op": PatchOperation.REMOVE,
+ "path": EditAiSettingsPatchPath.PER_USER_OVERRIDE.value.format(userId=13),
+ },
+ ],
+ [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSettingsPatchPath.MONTHLY_COST_LIMIT,
+ "value": 100,
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": "/perUserOverrides/12/dailyCostLimit",
+ "value": 5,
+ },
+ {
+ "op": PatchOperation.REMOVE,
+ "path": "/perUserOverrides/13",
+ },
+ ],
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -1656,6 +1705,268 @@ def test_edit_ai_provider_gateway_no_body(self, m_request, base_absolut_url):
request_data=None,
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_all_ai_provider_models(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_all_ai_provider_models(1) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/providers/models",
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"user_id": 1}, "users/1/ai/settings/snippets"),
+ ({"user_id": 1, "ai_snippet_id": 2}, "users/1/ai/settings/snippets/2"),
+ ),
+ )
+ def test_get_ai_snippets_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_snippets_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"limit": 25, "offset": 0}),
+ ({"limit": 10, "offset": 5}, {"limit": 10, "offset": 5}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_ai_snippets(self, m_request, in_params, request_params, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_ai_snippets(1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/settings/snippets",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ request_data = {
+ "description": "Product description",
+ "placeholder": "%custom:productDescription%",
+ "value": "The product is the professional consulting service",
+ }
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_ai_snippet(1, request_data) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="users/1/ai/settings/snippets",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_snippet(1, 2) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/settings/snippets/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_ai_snippet(1, 2) == "response"
+ m_request.assert_called_once_with(
+ method="delete",
+ path="users/1/ai/settings/snippets/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ patches = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSnippetPatchPath.VALUE,
+ "value": "New value",
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_ai_snippet(1, 2, patches) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="users/1/ai/settings/snippets/2",
+ request_data=patches,
+ )
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_data",
+ (
+ (
+ {},
+ {
+ "format": None,
+ "requestId": None,
+ "projectId": None,
+ "userId": None,
+ "aiProviderId": None,
+ "model": None,
+ "sourceAction": None,
+ "promptAction": None,
+ "statuses": None,
+ "systemCredentials": None,
+ "isAutoTriggered": None,
+ "tokenName": None,
+ "oauthClientId": None,
+ "createdAfter": None,
+ "createdBefore": None,
+ },
+ ),
+ (
+ {
+ "export_format": AiRequestLogExportFormat.CSV,
+ "request_id": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e",
+ "project_id": 8,
+ "request_user_id": 42,
+ "ai_provider_id": 3,
+ "model": "gpt-5.6-sol",
+ "source_action": AiRequestLogSourceAction.AI_GATEWAY,
+ "prompt_action": "qa_check",
+ "statuses": [AiRequestLogStatus.SUCCESS, AiRequestLogStatus.ERROR],
+ "system_credentials": True,
+ "is_auto_triggered": False,
+ "token_name": "Token name",
+ "oauth_client_id": "gpbccUFxAKZDrLm5Nq8t",
+ "created_after": datetime(2026, 7, 1, tzinfo=timezone.utc),
+ "created_before": datetime(2026, 7, 31, tzinfo=timezone.utc),
+ },
+ {
+ "format": AiRequestLogExportFormat.CSV,
+ "requestId": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e",
+ "projectId": 8,
+ "userId": 42,
+ "aiProviderId": 3,
+ "model": "gpt-5.6-sol",
+ "sourceAction": AiRequestLogSourceAction.AI_GATEWAY,
+ "promptAction": "qa_check",
+ "statuses": [AiRequestLogStatus.SUCCESS, AiRequestLogStatus.ERROR],
+ "systemCredentials": True,
+ "isAutoTriggered": False,
+ "tokenName": "Token name",
+ "oauthClientId": "gpbccUFxAKZDrLm5Nq8t",
+ "createdAfter": datetime(2026, 7, 1, tzinfo=timezone.utc),
+ "createdBefore": datetime(2026, 7, 31, tzinfo=timezone.utc),
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_export_ai_request_logs(self, m_request, incoming_data, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.export_ai_request_logs(1, **incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="users/1/ai/request-logs/exports",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_ai_request_logs_export_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.check_ai_request_logs_export_status(1, "abc") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/request-logs/exports/abc",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_download_ai_request_logs_export(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.download_ai_request_logs_export(1, "abc") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/request-logs/exports/abc/download",
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"user_id": 1}, "users/1/ai/usage/members"),
+ ({"user_id": 1, "member_id": 2}, "users/1/ai/usage/members/2"),
+ ),
+ )
+ def test_get_ai_usage_members_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_usage_members_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"userIds": None, "orderBy": None, "limit": 25, "offset": 0}),
+ (
+ {"user_ids": [1, 2], "order_by": ORDER_BY, "limit": 10, "offset": 5},
+ {"userIds": "1,2", "orderBy": ORDER_BY, "limit": 10, "offset": 5},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_ai_usage_members(self, m_request, in_params, request_params, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_ai_usage_members(1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/usage/members",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_ai_usage_member(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_usage_member(1, 2) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/ai/usage/members/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_project_ai_settings(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_project_ai_settings(projectId=5) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/5/ai/settings",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_project_ai_settings_default_project(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.resource_class(
+ requester=APIRequester(base_url=base_absolut_url), project_id=7
+ )
+ assert resource.get_project_ai_settings() == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/7/ai/settings",
+ )
+
class TestEnterpriseAIResources:
resource_class = EnterpriseAIResource
@@ -2302,7 +2613,8 @@ def test_list_ai_custom_placeholders(self, m_request, base_absolut_url):
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
- assert resource.list_ai_custom_placeholders() == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.list_ai_custom_placeholders() == "response"
m_request.assert_called_once_with(
method="get",
@@ -2331,7 +2643,8 @@ def test_add_ai_custom_placeholder(self, m_request, incoming_data, request_param
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
- assert resource.add_ai_custom_placeholder(incoming_data) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.add_ai_custom_placeholder(incoming_data) == "response"
m_request.assert_called_once_with(
method="post",
@@ -2346,7 +2659,8 @@ def test_get_ai_custom_placeholder(self, m_request, base_absolut_url):
ai_custom_placeholder_id = 1
resource = self.get_resource(base_absolut_url)
- assert resource.get_ai_custom_placeholder(ai_custom_placeholder_id) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.get_ai_custom_placeholder(ai_custom_placeholder_id) == "response"
m_request.assert_called_once_with(
method="get",
@@ -2360,7 +2674,8 @@ def test_delete_ai_custom_placeholder(self, m_request, base_absolut_url):
ai_custom_placeholder_id = 1
resource = self.get_resource(base_absolut_url)
- assert resource.delete_ai_custom_placeholder(ai_custom_placeholder_id) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.delete_ai_custom_placeholder(ai_custom_placeholder_id) == "response"
m_request.assert_called_once_with(
method="delete",
@@ -2405,7 +2720,8 @@ def test_edit_ai_custom_placeholder(self, m_request, incoming_data, request_para
ai_custom_placeholder_id = 1
resource = self.get_resource(base_absolut_url)
- assert resource.edit_ai_custom_placeholder(ai_custom_placeholder_id, incoming_data) == "response"
+ with pytest.warns(DeprecationWarning):
+ assert resource.edit_ai_custom_placeholder(ai_custom_placeholder_id, incoming_data) == "response"
m_request.assert_called_once_with(
method="patch",
@@ -2794,6 +3110,40 @@ def test_get_ai_settings(self, m_request, base_absolut_url):
}
],
),
+ (
+ [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSettingsPatchPath.MONTHLY_COST_LIMIT,
+ "value": 100,
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSettingsPatchPath.PER_USER_OVERRIDE_DAILY_COST_LIMIT.value.format(userId=12),
+ "value": 5,
+ },
+ {
+ "op": PatchOperation.REMOVE,
+ "path": EditAiSettingsPatchPath.PER_USER_OVERRIDE.value.format(userId=13),
+ },
+ ],
+ [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSettingsPatchPath.MONTHLY_COST_LIMIT,
+ "value": 100,
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": "/perUserOverrides/12/dailyCostLimit",
+ "value": 5,
+ },
+ {
+ "op": PatchOperation.REMOVE,
+ "path": "/perUserOverrides/13",
+ },
+ ],
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -3175,3 +3525,265 @@ def test_edit_ai_provider_gateway_no_body(self, m_request, base_absolut_url):
path=resource.get_ai_provider_gateway_path(ai_provider_id, path),
request_data=None,
)
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_all_ai_provider_models(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_all_ai_provider_models() == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/providers/models",
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({}, "ai/settings/snippets"),
+ ({"ai_snippet_id": 2}, "ai/settings/snippets/2"),
+ ),
+ )
+ def test_get_ai_snippets_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_snippets_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"limit": 25, "offset": 0}),
+ ({"limit": 10, "offset": 5}, {"limit": 10, "offset": 5}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_ai_snippets(self, m_request, in_params, request_params, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_ai_snippets(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/settings/snippets",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ request_data = {
+ "description": "Product description",
+ "placeholder": "%custom:productDescription%",
+ "value": "The product is the professional consulting service",
+ }
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_ai_snippet(request_data) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="ai/settings/snippets",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_snippet(2) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/settings/snippets/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_ai_snippet(2) == "response"
+ m_request.assert_called_once_with(
+ method="delete",
+ path="ai/settings/snippets/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_ai_snippet(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ patches = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditAiSnippetPatchPath.VALUE,
+ "value": "New value",
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_ai_snippet(2, patches) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="ai/settings/snippets/2",
+ request_data=patches,
+ )
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_data",
+ (
+ (
+ {},
+ {
+ "format": None,
+ "requestId": None,
+ "projectId": None,
+ "userId": None,
+ "aiProviderId": None,
+ "model": None,
+ "sourceAction": None,
+ "promptAction": None,
+ "statuses": None,
+ "systemCredentials": None,
+ "isAutoTriggered": None,
+ "tokenName": None,
+ "oauthClientId": None,
+ "createdAfter": None,
+ "createdBefore": None,
+ },
+ ),
+ (
+ {
+ "export_format": AiRequestLogExportFormat.CSV,
+ "request_id": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e",
+ "project_id": 8,
+ "request_user_id": 42,
+ "ai_provider_id": 3,
+ "model": "gpt-5.6-sol",
+ "source_action": AiRequestLogSourceAction.AI_GATEWAY,
+ "prompt_action": "qa_check",
+ "statuses": [AiRequestLogStatus.SUCCESS, AiRequestLogStatus.ERROR],
+ "system_credentials": True,
+ "is_auto_triggered": False,
+ "token_name": "Token name",
+ "oauth_client_id": "gpbccUFxAKZDrLm5Nq8t",
+ "created_after": datetime(2026, 7, 1, tzinfo=timezone.utc),
+ "created_before": datetime(2026, 7, 31, tzinfo=timezone.utc),
+ },
+ {
+ "format": AiRequestLogExportFormat.CSV,
+ "requestId": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e",
+ "projectId": 8,
+ "userId": 42,
+ "aiProviderId": 3,
+ "model": "gpt-5.6-sol",
+ "sourceAction": AiRequestLogSourceAction.AI_GATEWAY,
+ "promptAction": "qa_check",
+ "statuses": [AiRequestLogStatus.SUCCESS, AiRequestLogStatus.ERROR],
+ "systemCredentials": True,
+ "isAutoTriggered": False,
+ "tokenName": "Token name",
+ "oauthClientId": "gpbccUFxAKZDrLm5Nq8t",
+ "createdAfter": datetime(2026, 7, 1, tzinfo=timezone.utc),
+ "createdBefore": datetime(2026, 7, 31, tzinfo=timezone.utc),
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_export_ai_request_logs(self, m_request, incoming_data, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.export_ai_request_logs(**incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="ai/request-logs/exports",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_ai_request_logs_export_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.check_ai_request_logs_export_status("abc") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/request-logs/exports/abc",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_download_ai_request_logs_export(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.download_ai_request_logs_export("abc") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/request-logs/exports/abc/download",
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({}, "ai/usage/members"),
+ ({"member_id": 2}, "ai/usage/members/2"),
+ ),
+ )
+ def test_get_ai_usage_members_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_usage_members_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"userIds": None, "orderBy": None, "limit": 25, "offset": 0}),
+ (
+ {"user_ids": [1, 2], "order_by": ORDER_BY, "limit": 10, "offset": 5},
+ {"userIds": "1,2", "orderBy": ORDER_BY, "limit": 10, "offset": 5},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_ai_usage_members(self, m_request, in_params, request_params, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_ai_usage_members(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/usage/members",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_ai_usage_member(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_ai_usage_member(2) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="ai/usage/members/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_project_ai_settings(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_project_ai_settings(projectId=5) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/5/ai/settings",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_project_ai_settings_default_project(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.resource_class(
+ requester=APIRequester(base_url=base_absolut_url), project_id=7
+ )
+ assert resource.get_project_ai_settings() == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/7/ai/settings",
+ )
diff --git a/crowdin_api/api_resources/ai/types.py b/crowdin_api/api_resources/ai/types.py
index d155ba31..c5db8a58 100644
--- a/crowdin_api/api_resources/ai/types.py
+++ b/crowdin_api/api_resources/ai/types.py
@@ -6,6 +6,7 @@
AIProviderType,
AiToolType,
EditAiCustomPlaceholderPatchPath,
+ EditAiSnippetPatchPath,
EditAIPromptPath,
EditAIProviderPath,
EditAiSettingsPatchPath,
@@ -16,38 +17,64 @@
class OtherLanguageTranslation(TypedDict):
isEnabled: Optional[bool]
- languageIds: Optional[Iterable[int]]
+ # Language identifiers, e.g. "uk", "de".
+ languageIds: Optional[Iterable[Union[str, int]]]
class BasicModePreTranslateActionConfig(TypedDict):
mode: str
- companyDescription: Optional[bool]
- projectDescription: Optional[bool]
- audienceDescription: Optional[bool]
+ # Deprecated.
+ companyDescription: Optional[Union[str, bool]]
+ # Deprecated.
+ projectDescription: Optional[Union[str, bool]]
+ # Deprecated.
+ audienceDescription: Optional[Union[str, bool]]
+ # Deprecated: use `snippets` instead.
+ customPlaceholders: Optional[Iterable[str]]
+ snippets: Optional[Iterable[str]]
+ # Deprecated: misspelled key, use `otherLanguageTranslations` instead.
otherLanguageTranslation: Optional[OtherLanguageTranslation]
+ otherLanguageTranslations: Optional[OtherLanguageTranslation]
glossaryTerms: Optional[bool]
tmSuggestions: Optional[bool]
+ # Deprecated.
fileContent: Optional[bool]
fileContext: Optional[bool]
+ generateFileSummary: Optional[bool]
screenshots: Optional[bool]
# Deprecated: use `projectContext` instead.
publicProjectDescription: Optional[bool]
projectContext: Optional[bool]
+ # Crowdin Enterprise only.
organizationContext: Optional[bool]
+ siblingsStrings: Optional[bool]
+ retryOnQaIssues: Optional[bool]
class BasicModeAlignmentActionConfig(TypedDict):
mode: str
+ # Deprecated.
+ companyDescription: Optional[str]
+ # Deprecated.
+ projectDescription: Optional[str]
+ # Deprecated.
+ audienceDescription: Optional[str]
+ # Deprecated: use `snippets` instead.
customPlaceholders: Optional[Iterable[str]]
+ snippets: Optional[Iterable[str]]
# Deprecated: use `projectContext` instead.
publicProjectDescription: Optional[bool]
projectContext: Optional[bool]
+ # Crowdin Enterprise only.
organizationContext: Optional[bool]
class BasicModeQaCheckActionConfig(TypedDict):
mode: str
evaluationSteps: Iterable[str]
+ # Deprecated: use `snippets` instead.
+ customPlaceholders: Optional[Iterable[str]]
+ snippets: Optional[Iterable[str]]
glossaryTerms: Optional[bool]
tmSuggestions: Optional[bool]
fileContext: Optional[bool]
@@ -55,20 +82,29 @@ class BasicModeQaCheckActionConfig(TypedDict):
# Deprecated: use `projectContext` instead.
publicProjectDescription: Optional[bool]
projectContext: Optional[bool]
+ # Crowdin Enterprise only.
organizationContext: Optional[bool]
class AdvancedModeConfig(TypedDict):
mode: str
+ generateFileSummary: Optional[bool]
+ glossaryTerms: Optional[bool]
+ tmSuggestions: Optional[bool]
screenshots: Optional[bool]
prompt: str
+ otherLanguageTranslations: Optional[OtherLanguageTranslation]
+ retryOnQaIssues: Optional[bool]
class ExternalMode(TypedDict):
+ mode: str
+ # Deprecated: not part of the API schema.
name: str
identifier: str
key: str
options: Dict
+ retryOnQaIssues: Optional[bool]
class AddAIPromptRequestScheme(TypedDict):
@@ -76,6 +112,7 @@ class AddAIPromptRequestScheme(TypedDict):
action: AIPromptAction
aiProviderId: int
aiModelId: str
+ # Deprecated.
isEnabled: Optional[bool]
enabledProjectIds: Optional[Iterable[int]]
config: Union[
@@ -95,6 +132,9 @@ class EditAIPromptScheme(TypedDict):
class OpenAICredential(TypedDict):
apiKey: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
class AzureOpenAICredential(TypedDict):
@@ -102,20 +142,78 @@ class AzureOpenAICredential(TypedDict):
apiKey: str
deploymentName: str
apiVersion: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
class GoogleGeminiCredential(TypedDict):
+ """Google Gemini (Vertex AI) credentials."""
+
project: str
region: str
- serviceAccountKey: Dict
+ serviceAccountKey: Optional[Dict]
+ workloadIdentityFederationAudience: Optional[str]
+ serviceAccountEmail: Optional[str]
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
+
+
+class GoogleGeminiAIStudioCredential(TypedDict):
+ apiKey: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
class MistralAICredential(TypedDict):
apiKey: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
class AnthropicCredential(TypedDict):
apiKey: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
+
+
+class XAICredential(TypedDict):
+ apiKey: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
+
+
+class WatsonxCredential(TypedDict):
+ apiKey: str
+ projectId: str
+ region: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
+
+
+class DeepSeekCredential(TypedDict):
+ apiKey: str
+ baseUrl: Optional[str]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
+
+
+class MicrosoftFoundryDeployment(TypedDict):
+ deploymentName: str
+ targetUri: str
+ apiKey: str
+
+
+class MicrosoftFoundryCredential(TypedDict):
+ deployments: Iterable[MicrosoftFoundryDeployment]
+ headers: Optional[Dict[str, str]]
+ sendCustomHeadersOnly: Optional[bool]
class CustomAICredential(TypedDict):
@@ -125,7 +223,7 @@ class CustomAICredential(TypedDict):
class ActionRule(TypedDict):
action: AIPromptAction
- availableAiModelIds: Iterable[int]
+ availableAiModelIds: Iterable[Union[str, int]]
class ActionRules(TypedDict):
@@ -140,8 +238,13 @@ class AddAIProviderReqeustScheme(TypedDict):
OpenAICredential,
AzureOpenAICredential,
GoogleGeminiCredential,
+ GoogleGeminiAIStudioCredential,
MistralAICredential,
AnthropicCredential,
+ XAICredential,
+ WatsonxCredential,
+ DeepSeekCredential,
+ MicrosoftFoundryCredential,
CustomAICredential,
]
]
@@ -209,6 +312,18 @@ class CreateAIPromptFineTuningJobRequest(TypedDict):
validationOptions: Optional[ValidationOptions]
+class AddAiSnippetRequest(TypedDict):
+ description: str
+ placeholder: str
+ value: str
+
+
+class EditAiSnippetPatch(TypedDict):
+ op: PatchOperation
+ path: EditAiSnippetPatchPath
+ value: Any
+
+
class AddAiCustomPlaceholderRequest(TypedDict):
description: str
placeholder: str
diff --git a/crowdin_api/api_resources/application/enums.py b/crowdin_api/api_resources/application/enums.py
index 649e03e8..433bfc58 100644
--- a/crowdin_api/api_resources/application/enums.py
+++ b/crowdin_api/api_resources/application/enums.py
@@ -27,3 +27,28 @@ class ApplicationConsentStatus(Enum):
class ApplicationConsentPatchPath(Enum):
STATUS = "/status"
SCOPES = "/scopes"
+
+
+class ApplicationInstallationPatchPath(Enum):
+ PERMISSIONS = "/permissions"
+ MANIFEST = "/manifest"
+
+
+class ApplicationManifestEnvironment(Enum):
+ CROWDIN = "crowdin"
+ CROWDIN_ENTERPRISE = "crowdin-enterprise"
+
+
+class ApplicationBundleMode(Enum):
+ INTERNAL = "internal"
+ EXTERNAL = "external"
+
+
+class ApplicationKVRecordPatchPath(Enum):
+ VALUE = "/value"
+ TTL = "/ttl"
+
+
+class IntegrationSyncProvider(Enum):
+ CROWDIN = "crowdin"
+ INTEGRATION = "integration"
diff --git a/crowdin_api/api_resources/application/resource.py b/crowdin_api/api_resources/application/resource.py
index ef4a9625..c5e61b04 100644
--- a/crowdin_api/api_resources/application/resource.py
+++ b/crowdin_api/api_resources/application/resource.py
@@ -1,11 +1,19 @@
-from typing import Iterable, Optional
+from typing import Any, Dict, Iterable, Optional, Union
+from urllib.parse import quote
-from crowdin_api.parser import dumps
+from deprecated import deprecated
+
+from crowdin_api.api_resources.application.enums import IntegrationSyncProvider
from crowdin_api.api_resources.application.types import (
AddApplicationConsentRequest,
ApplicationConsentPatchRequest,
+ ApplicationKVRecordPatchRequest,
+ ApplicationManifest,
+ ApplicationModulePermissions,
ApplicationPermissions,
ApplicationInstallationPatchRequest,
+ IntegrationCrowdinUpdateFile,
+ IntegrationSyncSettingsFile,
)
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.sorting import Sorting
@@ -18,10 +26,10 @@ class ApplicationResource(BaseResource):
Use the API to manage the necessary app data.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Applications
+ https://support.crowdin.com/developer/api/v2/#tag/Applications
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Applications
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Applications
"""
def get_application_path(self, applicationIdentifier: str, path: str):
@@ -36,29 +44,61 @@ def list_application_installations(
self,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ installedBy: Optional[int] = None,
+ orderBy: Optional[Sorting] = None,
):
"""
List Application Installations
- Link to documentaion:
- https://developer.crowdin.com/api/v2/#operation/api.applications.installations.getMany
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.getMany
"""
- return self.requester.request(
+ params = {"installedBy": installedBy, "orderBy": orderBy}
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
+ return self._get_entire_data(
method="get",
path=self.get_application_installations_path(),
- params=self.get_page_params(offset=offset, limit=limit),
+ params=params,
)
def install_application(
- self, url: str, permissions: Optional[ApplicationPermissions] = None
+ self,
+ url: Optional[str] = None,
+ permissions: Optional[ApplicationPermissions] = None,
+ manifest: Optional[ApplicationManifest] = None,
+ modules: Optional[Iterable[ApplicationModulePermissions]] = None,
+ assignAgent: Optional[bool] = None,
):
"""
Install Application
+ Install an application either from a hosted manifest URL (`url`) or from its
+ manifest content (`manifest`, serverless apps only). Exactly one of `url` or
+ `manifest` must be provided.
+
+ `permissions.user` is deprecated by the API, use `modules` permissions instead.
+ `assignAgent` is supported only when installing from a manifest URL.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.installations.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.post
"""
- request_data = {"url": url, "permissions": permissions}
+ if (url is None) == (manifest is None):
+ raise ValueError("You must set either url or manifest.")
+
+ request_data = {
+ "url": url,
+ "manifest": manifest,
+ "permissions": permissions,
+ "modules": modules,
+ "assignAgent": assignAgent,
+ }
return self.requester.request(
method="post",
path=self.get_application_installations_path(),
@@ -70,7 +110,10 @@ def get_application_installation(self, identifier: str):
Get Application Installation
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.installations.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.get
"""
return self.requester.request(
method="get",
@@ -81,10 +124,13 @@ def delete_application_installation(
self, identifier: str, force: Optional[bool] = None
):
"""
- Delete Applcation Installation
+ Delete Application Installation
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.installations.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.delete
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.delete
"""
params = {"force": force}
@@ -94,11 +140,20 @@ def delete_application_installation(
params=params,
)
- def edit_applicatoin_installation(
+ def edit_application_installation(
self, identifier: str, data: Iterable[ApplicationInstallationPatchRequest]
):
"""
Edit Application Installation
+
+ Supported paths: `/permissions`, `/modules/{moduleKey}/permissions` and `/manifest`
+ (the latter only for applications installed from manifest content).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.patch
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.patch
"""
return self.requester.request(
method="patch",
@@ -106,6 +161,73 @@ def edit_applicatoin_installation(
request_data=data,
)
+ @deprecated("Use `edit_application_installation` instead")
+ def edit_applicatoin_installation(
+ self, identifier: str, data: Iterable[ApplicationInstallationPatchRequest]
+ ):
+ """
+ Edit Application Installation
+
+ Deprecated: use `edit_application_installation` instead.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.patch
+ """
+ return self.edit_application_installation(identifier=identifier, data=data)
+
+ def upload_application_bundle(self, identifier: str, storageId: int):
+ """
+ Upload Application Bundle
+
+ Upload a bundle archive (ZIP with a non-empty `app.js` at its root) for a
+ serverless app installed from manifest content.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.bundles.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.bundles.post
+ """
+ return self.requester.request(
+ method="post",
+ path=f"{self.get_application_installations_path(identifier=identifier)}/bundles",
+ request_data={"storageId": storageId},
+ )
+
+ def get_application_installation_update(self, identifier: str):
+ """
+ Get Application Installation Update
+
+ Returns the diff between the installed application and the latest cached manifest,
+ tagged with `manifestHash`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.update.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.update.get
+ """
+ return self.requester.request(
+ method="get",
+ path=f"{self.get_application_installations_path(identifier=identifier)}/update",
+ )
+
+ def apply_application_installation_update(self, identifier: str, manifestHash: str):
+ """
+ Apply Application Installation Update
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.installations.update.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.installations.update.post
+ """
+ return self.requester.request(
+ method="post",
+ path=f"{self.get_application_installations_path(identifier=identifier)}/update",
+ request_data={"manifestHash": manifestHash},
+ )
+
def get_application_consents_path(self, consent_id: Optional[int] = None):
if consent_id is not None:
return f"applications/consents/{consent_id}"
@@ -124,7 +246,7 @@ def list_application_consents(
Available for Crowdin.com only (not supported in Crowdin Enterprise).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.consents.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.consents.getMany
"""
params = {
"identifier": identifier,
@@ -145,7 +267,7 @@ def add_application_consent(self, request_data: AddApplicationConsentRequest):
Available for Crowdin.com only (not supported in Crowdin Enterprise).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.consents.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.consents.post
"""
return self.requester.request(
method="post",
@@ -164,7 +286,7 @@ def edit_application_consent(
Available for Crowdin.com only (not supported in Crowdin Enterprise).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.consents.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.consents.patch
"""
return self.requester.request(
method="patch",
@@ -179,95 +301,602 @@ def delete_application_consent(self, consent_id: int):
Available for Crowdin.com only (not supported in Crowdin Enterprise).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.consents.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.consents.delete
"""
return self.requester.request(
method="delete",
path=self.get_application_consents_path(consent_id=consent_id),
)
- def get_application_data(self, applicationIdentifier: str, path: str):
+ def get_application_kv_records_path(
+ self, applicationIdentifier: str, key: Optional[str] = None
+ ):
+ path = f"applications/{applicationIdentifier}/storage/kv/records"
+ if key is not None:
+ return f"{path}/{quote(key, safe='')}"
+ return path
+
+ def list_application_kv_records(
+ self,
+ applicationIdentifier: str,
+ prefix: Optional[str] = None,
+ orderBy: Optional[Sorting] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List Application KV Records
+
+ Requires the application's own access token (personal access tokens are not supported).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.storage.kv.records.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.storage.kv.records.getMany
+ """
+ params = {"prefix": prefix, "orderBy": orderBy}
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_application_kv_records_path(applicationIdentifier),
+ params=params,
+ )
+
+ def add_application_kv_record(
+ self,
+ applicationIdentifier: str,
+ key: str,
+ value: Any,
+ secret: Optional[bool] = None,
+ ttl: Optional[int] = None,
+ ):
+ """
+ Add Application KV Record
+
+ Requires the application's own access token (personal access tokens are not supported).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.storage.kv.records.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.storage.kv.records.post
+ """
+ return self.requester.request(
+ method="post",
+ path=self.get_application_kv_records_path(applicationIdentifier),
+ request_data={"key": key, "value": value, "secret": secret, "ttl": ttl},
+ )
+
+ def get_application_kv_record(self, applicationIdentifier: str, key: str):
+ """
+ Get Application KV Record
+
+ Requires the application's own access token (personal access tokens are not supported).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.storage.kv.records.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.storage.kv.records.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_application_kv_records_path(applicationIdentifier, key),
+ )
+
+ def edit_application_kv_record(
+ self,
+ applicationIdentifier: str,
+ key: str,
+ data: Iterable[ApplicationKVRecordPatchRequest],
+ ):
+ """
+ Edit Application KV Record
+
+ Requires the application's own access token (personal access tokens are not supported).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.storage.kv.records.patch
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.storage.kv.records.patch
+ """
+ return self.requester.request(
+ method="patch",
+ path=self.get_application_kv_records_path(applicationIdentifier, key),
+ request_data=data,
+ )
+
+ def delete_application_kv_record(self, applicationIdentifier: str, key: str):
+ """
+ Delete Application KV Record
+
+ Requires the application's own access token (personal access tokens are not supported).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.storage.kv.records.delete
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.storage.kv.records.delete
+ """
+ return self.requester.request(
+ method="delete",
+ path=self.get_application_kv_records_path(applicationIdentifier, key),
+ )
+
+ def get_application_data(
+ self, applicationIdentifier: str, path: str, params: Optional[Dict] = None
+ ):
"""
Get Application Data.
+ `params` are application-specific query parameters.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.api.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.api.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.api.get
"""
return self.requester.request(
method="get",
path=self.get_application_path(applicationIdentifier, path),
+ params=params,
)
- def update_application_data(self, applicationIdentifier: str, path: str, data: dict):
+ def update_application_data(
+ self, applicationIdentifier: str, path: str, data: dict, params: Optional[Dict] = None
+ ):
"""
Update or Restore Application Data.
+ `params` are application-specific query parameters.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.api.put
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.api.put
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.put
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.api.put
"""
- json_data = dumps(data)
return self.requester.request(
method="put",
path=self.get_application_path(applicationIdentifier, path),
- request_data=json_data,
+ params=params,
+ request_data=data,
)
- def add_application_data(self, applicationIdentifier: str, path: str, data: dict):
+ def add_application_data(
+ self, applicationIdentifier: str, path: str, data: dict, params: Optional[Dict] = None
+ ):
"""
Add Application Data.
+ `params` are application-specific query parameters.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.api.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.api.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.api.post
"""
- json_data = dumps(data)
return self.requester.request(
method="post",
path=self.get_application_path(applicationIdentifier, path),
- request_data=json_data,
+ params=params,
+ request_data=data,
)
- def delete_application_data(self, applicationIdentifier: str, path: str):
+ def delete_application_data(
+ self, applicationIdentifier: str, path: str, params: Optional[Dict] = None
+ ):
"""
Delete Application Data.
+ `params` are application-specific query parameters.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.api.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.api.delete
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.api.delete
"""
return self.requester.request(
- method='delete',
- path=self.get_application_path(applicationIdentifier, path)
+ method="delete",
+ path=self.get_application_path(applicationIdentifier, path),
+ params=params,
)
- def edit_application_data(self, applicationIdentifier: str, path: str, data: dict):
+ def edit_application_data(
+ self, applicationIdentifier: str, path: str, data: dict, params: Optional[Dict] = None
+ ):
"""
Edit Application Data.
+ `params` are application-specific query parameters.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.applications.api.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.api.patch
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.api.patch
"""
- json_data = dumps(data)
return self.requester.request(
method="patch",
path=self.get_application_path(applicationIdentifier, path),
- request_data=json_data,
+ params=params,
+ request_data=data,
+ )
+
+ # Integrations API (file-based projects only)
+
+ def list_integration_crowdin_files(
+ self, applicationIdentifier: str, projectId: Optional[int] = None
+ ):
+ """
+ List Crowdin Files
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.crowdin.files
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.crowdin.files
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "crowdin-files"),
+ params={"projectId": projectId},
+ )
+
+ def update_integration_crowdin_files(
+ self,
+ applicationIdentifier: str,
+ files: Iterable[IntegrationCrowdinUpdateFile],
+ uploadTranslations: Optional[bool] = None,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Update Crowdin Files
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.crowdin.update
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.crowdin.update
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_application_path(applicationIdentifier, "crowdin-update"),
+ request_data={
+ "projectId": projectId,
+ "files": files,
+ "uploadTranslations": uploadTranslations,
+ },
+ )
+
+ def get_integration_file_progress(
+ self, applicationIdentifier: str, fileId: int, projectId: Optional[int] = None
+ ):
+ """
+ Get File Progress
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.file.progress
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.file.progress
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "file-progress"),
+ params={"projectId": projectId, "fileId": fileId},
+ )
+
+ def get_integration_login_fields(self, applicationIdentifier: str):
+ """
+ Get Integration Login Form Fields
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.integration.fields
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.integration.fields
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "login-fields"),
+ )
+
+ def list_integration_files(
+ self, applicationIdentifier: str, projectId: Optional[int] = None
+ ):
+ """
+ List Integration Files
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.integration.files
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.integration.files
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "integration-files"),
+ params={"projectId": projectId},
+ )
+
+ def integration_login(
+ self,
+ applicationIdentifier: str,
+ credentials: Dict[str, Any],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Integration Login
+
+ `credentials` are the login form fields, see `get_integration_login_fields`.
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.integration.login
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.integration.login
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_application_path(applicationIdentifier, "login"),
+ request_data={"projectId": projectId, "credentials": credentials},
+ )
+
+ def update_integration_files(
+ self,
+ applicationIdentifier: str,
+ files: Dict[Union[int, str], Iterable[str]],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Update Integration Files
+
+ `files` maps Crowdin file ids to lists of language ids, e.g. `{102: ["de", "fr"]}`.
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.integration.update
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.integration.update
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_application_path(applicationIdentifier, "integration-update"),
+ request_data={"projectId": projectId, "files": files},
+ )
+
+ def get_integration_job(
+ self,
+ applicationIdentifier: str,
+ jobId: Optional[str] = None,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Get Job
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.job.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.job.get
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "jobs"),
+ params={"projectId": projectId, "jobId": jobId},
+ )
+
+ def cancel_integration_job(
+ self, applicationIdentifier: str, jobId: str, projectId: Optional[int] = None
+ ):
+ """
+ Cancel Job
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.job.cancel
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.job.cancel
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="delete",
+ path=self.get_application_path(applicationIdentifier, "jobs"),
+ params={"projectId": projectId, "jobId": jobId},
+ )
+
+ def get_integration_job_info(
+ self, applicationIdentifier: str, jobId: str, projectId: Optional[int] = None
+ ):
+ """
+ Get Job Info
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.job.info
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.job.info
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "job-info"),
+ params={"projectId": projectId, "jobId": jobId},
+ )
+
+ def list_integration_jobs(
+ self,
+ applicationIdentifier: str,
+ projectId: Optional[int] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List Jobs
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.job.list
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.job.list
+ """
+ projectId = projectId or self.get_project_id()
+
+ params = {"projectId": projectId}
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "all-jobs"),
+ params=params,
+ )
+
+ def get_integration_settings(
+ self, applicationIdentifier: str, projectId: Optional[int] = None
+ ):
+ """
+ Get Application Settings
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.settings.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.settings.get
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "settings"),
+ params={"projectId": projectId},
+ )
+
+ def update_integration_settings(
+ self,
+ applicationIdentifier: str,
+ config: Dict[str, Any],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Update Application Settings
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.settings.update
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.settings.update
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_application_path(applicationIdentifier, "settings"),
+ request_data={"projectId": projectId, "config": config},
+ )
+
+ def get_integration_sync_settings(
+ self,
+ applicationIdentifier: str,
+ provider: Union[IntegrationSyncProvider, str],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Get Sync Settings
+
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.sync.settings.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.sync.settings.get
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_application_path(applicationIdentifier, "sync-settings"),
+ params={"projectId": projectId, "provider": provider},
+ )
+
+ def update_integration_sync_settings(
+ self,
+ applicationIdentifier: str,
+ provider: IntegrationSyncProvider,
+ files: Union[
+ Dict[Union[int, str], Iterable[str]], Iterable[IntegrationSyncSettingsFile]
+ ],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Update Sync Settings
+
+ `files` is either a mapping of Crowdin file ids to language ids
+ (e.g. `{102: ["uk", "de"]}`) or a list of integration file objects.
+ Available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.applications.integrations.sync.settings.update
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.applications.integrations.sync.settings.update
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_application_path(applicationIdentifier, "sync-settings"),
+ request_data={"projectId": projectId, "provider": provider, "files": files},
)
diff --git a/crowdin_api/api_resources/application/tests/test_applications_resources.py b/crowdin_api/api_resources/application/tests/test_applications_resources.py
index 34f57ca7..bf76e304 100644
--- a/crowdin_api/api_resources/application/tests/test_applications_resources.py
+++ b/crowdin_api/api_resources/application/tests/test_applications_resources.py
@@ -4,8 +4,13 @@
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.api_resources.application.resource import ApplicationResource
from crowdin_api.api_resources.application.enums import (
+ ApplicationBundleMode,
ApplicationConsentPatchPath,
ApplicationConsentStatus,
+ ApplicationInstallationPatchPath,
+ ApplicationKVRecordPatchPath,
+ ApplicationManifestEnvironment,
+ IntegrationSyncProvider,
ListApplicationConsentsOrderBy,
UserPermissions,
ProjectPermissions,
@@ -50,7 +55,25 @@ def test_list_application_installations(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="get",
path=resource.get_application_installations_path(),
- params=resource.get_page_params()
+ params={"installedBy": None, "orderBy": None, "offset": 0, "limit": 25},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_application_installations_with_filters(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ order_by = Sorting([SortingRule(ListApplicationConsentsOrderBy.ID, SortingOrder.ASC)])
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.list_application_installations(
+ offset=5, limit=10, installedBy=12, orderBy=order_by
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path=resource.get_application_installations_path(),
+ params={"installedBy": 12, "orderBy": order_by, "offset": 5, "limit": 10},
)
@pytest.mark.parametrize(
@@ -62,7 +85,10 @@ def test_list_application_installations(self, m_request, base_absolut_url):
},
{
"url": "https://localhost.dev/crowdin.json",
- "permissions": None
+ "manifest": None,
+ "permissions": None,
+ "modules": None,
+ "assignAgent": None,
}
),
(
@@ -81,6 +107,7 @@ def test_list_application_installations(self, m_request, base_absolut_url):
},
{
"url": "https://localhost.dev/crowdin.json",
+ "manifest": None,
"permissions": {
"user": {
"value": UserPermissions.OWNER,
@@ -90,7 +117,76 @@ def test_list_application_installations(self, m_request, base_absolut_url):
"value": ProjectPermissions.OWN,
"ids": [4, 5, 6]
}
- }
+ },
+ "modules": None,
+ "assignAgent": None,
+ }
+ ),
+ (
+ {
+ "url": "https://localhost.dev/crowdin.json",
+ "modules": [
+ {
+ "key": "some-module-key",
+ "permissions": {
+ "user": {"value": UserPermissions.RESTRICTED, "ids": [1]}
+ },
+ }
+ ],
+ "assignAgent": True,
+ },
+ {
+ "url": "https://localhost.dev/crowdin.json",
+ "manifest": None,
+ "permissions": None,
+ "modules": [
+ {
+ "key": "some-module-key",
+ "permissions": {
+ "user": {"value": UserPermissions.RESTRICTED, "ids": [1]}
+ },
+ }
+ ],
+ "assignAgent": True,
+ }
+ ),
+ (
+ {
+ "manifest": {
+ "name": "My App",
+ "scopes": ["project"],
+ "modules": {
+ "project-tools": [
+ {
+ "key": "my-module",
+ "name": "My Module",
+ "environments": [ApplicationManifestEnvironment.CROWDIN],
+ }
+ ]
+ },
+ "bundle": {"mode": ApplicationBundleMode.INTERNAL},
+ },
+ "permissions": {"project": {"value": ProjectPermissions.OWN, "ids": []}},
+ },
+ {
+ "url": None,
+ "manifest": {
+ "name": "My App",
+ "scopes": ["project"],
+ "modules": {
+ "project-tools": [
+ {
+ "key": "my-module",
+ "name": "My Module",
+ "environments": [ApplicationManifestEnvironment.CROWDIN],
+ }
+ ]
+ },
+ "bundle": {"mode": ApplicationBundleMode.INTERNAL},
+ },
+ "permissions": {"project": {"value": ProjectPermissions.OWN, "ids": []}},
+ "modules": None,
+ "assignAgent": None,
}
),
),
@@ -107,6 +203,18 @@ def test_install_application(self, m_request, in_params, request_data, base_abso
request_data=request_data
)
+ @pytest.mark.parametrize(
+ "in_params",
+ (
+ {},
+ {"url": "https://localhost.dev/crowdin.json", "manifest": {"name": "a", "modules": {}}},
+ ),
+ )
+ def test_install_application_invalid(self, in_params, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ with pytest.raises(ValueError):
+ resource.install_application(**in_params)
+
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_get_applcation_installation(self, m_request, base_absolut_url):
m_request.return_value = "response"
@@ -154,10 +262,34 @@ def test_edit_application_installation(
identifier = "exmaple-application"
data = [{"op": "replace", "path": "/permissions", "value": "test"}]
resource = self.get_resource(base_absolut_url)
- assert resource.edit_applicatoin_installation(
+ assert resource.edit_application_installation(
identifier=identifier,
data=data,
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path=resource.get_application_installations_path(identifier=identifier),
+ request_data=data,
)
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_applicatoin_installation_deprecated(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ identifier = "exmaple-application"
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": ApplicationInstallationPatchPath.MANIFEST,
+ "value": {"name": "My App", "modules": {}},
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ with pytest.deprecated_call():
+ assert resource.edit_applicatoin_installation(
+ identifier=identifier,
+ data=data,
+ ) == "response"
m_request.assert_called_once_with(
method="patch",
path=resource.get_application_installations_path(identifier=identifier),
@@ -304,6 +436,7 @@ def test_get_application_data(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="get",
path=resource.get_application_path(applicationIdentifier="abc", path="test"),
+ params=None,
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -316,7 +449,8 @@ def test_add_application_data(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="post",
path=resource.get_application_path(applicationIdentifier="abc", path="test"),
- request_data='{"key2": 2}'
+ params=None,
+ request_data={"key2": 2},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -328,6 +462,7 @@ def test_delete_application_data(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="delete",
path=resource.get_application_path(applicationIdentifier="abc", path="test"),
+ params=None,
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -339,7 +474,8 @@ def test_edit_application_data(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="patch",
path=resource.get_application_path(applicationIdentifier="abc", path="test"),
- request_data='{"key2": 2}',
+ params=None,
+ request_data={"key2": 2},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -351,5 +487,304 @@ def test_update_application_data(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="put",
path=resource.get_application_path(applicationIdentifier="abc", path="test"),
- request_data='{"key2": 2}',
+ params=None,
+ request_data={"key2": 2},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_application_data_with_params(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.get_application_data("abc", "test", params={"projectId": 1}) == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path="applications/abc/api/test",
+ params={"projectId": 1},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_upload_application_bundle(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.upload_application_bundle("example-app", storageId=12) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="applications/installations/example-app/bundles",
+ request_data={"storageId": 12},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_application_installation_update(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_application_installation_update("example-app") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="applications/installations/example-app/update",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_apply_application_installation_update(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.apply_application_installation_update("example-app", manifestHash="abc")
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path="applications/installations/example-app/update",
+ request_data={"manifestHash": "abc"},
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"applicationIdentifier": "app"}, "applications/app/storage/kv/records"),
+ (
+ {"applicationIdentifier": "app", "key": "user:1:settings"},
+ "applications/app/storage/kv/records/user%3A1%3Asettings",
+ ),
+ ),
+ )
+ def test_get_application_kv_records_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_application_kv_records_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"prefix": None, "orderBy": None, "offset": 0, "limit": 25}),
+ (
+ {"prefix": "user:", "offset": 10, "limit": 5},
+ {"prefix": "user:", "orderBy": None, "offset": 10, "limit": 5},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_application_kv_records(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_application_kv_records("app", **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="applications/app/storage/kv/records",
+ params=request_params,
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {"key": "k", "value": {"a": 1}},
+ {"key": "k", "value": {"a": 1}, "secret": None, "ttl": None},
+ ),
+ (
+ {"key": "k", "value": "v", "secret": True, "ttl": 3600},
+ {"key": "k", "value": "v", "secret": True, "ttl": 3600},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_application_kv_record(
+ self, m_request, in_params, request_data, base_absolut_url
+ ):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_application_kv_record("app", **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="applications/app/storage/kv/records",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_application_kv_record(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_application_kv_record("app", "k") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="applications/app/storage/kv/records/k",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_application_kv_record(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ data = [
+ {"op": PatchOperation.REPLACE, "path": ApplicationKVRecordPatchPath.VALUE, "value": 1},
+ {"op": PatchOperation.REPLACE, "path": ApplicationKVRecordPatchPath.TTL, "value": 60},
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_application_kv_record("app", "k", data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="applications/app/storage/kv/records/k",
+ request_data=data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_application_kv_record(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_application_kv_record("app", "k") == "response"
+ m_request.assert_called_once_with(
+ method="delete",
+ path="applications/app/storage/kv/records/k",
+ )
+
+ @pytest.mark.parametrize(
+ "method_name, in_params, http_method, api_path, call_kwargs",
+ (
+ (
+ "list_integration_crowdin_files",
+ {},
+ "get",
+ "crowdin-files",
+ {"params": {"projectId": 1}},
+ ),
+ (
+ "update_integration_crowdin_files",
+ {"files": [{"id": "1", "name": "Landing pages"}], "uploadTranslations": True},
+ "post",
+ "crowdin-update",
+ {
+ "request_data": {
+ "projectId": 1,
+ "files": [{"id": "1", "name": "Landing pages"}],
+ "uploadTranslations": True,
+ }
+ },
+ ),
+ (
+ "get_integration_file_progress",
+ {"fileId": 2},
+ "get",
+ "file-progress",
+ {"params": {"projectId": 1, "fileId": 2}},
+ ),
+ (
+ "list_integration_files",
+ {},
+ "get",
+ "integration-files",
+ {"params": {"projectId": 1}},
+ ),
+ (
+ "integration_login",
+ {"credentials": {"email": "user@crowdin.com"}},
+ "post",
+ "login",
+ {"request_data": {"projectId": 1, "credentials": {"email": "user@crowdin.com"}}},
+ ),
+ (
+ "update_integration_files",
+ {"files": {102: ["de", "fr"]}},
+ "post",
+ "integration-update",
+ {"request_data": {"projectId": 1, "files": {102: ["de", "fr"]}}},
+ ),
+ (
+ "get_integration_job",
+ {"jobId": "j"},
+ "get",
+ "jobs",
+ {"params": {"projectId": 1, "jobId": "j"}},
+ ),
+ (
+ "cancel_integration_job",
+ {"jobId": "j"},
+ "delete",
+ "jobs",
+ {"params": {"projectId": 1, "jobId": "j"}},
+ ),
+ (
+ "get_integration_job_info",
+ {"jobId": "j"},
+ "get",
+ "job-info",
+ {"params": {"projectId": 1, "jobId": "j"}},
+ ),
+ (
+ "list_integration_jobs",
+ {"offset": 0, "limit": 10},
+ "get",
+ "all-jobs",
+ {"params": {"projectId": 1, "offset": 0, "limit": 10}},
+ ),
+ (
+ "get_integration_settings",
+ {},
+ "get",
+ "settings",
+ {"params": {"projectId": 1}},
+ ),
+ (
+ "update_integration_settings",
+ {"config": {"schedule": "0"}},
+ "post",
+ "settings",
+ {"request_data": {"projectId": 1, "config": {"schedule": "0"}}},
+ ),
+ (
+ "get_integration_sync_settings",
+ {"provider": IntegrationSyncProvider.CROWDIN},
+ "get",
+ "sync-settings",
+ {"params": {"projectId": 1, "provider": IntegrationSyncProvider.CROWDIN}},
+ ),
+ (
+ "update_integration_sync_settings",
+ {"provider": IntegrationSyncProvider.INTEGRATION, "files": {102: ["uk"]}},
+ "post",
+ "sync-settings",
+ {
+ "request_data": {
+ "projectId": 1,
+ "provider": IntegrationSyncProvider.INTEGRATION,
+ "files": {102: ["uk"]},
+ }
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_integration_methods(
+ self, m_request, method_name, in_params, http_method, api_path, call_kwargs,
+ base_absolut_url,
+ ):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert getattr(resource, method_name)("app", projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method=http_method,
+ path=f"applications/app/api/{api_path}",
+ **call_kwargs,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_integration_methods_default_project(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.resource_class(
+ requester=APIRequester(base_url=base_absolut_url), project_id=7
+ )
+ assert resource.list_integration_files("app") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="applications/app/api/integration-files",
+ params={"projectId": 7},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_integration_login_fields(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_integration_login_fields("app") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="applications/app/api/login-fields",
)
diff --git a/crowdin_api/api_resources/application/types.py b/crowdin_api/api_resources/application/types.py
index 53604db8..90253db0 100644
--- a/crowdin_api/api_resources/application/types.py
+++ b/crowdin_api/api_resources/application/types.py
@@ -1,10 +1,14 @@
-from typing import Any, Iterable, Optional
+from typing import Any, Dict, Iterable, Optional, Union
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.typing import TypedDict
from crowdin_api.api_resources.application.enums import (
+ ApplicationBundleMode,
ApplicationConsentPatchPath,
ApplicationConsentStatus,
+ ApplicationInstallationPatchPath,
+ ApplicationKVRecordPatchPath,
+ ApplicationManifestEnvironment,
UserPermissions,
ProjectPermissions,
)
@@ -20,15 +24,60 @@ class ApplicationProject(TypedDict):
ids: Iterable[int]
-class ApplicationPermissions(TypedDict):
- user: ApplicationUser
+class ApplicationPermissions(TypedDict, total=False):
+ user: ApplicationUser # deprecated by the API, use module permissions instead
project: ApplicationProject
+class ApplicationModuleUserPermissions(TypedDict, total=False):
+ user: ApplicationUser
+
+
+class ApplicationModulePermissions(TypedDict, total=False):
+ key: str
+ permissions: ApplicationModuleUserPermissions
+
+
+class _ApplicationManifestModuleRequired(TypedDict):
+ key: str
+ name: str
+
+
+class ApplicationManifestModule(_ApplicationManifestModuleRequired, total=False):
+ logo: str
+ description: str
+ environments: Iterable[ApplicationManifestEnvironment]
+ permissions: ApplicationModuleUserPermissions
+
+
+class ApplicationManifestDefaultPermissions(TypedDict, total=False):
+ user: UserPermissions
+ project: ProjectPermissions
+
+
+class ApplicationManifestBundle(TypedDict, total=False):
+ mode: ApplicationBundleMode
+ url: str # external mode only
+
+
+class _ApplicationManifestRequired(TypedDict):
+ name: str
+ modules: Dict[str, Iterable[ApplicationManifestModule]]
+
+
+class ApplicationManifest(_ApplicationManifestRequired, total=False):
+ description: str
+ logo: str
+ scopes: Iterable[str]
+ stringBasedAvailable: bool
+ default_permissions: ApplicationManifestDefaultPermissions
+ bundle: ApplicationManifestBundle
+
+
class ApplicationInstallationPatchRequest(TypedDict):
- op: str
- path: str
- value: str
+ op: Union[PatchOperation, str]
+ path: Union[ApplicationInstallationPatchPath, str]
+ value: Any
class AddApplicationConsentRequest(TypedDict):
@@ -42,3 +91,27 @@ class ApplicationConsentPatchRequest(TypedDict):
op: PatchOperation
path: ApplicationConsentPatchPath
value: Any
+
+
+class ApplicationKVRecordPatchRequest(TypedDict):
+ op: PatchOperation
+ path: ApplicationKVRecordPatchPath
+ value: Any
+
+
+class IntegrationCrowdinUpdateFile(TypedDict, total=False):
+ id: str
+ name: str
+ parent_id: str
+ parentId: str
+ type: str
+ node_type: str
+
+
+class IntegrationSyncSettingsFile(TypedDict, total=False):
+ id: str
+ name: str
+ parentId: str
+ type: str
+ node_type: str
+ schedule: bool
diff --git a/crowdin_api/api_resources/branches/enums.py b/crowdin_api/api_resources/branches/enums.py
index 727c599d..8198e20f 100644
--- a/crowdin_api/api_resources/branches/enums.py
+++ b/crowdin_api/api_resources/branches/enums.py
@@ -4,7 +4,9 @@
class EditBranchPatchPath(Enum):
NAME = "/name"
TITLE = "/title"
+ EXPORT_PATTERN = "/exportPattern" # file-based projects only
PRIORITY = "/priority"
+ IS_PROTECTED = "/isProtected" # string-based projects only
class ListBranchesOrderBy(Enum):
diff --git a/crowdin_api/api_resources/branches/resource.py b/crowdin_api/api_resources/branches/resource.py
index 524dfcd3..f73653eb 100644
--- a/crowdin_api/api_resources/branches/resource.py
+++ b/crowdin_api/api_resources/branches/resource.py
@@ -1,4 +1,4 @@
-from typing import Optional, Iterable
+from typing import Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.branches.types import (
@@ -12,12 +12,17 @@
class BranchesResource(BaseResource):
"""
- Resource for Bundles
+ Resource for Branches.
+
+ Branch CRUD and deletion status endpoints are available for both file-based and
+ string-based projects. Clones and merges are available for string-based projects only.
Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches
Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches
https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches
"""
@@ -30,6 +35,8 @@ def get_cloned_branch(
"""
Get Cloned Branch
+ String-based projects only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.clones.branch.get
@@ -56,6 +63,8 @@ def clone_branch(
"""
Clone Branch
+ String-based projects only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.clones.post
@@ -78,6 +87,8 @@ def check_branch_clone_status(
"""
Check Branch Clone Status
+ String-based projects only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.clones.get
@@ -112,9 +123,11 @@ def list_branches(
List Branches
Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches/operation/api.projects.branches.getMany
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.getMany
Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches/operation/api.projects.branches.getMany
https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches/operation/api.projects.branches.getMany
"""
@@ -124,7 +137,7 @@ def list_branches(
}
params.update(self.get_page_params(limit=limit, offset=offset))
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_branches_path(project_id),
params=params,
@@ -138,10 +151,15 @@ def add_branch(
"""
Add Branch
+ `exportPattern` and `priority` are for file-based projects only,
+ `isProtected` is for string-based projects only.
+
Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches/operation/api.projects.branches.post
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.post
Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches/operation/api.projects.branches.post
https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches/operation/api.projects.branches.post
"""
@@ -156,9 +174,11 @@ def get_branch(self, project_id: int, branch_id: int):
Get Branch
Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches/operation/api.projects.branches.get
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.get
Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches/operation/api.projects.branches.get
https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches/operation/api.projects.branches.get
"""
@@ -167,30 +187,61 @@ def get_branch(self, project_id: int, branch_id: int):
path=self.get_branches_path(project_id, branch_id),
)
- def delete_branch(self, project_id: int, branch_id: int):
+ def delete_branch(self, project_id: int, branch_id: int, prefer: Optional[str] = None):
"""
Delete Branch
+ Pass `prefer="respond-async"` to delete the branch asynchronously (recommended); the API
+ then returns a job to poll via `check_branch_deletion_status`.
+
Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches/operation/api.projects.branches.delete
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.delete
Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches/operation/api.projects.branches.delete
https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches/operation/api.projects.branches.delete
"""
+ headers = {"Prefer": prefer} if prefer is not None else None
+
return self.requester.request(
method="delete",
+ headers=headers,
path=self.get_branches_path(project_id, branch_id),
)
+ def check_branch_deletion_status(self, project_id: int, branch_id: int, job_identifier: str):
+ """
+ Check Branch Deletion Status
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches/operation/api.projects.branches.jobs.get
+ https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.jobs.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches/operation/api.projects.branches.jobs.get
+ https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches/operation/api.projects.branches.jobs.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=f"{self.get_branches_path(project_id, branch_id)}/jobs/{job_identifier}",
+ )
+
def edit_branch(self, project_id: int, branch_id: int, patches: Iterable[EditBranchPatch]):
"""
Edit Branch
+ `/exportPattern` is for file-based projects only, `/isProtected` is for string-based
+ projects only.
+
Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Branches/operation/api.projects.branches.patch
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.patch
Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Branches/operation/api.projects.branches.patch
https://support.crowdin.com/developer/enterprise/api/v2/string-based/#tag/Branches/operation/api.projects.branches.patch
"""
@@ -200,7 +251,7 @@ def edit_branch(self, project_id: int, branch_id: int, patches: Iterable[EditBra
request_data=patches,
)
- def get_branch_merges_path(self, project_id: int, branch_id: int, merge_id: Optional[int] = None):
+ def get_branch_merges_path(self, project_id: int, branch_id: int, merge_id: Optional[Union[int, str]] = None):
if merge_id is not None:
return f"projects/{project_id}/branches/{branch_id}/merges/{merge_id}"
@@ -210,6 +261,8 @@ def merge_branch(self, project_id: int, branch_id: int, request: MergeBranchRequ
"""
Merge Branch
+ String-based projects only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.merges.post
@@ -223,10 +276,12 @@ def merge_branch(self, project_id: int, branch_id: int, request: MergeBranchRequ
request_data=request,
)
- def check_branch_merge_status(self, project_id: int, branch_id: int, merge_id: int):
+ def check_branch_merge_status(self, project_id: int, branch_id: int, merge_id: Union[int, str]):
"""
Check Branch Merge Status
+ String-based projects only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.merges.get
@@ -239,10 +294,12 @@ def check_branch_merge_status(self, project_id: int, branch_id: int, merge_id: i
path=self.get_branch_merges_path(project_id, branch_id, merge_id),
)
- def get_branch_merge_summary(self, project_id: int, branch_id: int, merge_id: int):
+ def get_branch_merge_summary(self, project_id: int, branch_id: int, merge_id: Union[int, str]):
"""
Get Branch Merge Summary
+ String-based projects only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/string-based/#tag/Branches/operation/api.projects.branches.merges.summary.get
diff --git a/crowdin_api/api_resources/branches/tests/test_branches.py b/crowdin_api/api_resources/branches/tests/test_branches.py
index 11f505dd..9ae12723 100644
--- a/crowdin_api/api_resources/branches/tests/test_branches.py
+++ b/crowdin_api/api_resources/branches/tests/test_branches.py
@@ -7,6 +7,7 @@
from crowdin_api.api_resources.branches.types import CloneBranchRequest, AddBranchRequest, EditBranchPatch, \
MergeBranchRequest
from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.source_files.enums import Priority
from crowdin_api.requester import APIRequester
from crowdin_api.sorting import SortingRule, Sorting, SortingOrder
@@ -45,6 +46,16 @@ def test_get_cloned_branch(self, m_request, base_absolut_url):
"title": "Branch title"
}
),
+ (
+ CloneBranchRequest(
+ name="Branch name",
+ isProtected=True
+ ),
+ {
+ "name": "Branch name",
+ "isProtected": True
+ }
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -127,6 +138,22 @@ def test_list_branches(self, m_request, in_params, query_params, base_absolut_ur
"title": "Title of new branch"
}
),
+ (
+ AddBranchRequest(
+ name="New branch",
+ exportPattern="%three_letters_code%",
+ priority=Priority.HIGH,
+ ),
+ {
+ "name": "New branch",
+ "exportPattern": "%three_letters_code%",
+ "priority": Priority.HIGH,
+ }
+ ),
+ (
+ AddBranchRequest(name="New branch", isProtected=True),
+ {"name": "New branch", "isProtected": True}
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -168,9 +195,55 @@ def test_delete_branch(self, m_request, base_absolut_url):
assert resource.delete_branch(project_id, branch_id) == "response"
m_request.assert_called_once_with(
method="delete",
+ headers=None,
path=f"projects/{project_id}/branches/{branch_id}",
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_branch_async(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_branch(1, 2, prefer="respond-async") == "response"
+ m_request.assert_called_once_with(
+ method="delete",
+ headers={"Prefer": "respond-async"},
+ path="projects/1/branches/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_branch_deletion_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.check_branch_deletion_status(1, 2, "job") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/branches/2/jobs/job",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_branch_merge_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.check_branch_merge_status(1, 2, "merge") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/branches/2/merges/merge",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_branch_merge_summary(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_branch_merge_summary(1, 2, "merge") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/branches/2/merges/merge/summary",
+ )
+
@pytest.mark.parametrize(
"in_body, request_body",
(
@@ -190,6 +263,22 @@ def test_delete_branch(self, m_request, base_absolut_url):
}
]
),
+ (
+ [
+ EditBranchPatch(
+ op=PatchOperation.REPLACE,
+ path=EditBranchPatchPath.IS_PROTECTED,
+ value=True
+ )
+ ],
+ [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": EditBranchPatchPath.IS_PROTECTED,
+ "value": True
+ }
+ ]
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
diff --git a/crowdin_api/api_resources/branches/types.py b/crowdin_api/api_resources/branches/types.py
index fa3c617a..239ec45e 100644
--- a/crowdin_api/api_resources/branches/types.py
+++ b/crowdin_api/api_resources/branches/types.py
@@ -1,17 +1,36 @@
-from typing import TypedDict, Optional, Any
+from typing import Optional, Any
from crowdin_api.api_resources.branches.enums import EditBranchPatchPath
from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.source_files.enums import Priority
+from crowdin_api.typing import TypedDict
-class CloneBranchRequest(TypedDict):
+class _CloneBranchRequestRequired(TypedDict):
name: str
+
+
+class CloneBranchRequest(_CloneBranchRequestRequired, total=False):
+ """String-based projects only."""
+
title: Optional[str]
+ isProtected: Optional[bool]
-class AddBranchRequest(TypedDict):
+class _AddBranchRequestRequired(TypedDict):
name: str
+
+
+class AddBranchRequest(_AddBranchRequestRequired, total=False):
+ """
+ `exportPattern` and `priority` are for file-based projects only,
+ `isProtected` is for string-based projects only.
+ """
+
title: Optional[str]
+ exportPattern: Optional[str]
+ priority: Optional[Priority]
+ isProtected: Optional[bool]
class EditBranchPatch(TypedDict):
@@ -20,8 +39,13 @@ class EditBranchPatch(TypedDict):
value: Any
-class MergeBranchRequest(TypedDict):
- deleteAfterMerge: Optional[bool]
+class _MergeBranchRequestRequired(TypedDict):
sourceBranchId: int
+
+
+class MergeBranchRequest(_MergeBranchRequestRequired, total=False):
+ """String-based projects only."""
+
+ deleteAfterMerge: Optional[bool]
dryRun: Optional[bool]
acceptSourceChanges: Optional[bool]
diff --git a/crowdin_api/api_resources/bundles/enums.py b/crowdin_api/api_resources/bundles/enums.py
index 0679bce1..41797491 100644
--- a/crowdin_api/api_resources/bundles/enums.py
+++ b/crowdin_api/api_resources/bundles/enums.py
@@ -7,5 +7,16 @@ class BundlePatchPath(Enum):
SOURCE_PATTERNS = "/sourcePatterns"
IGNORE_PATTERNS = "/ignorePatterns"
EXPORT_PATTERNS = "/exportPattern"
- DESCRIPTION = "/description"
+ DESCRIPTION = "/description" # no longer listed in the API spec, kept for backward compatibility
+ IS_MULTILINGUAL = "/isMultilingual"
+ INCLUDE_PROJECT_SOURCE_LANGUAGE = "/includeProjectSourceLanguage"
+ INCLUDE_IN_CONTEXT_PSEUDO_LANGUAGE = "/includeInContextPseudoLanguage"
LABEL_IDS = "/labelIds"
+ EXCLUDE_LABEL_IDS = "/excludeLabelIds"
+ LABEL_MATCH_RULE = "/labelMatchRule"
+ EXCLUDE_LABEL_MATCH_RULE = "/excludeLabelMatchRule"
+
+
+class BundleLabelMatchRule(Enum):
+ ALL = "all"
+ ANY = "any"
diff --git a/crowdin_api/api_resources/bundles/resource.py b/crowdin_api/api_resources/bundles/resource.py
index ae6c8608..db776a37 100644
--- a/crowdin_api/api_resources/bundles/resource.py
+++ b/crowdin_api/api_resources/bundles/resource.py
@@ -1,6 +1,7 @@
from typing import Optional, Iterable
from crowdin_api.api_resources.abstract.resources import BaseResource
+from crowdin_api.api_resources.bundles.enums import BundleLabelMatchRule
from crowdin_api.api_resources.bundles.types import BundlePatchRequest
@@ -9,10 +10,10 @@ class BundlesResource(BaseResource):
Resource for Bundles.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Bundles
+ https://support.crowdin.com/developer/api/v2/#tag/Bundles
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Bundles
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Bundles
"""
def get_bundles_path(self, projectId: int, bundleId: Optional[int] = None):
if bundleId:
@@ -36,10 +37,10 @@ def list_bundles(
List Bundles.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.getMany
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.getMany
"""
params = self.get_page_params(offset=offset, limit=limit)
@@ -54,24 +55,33 @@ def list_bundles(
def add_bundle(
self,
name: str,
- format: str,
+ format: Optional[str],
sourcePatterns: Iterable[str],
- exportPattern: str,
+ exportPattern: Optional[str],
projectId: Optional[int] = None,
ignorePatterns: Optional[Iterable[str]] = None,
isMultilingual: Optional[bool] = None,
includeProjectSourceLanguage: Optional[bool] = None,
labelIds: Optional[Iterable[int]] = None,
excludeLabelIds: Optional[Iterable[int]] = None,
+ sourceLanguageExportPattern: Optional[str] = None,
+ includeInContextPseudoLanguage: Optional[bool] = None,
+ labelMatchRule: Optional[BundleLabelMatchRule] = None,
+ excludeLabelMatchRule: Optional[BundleLabelMatchRule] = None,
+ languageIds: Optional[Iterable[str]] = None,
):
"""
Add Bundles.
+ `format` and `exportPattern` may be `None`: if `format` is not provided, files are exported
+ in their original format (`format` is required for string-based projects), and
+ `exportPattern` is required only if `format` is specified.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.post
"""
projectId = projectId or self.get_project_id()
@@ -89,6 +99,11 @@ def add_bundle(
"includeProjectSourceLanguage": includeProjectSourceLanguage,
"labelIds": labelIds,
"excludeLabelIds": excludeLabelIds,
+ "sourceLanguageExportPattern": sourceLanguageExportPattern,
+ "includeInContextPseudoLanguage": includeInContextPseudoLanguage,
+ "labelMatchRule": labelMatchRule,
+ "excludeLabelMatchRule": excludeLabelMatchRule,
+ "languageIds": languageIds,
}
)
@@ -97,10 +112,10 @@ def get_bundle(self, bundleId: int, projectId: Optional[int] = None):
Get Bundle.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.get
"""
projectId = projectId or self.get_project_id()
@@ -115,10 +130,10 @@ def delete_bundle(self, bundleId: int, projectId: Optional[int] = None):
Delete Bundle.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.delete
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.delete
"""
projectId = projectId or self.get_project_id()
@@ -138,10 +153,10 @@ def edit_bundle(
Edit Bundle.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.patch
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.patch
"""
projectId = projectId or self.get_project_id()
@@ -159,10 +174,10 @@ def download_bundle(
Download bundle.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.exports.download.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.exports.download.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.exports.download.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.exports.download.get
"""
projectId = projectId or self.get_project_id()
@@ -186,11 +201,15 @@ def export_bundle(
"""
Export bundle.
+ `skipUntranslatedFiles` is for file-based projects only, `exportApprovedOnly` is for
+ Crowdin only, `exportWithMinApprovalsCount` and `exportStringsThatPassedWorkflow` are for
+ Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.exports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.exports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.exports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.exports.post
"""
projectId = projectId or self.get_project_id()
@@ -215,10 +234,10 @@ def check_bundle_export_status(
Check Bundle Export Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.exports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.exports.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.exports.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.exports.get
"""
projectId = projectId or self.get_project_id()
@@ -239,10 +258,10 @@ def get_bundle_list_files(
Get Bundle List Files.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.bundles.files.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.bundles.files.getMany
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.bundles.files.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.bundles.files.getMany
"""
params = self.get_page_params(offset=offset, limit=limit)
@@ -253,3 +272,31 @@ def get_bundle_list_files(
path=f"{self.get_bundles_path(projectId=projectId, bundleId=bundleId)}/files",
params=params,
)
+
+ def list_bundle_branches(
+ self,
+ bundleId: int,
+ projectId: Optional[int] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ Get Bundle List Branches.
+
+ String-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/string-based/#operation/api.projects.bundles.branches.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/string-based/#operation/api.projects.bundles.branches.getMany
+ """
+
+ params = self.get_page_params(offset=offset, limit=limit)
+ projectId = projectId or self.get_project_id()
+
+ return self._get_entire_data(
+ method="get",
+ path=f"{self.get_bundles_path(projectId=projectId, bundleId=bundleId)}/branches",
+ params=params,
+ )
diff --git a/crowdin_api/api_resources/bundles/tests/test_bundles_resources.py b/crowdin_api/api_resources/bundles/tests/test_bundles_resources.py
index 9fbeceec..fde09b9b 100644
--- a/crowdin_api/api_resources/bundles/tests/test_bundles_resources.py
+++ b/crowdin_api/api_resources/bundles/tests/test_bundles_resources.py
@@ -3,7 +3,7 @@
import pytest
from crowdin_api.api_resources import BundlesResource
-from crowdin_api.api_resources.bundles.enums import BundlePatchPath
+from crowdin_api.api_resources.bundles.enums import BundleLabelMatchRule, BundlePatchPath
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.requester import APIRequester
@@ -155,7 +155,60 @@ def test_add_bundle(self, m_request, incoming_data, request_data, base_absolut_u
m_request.assert_called_once_with(
method="post",
path=resource.get_bundles_path(projectId=1),
- request_data=request_data,
+ request_data={
+ "sourceLanguageExportPattern": None,
+ "includeInContextPseudoLanguage": None,
+ "labelMatchRule": None,
+ "excludeLabelMatchRule": None,
+ "languageIds": None,
+ **request_data,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_bundle_with_all_params(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_bundle(
+ projectId=1,
+ name="test_name",
+ format=None,
+ sourcePatterns=["/master/"],
+ exportPattern=None,
+ ignorePatterns=["/master/environments/"],
+ isMultilingual=True,
+ includeProjectSourceLanguage=True,
+ labelIds=[2],
+ excludeLabelIds=[3],
+ sourceLanguageExportPattern="source.resx",
+ includeInContextPseudoLanguage=False,
+ labelMatchRule=BundleLabelMatchRule.ANY,
+ excludeLabelMatchRule=BundleLabelMatchRule.ALL,
+ languageIds=["uk", "de"],
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path=resource.get_bundles_path(projectId=1),
+ request_data={
+ "name": "test_name",
+ "format": None,
+ "sourcePatterns": ["/master/"],
+ "exportPattern": None,
+ "ignorePatterns": ["/master/environments/"],
+ "isMultilingual": True,
+ "includeProjectSourceLanguage": True,
+ "labelIds": [2],
+ "excludeLabelIds": [3],
+ "sourceLanguageExportPattern": "source.resx",
+ "includeInContextPseudoLanguage": False,
+ "labelMatchRule": BundleLabelMatchRule.ANY,
+ "excludeLabelMatchRule": BundleLabelMatchRule.ALL,
+ "languageIds": ["uk", "de"],
+ },
)
@pytest.mark.parametrize(
@@ -291,3 +344,47 @@ def test_get_bundle_list_files(
path="projects/1/bundles/1/files",
params=request_params,
)
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_params",
+ (
+ ({}, {"limit": 25, "offset": 0}),
+ ({"limit": 10, "offset": 2}, {"limit": 10, "offset": 2}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_bundle_branches(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_bundle_branches(projectId=1, bundleId=2, **incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/bundles/2/branches",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_bundle_label_match_rule(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": BundlePatchPath.LABEL_MATCH_RULE,
+ "value": BundleLabelMatchRule.ANY,
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": BundlePatchPath.INCLUDE_IN_CONTEXT_PSEUDO_LANGUAGE,
+ "value": False,
+ },
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_bundle(projectId=1, bundleId=2, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path=resource.get_bundles_path(projectId=1, bundleId=2),
+ request_data=data,
+ )
diff --git a/crowdin_api/api_resources/bundles/types.py b/crowdin_api/api_resources/bundles/types.py
index f894bef0..8e61ef52 100644
--- a/crowdin_api/api_resources/bundles/types.py
+++ b/crowdin_api/api_resources/bundles/types.py
@@ -1,4 +1,4 @@
-from typing import Union
+from typing import Any
from crowdin_api.api_resources.bundles.enums import BundlePatchPath
from crowdin_api.api_resources.enums import PatchOperation
@@ -6,6 +6,6 @@
class BundlePatchRequest(TypedDict):
- value: Union[str, int]
+ value: Any
op: PatchOperation
path: BundlePatchPath
diff --git a/crowdin_api/api_resources/clients/resource.py b/crowdin_api/api_resources/clients/resource.py
index 428644f3..c67d3ece 100644
--- a/crowdin_api/api_resources/clients/resource.py
+++ b/crowdin_api/api_resources/clients/resource.py
@@ -7,10 +7,15 @@ class ClientsResource(BaseResource):
"""
Resource for Clients.
- Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Clients
+ Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Clients
"""
+ def get_clients_path(self):
+ return "clients"
+
def list_clients(
self,
limit: Optional[int] = None,
@@ -19,12 +24,12 @@ def list_clients(
"""
List Clients
- Link to documentation for enterprise:
- https://support.crowdin.com/developer/enterprise/api/v2/#tag/Clients/operation/api.clients.getMany
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.clients.getMany
"""
- return self.requester.request(
+ return self._get_entire_data(
method="get",
- path="/clients",
- params=self.get_page_params(offset=offset, limit=limit)
+ path=self.get_clients_path(),
+ params=self.get_page_params(offset=offset, limit=limit),
)
diff --git a/crowdin_api/api_resources/clients/tests/test_clients_resources.py b/crowdin_api/api_resources/clients/tests/test_clients_resources.py
new file mode 100644
index 00000000..9e78ca16
--- /dev/null
+++ b/crowdin_api/api_resources/clients/tests/test_clients_resources.py
@@ -0,0 +1,36 @@
+from unittest import mock
+
+import pytest
+
+from crowdin_api.api_resources.clients.resource import ClientsResource
+from crowdin_api.requester import APIRequester
+
+
+class TestClientsResource:
+ resource_class = ClientsResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ def test_get_clients_path(self, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_clients_path() == "clients"
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_params",
+ (
+ ({}, {"limit": 25, "offset": 0}),
+ ({"limit": 10, "offset": 2}, {"limit": 10, "offset": 2}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_clients(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_clients(**incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="clients",
+ params=request_params,
+ )
diff --git a/crowdin_api/api_resources/custom_placeholders/__init__.py b/crowdin_api/api_resources/custom_placeholders/__init__.py
new file mode 100644
index 00000000..9f77d639
--- /dev/null
+++ b/crowdin_api/api_resources/custom_placeholders/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {'tests': False}
diff --git a/crowdin_api/api_resources/custom_placeholders/enums.py b/crowdin_api/api_resources/custom_placeholders/enums.py
new file mode 100644
index 00000000..e7a30034
--- /dev/null
+++ b/crowdin_api/api_resources/custom_placeholders/enums.py
@@ -0,0 +1,7 @@
+from enum import Enum
+
+
+class CustomPlaceholderPatchPath(Enum):
+ DESCRIPTION = "/description"
+ DEFINITION = "/definition"
+ ARGUMENT_DELIMITER = "/argumentDelimiter"
diff --git a/crowdin_api/api_resources/custom_placeholders/resource.py b/crowdin_api/api_resources/custom_placeholders/resource.py
new file mode 100644
index 00000000..044e01e1
--- /dev/null
+++ b/crowdin_api/api_resources/custom_placeholders/resource.py
@@ -0,0 +1,104 @@
+from typing import Iterable, Optional
+
+from crowdin_api.api_resources.abstract.resources import BaseResource
+from crowdin_api.api_resources.custom_placeholders.types import CustomPlaceholderPatchRequest
+
+
+class CustomPlaceholdersResource(BaseResource):
+ """
+ Resource for Custom Placeholders.
+
+ Custom placeholders of the organization. Assign them to a project with
+ Add Project Placeholder.
+
+ Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Custom-Placeholders
+ """
+
+ def get_custom_placeholders_path(self, customPlaceholderId: Optional[int] = None):
+ if customPlaceholderId is not None:
+ return f"custom-placeholders/{customPlaceholderId}"
+
+ return "custom-placeholders"
+
+ def list_custom_placeholders(
+ self,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List Custom Placeholders.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-placeholders.getMany
+ """
+ return self._get_entire_data(
+ method="get",
+ path=self.get_custom_placeholders_path(),
+ params=self.get_page_params(offset=offset, limit=limit),
+ )
+
+ def add_custom_placeholder(
+ self,
+ definition: str,
+ description: Optional[str] = None,
+ argumentDelimiter: Optional[str] = None,
+ ):
+ """
+ Add Custom Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-placeholders.post
+ """
+ return self.requester.request(
+ method="post",
+ path=self.get_custom_placeholders_path(),
+ request_data={
+ "definition": definition,
+ "description": description,
+ "argumentDelimiter": argumentDelimiter,
+ },
+ )
+
+ def get_custom_placeholder(self, customPlaceholderId: int):
+ """
+ Get Custom Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-placeholders.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_custom_placeholders_path(customPlaceholderId=customPlaceholderId),
+ )
+
+ def edit_custom_placeholder(
+ self,
+ customPlaceholderId: int,
+ data: Iterable[CustomPlaceholderPatchRequest],
+ ):
+ """
+ Edit Custom Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-placeholders.patch
+ """
+ return self.requester.request(
+ method="patch",
+ path=self.get_custom_placeholders_path(customPlaceholderId=customPlaceholderId),
+ request_data=data,
+ )
+
+ def delete_custom_placeholder(self, customPlaceholderId: int):
+ """
+ Delete Custom Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-placeholders.delete
+ """
+ return self.requester.request(
+ method="delete",
+ path=self.get_custom_placeholders_path(customPlaceholderId=customPlaceholderId),
+ )
diff --git a/crowdin_api/api_resources/custom_placeholders/tests/test_custom_placeholders_resources.py b/crowdin_api/api_resources/custom_placeholders/tests/test_custom_placeholders_resources.py
new file mode 100644
index 00000000..53e30c97
--- /dev/null
+++ b/crowdin_api/api_resources/custom_placeholders/tests/test_custom_placeholders_resources.py
@@ -0,0 +1,119 @@
+from unittest import mock
+
+import pytest
+
+from crowdin_api.api_resources.custom_placeholders.enums import CustomPlaceholderPatchPath
+from crowdin_api.api_resources.custom_placeholders.resource import CustomPlaceholdersResource
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.requester import APIRequester
+
+
+class TestCustomPlaceholdersResource:
+ resource_class = CustomPlaceholdersResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({}, "custom-placeholders"),
+ ({"customPlaceholderId": 1}, "custom-placeholders/1"),
+ ),
+ )
+ def test_get_custom_placeholders_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_custom_placeholders_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"offset": 0, "limit": 25}),
+ ({"offset": 10, "limit": 5}, {"offset": 10, "limit": 5}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_custom_placeholders(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_custom_placeholders(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="custom-placeholders",
+ params=request_params,
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {"definition": "start, then \"http\", end"},
+ {
+ "definition": "start, then \"http\", end",
+ "description": None,
+ "argumentDelimiter": None,
+ },
+ ),
+ (
+ {
+ "definition": "start, then \"http\", end",
+ "description": "URL validation",
+ "argumentDelimiter": "'",
+ },
+ {
+ "definition": "start, then \"http\", end",
+ "description": "URL validation",
+ "argumentDelimiter": "'",
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_custom_placeholder(self, m_request, in_params, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_custom_placeholder(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="custom-placeholders",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_custom_placeholder(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_custom_placeholder(customPlaceholderId=1) == "response"
+ m_request.assert_called_once_with(method="get", path="custom-placeholders/1")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_custom_placeholder(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": CustomPlaceholderPatchPath.DESCRIPTION,
+ "value": "test",
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_custom_placeholder(customPlaceholderId=1, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="custom-placeholders/1",
+ request_data=data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_custom_placeholder(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_custom_placeholder(customPlaceholderId=1) == "response"
+ m_request.assert_called_once_with(method="delete", path="custom-placeholders/1")
diff --git a/crowdin_api/api_resources/custom_placeholders/types.py b/crowdin_api/api_resources/custom_placeholders/types.py
new file mode 100644
index 00000000..f3677722
--- /dev/null
+++ b/crowdin_api/api_resources/custom_placeholders/types.py
@@ -0,0 +1,11 @@
+from typing import Any
+
+from crowdin_api.api_resources.custom_placeholders.enums import CustomPlaceholderPatchPath
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.typing import TypedDict
+
+
+class CustomPlaceholderPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: CustomPlaceholderPatchPath
diff --git a/crowdin_api/api_resources/custom_spellcheckers/__init__.py b/crowdin_api/api_resources/custom_spellcheckers/__init__.py
new file mode 100644
index 00000000..9f77d639
--- /dev/null
+++ b/crowdin_api/api_resources/custom_spellcheckers/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {'tests': False}
diff --git a/crowdin_api/api_resources/custom_spellcheckers/resource.py b/crowdin_api/api_resources/custom_spellcheckers/resource.py
new file mode 100644
index 00000000..31909935
--- /dev/null
+++ b/crowdin_api/api_resources/custom_spellcheckers/resource.py
@@ -0,0 +1,49 @@
+from typing import Optional
+
+from crowdin_api.api_resources.abstract.resources import BaseResource
+
+
+class CustomSpellcheckersResource(BaseResource):
+ """
+ Resource for Custom Spellcheckers.
+
+ Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Custom-Spellcheckers
+ """
+
+ def get_custom_spellcheckers_path(self, customSpellcheckerId: Optional[int] = None):
+ if customSpellcheckerId is not None:
+ return f"custom-spellcheckers/{customSpellcheckerId}"
+
+ return "custom-spellcheckers"
+
+ def list_custom_spellcheckers(
+ self,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List Custom Spellcheckers.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-spellcheckers.getMany
+ """
+ return self._get_entire_data(
+ method="get",
+ path=self.get_custom_spellcheckers_path(),
+ params=self.get_page_params(offset=offset, limit=limit),
+ )
+
+ def get_custom_spellchecker(self, customSpellcheckerId: int):
+ """
+ Get Custom Spellchecker.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.custom-spellcheckers.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_custom_spellcheckers_path(customSpellcheckerId=customSpellcheckerId),
+ )
diff --git a/crowdin_api/api_resources/custom_spellcheckers/tests/test_custom_spellcheckers_resources.py b/crowdin_api/api_resources/custom_spellcheckers/tests/test_custom_spellcheckers_resources.py
new file mode 100644
index 00000000..379441e4
--- /dev/null
+++ b/crowdin_api/api_resources/custom_spellcheckers/tests/test_custom_spellcheckers_resources.py
@@ -0,0 +1,53 @@
+from unittest import mock
+
+import pytest
+
+from crowdin_api.api_resources.custom_spellcheckers.resource import CustomSpellcheckersResource
+from crowdin_api.requester import APIRequester
+
+
+class TestCustomSpellcheckersResource:
+ resource_class = CustomSpellcheckersResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({}, "custom-spellcheckers"),
+ ({"customSpellcheckerId": 1}, "custom-spellcheckers/1"),
+ ),
+ )
+ def test_get_custom_spellcheckers_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_custom_spellcheckers_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"offset": 0, "limit": 25}),
+ ({"offset": 10, "limit": 5}, {"offset": 10, "limit": 5}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_custom_spellcheckers(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_custom_spellcheckers(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="custom-spellcheckers",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_custom_spellchecker(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_custom_spellchecker(customSpellcheckerId=1) == "response"
+ m_request.assert_called_once_with(method="get", path="custom-spellcheckers/1")
diff --git a/crowdin_api/api_resources/dictionaries/resource.py b/crowdin_api/api_resources/dictionaries/resource.py
index 6a13466f..eeeda9f5 100644
--- a/crowdin_api/api_resources/dictionaries/resource.py
+++ b/crowdin_api/api_resources/dictionaries/resource.py
@@ -1,7 +1,7 @@
from typing import Iterable, Optional
from crowdin_api.api_resources.abstract.resources import BaseResource
-from crowdin_api.api_resources.dictionaries.types import DictionaryPatchPath
+from crowdin_api.api_resources.dictionaries.types import DictionaryPatchRequest
class DictionariesResource(BaseResource):
@@ -13,7 +13,7 @@ class DictionariesResource(BaseResource):
Use API to get the list of organization dictionaries and to edit a specific dictionary.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Dictionaries
+ https://support.crowdin.com/developer/api/v2/#tag/Dictionaries
"""
def list_dictionaries(
@@ -28,7 +28,7 @@ def list_dictionaries(
List Dictionaries.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.dictionaries.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.dictionaries.getMany
"""
params = self.get_page_params(page=page, offset=offset, limit=limit)
@@ -44,14 +44,19 @@ def list_dictionaries(
def edit_dictionary(
self,
languageId: str,
- data: Iterable[DictionaryPatchPath],
+ data: Iterable[DictionaryPatchRequest],
projectId: Optional[int] = None,
):
"""
Edit Dictionary.
+ :param data: JSON Patch operations ("add"/"remove") on "/words/{index}" paths,
+ e.g. {"op": "add", "path": "/words/-", "value": "word"} or
+ {"op": "remove", "path": "/words/0"}. To remove several words in one request,
+ specify the word indexes in reverse order.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.dictionaries.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.dictionaries.patch
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/dictionaries/tests/test_dictionaries_resources.py b/crowdin_api/api_resources/dictionaries/tests/test_dictionaries_resources.py
index 3bf8d908..44c2b13f 100644
--- a/crowdin_api/api_resources/dictionaries/tests/test_dictionaries_resources.py
+++ b/crowdin_api/api_resources/dictionaries/tests/test_dictionaries_resources.py
@@ -58,9 +58,14 @@ def test_edit_custom_language(self, m_request, base_absolut_url):
data = [
{
- "op": PatchOperation.REPLACE,
+ "op": PatchOperation.ADD,
+ "path": "/words/-",
+ "value": "word",
+ },
+ {
+ "op": PatchOperation.REMOVE,
"path": "/words/0",
- }
+ },
]
resource = self.get_resource(base_absolut_url)
diff --git a/crowdin_api/api_resources/dictionaries/types.py b/crowdin_api/api_resources/dictionaries/types.py
index 988654a1..9fa1c03a 100644
--- a/crowdin_api/api_resources/dictionaries/types.py
+++ b/crowdin_api/api_resources/dictionaries/types.py
@@ -1,3 +1,5 @@
+from typing import Iterable, Union
+
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.typing import TypedDict
@@ -5,3 +7,15 @@
class DictionaryPatchPath(TypedDict):
op: PatchOperation
path: str
+
+
+class DictionaryPatchRequest(DictionaryPatchPath, total=False):
+ """
+ Dictionary JSON Patch operation.
+
+ `op` is "add" or "remove". `path` points to a word, e.g. "/words/0" (word index) or
+ "/words/-" (append, with "add"). `value` is required for "add" (a word or a list of words).
+ To remove several words in one request, specify the word indexes in reverse order.
+ """
+
+ value: Union[str, Iterable[str]]
diff --git a/crowdin_api/api_resources/distributions/enums.py b/crowdin_api/api_resources/distributions/enums.py
index e0a50447..057f1aba 100644
--- a/crowdin_api/api_resources/distributions/enums.py
+++ b/crowdin_api/api_resources/distributions/enums.py
@@ -2,9 +2,9 @@
class DistributionPatchPath(Enum):
- EXPORT_MODE = "/exportMode"
+ EXPORT_MODE = "/exportMode" # file-based projects only; `exportMode` is deprecated
NAME = "/name"
- FILE_IDS = "/fileIds"
+ FILE_IDS = "/fileIds" # file-based projects only; `fileIds` is deprecated, use `/bundleIds`
BUNDLE_IDS = "/bundleIds"
diff --git a/crowdin_api/api_resources/distributions/resource.py b/crowdin_api/api_resources/distributions/resource.py
index 3fd89ff9..79890bd3 100644
--- a/crowdin_api/api_resources/distributions/resource.py
+++ b/crowdin_api/api_resources/distributions/resource.py
@@ -11,7 +11,7 @@ class DistributionsResource(BaseResource):
Resource for Distributions.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Distributions
+ https://support.crowdin.com/developer/api/v2/#tag/Distributions
"""
def get_distributions_path(self, projectId: int, hash: Optional[str] = None):
@@ -30,7 +30,7 @@ def list_distributions(
List Distributions.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.getMany
"""
projectId = projectId or self.get_project_id()
@@ -52,8 +52,13 @@ def add_distribution(
"""
Add Distribution.
+ :param fileIds: Deprecated, use `bundleIds` instead. File-based projects only.
+ :param bundleIds: Bundles ids. Get via List Bundles. Required for string-based projects.
+ :param exportMode: Deprecated, omit it to use the API default behavior.
+ File-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.post
"""
projectId = projectId or self.get_project_id()
@@ -86,7 +91,7 @@ def get_distribution(self, hash: str, projectId: Optional[int] = None):
Get Distribution.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.get
"""
projectId = projectId or self.get_project_id()
@@ -101,7 +106,7 @@ def delete_distribution(self, hash: str, projectId: Optional[int] = None):
Delete Distribution.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.delete
"""
projectId = projectId or self.get_project_id()
@@ -121,7 +126,7 @@ def edit_distribution(
Edit Distribution.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.patch
"""
projectId = projectId or self.get_project_id()
@@ -137,7 +142,7 @@ def get_distribution_release(self, hash: str, projectId: Optional[int] = None):
Get Distribution Release.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.release.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.release.get
"""
projectId = projectId or self.get_project_id()
@@ -152,7 +157,7 @@ def release_distribution(self, hash: str, projectId: Optional[int] = None):
Release Distribution.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.release.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.distributions.release.post
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/external_qa_checks/__init__.py b/crowdin_api/api_resources/external_qa_checks/__init__.py
new file mode 100644
index 00000000..9f77d639
--- /dev/null
+++ b/crowdin_api/api_resources/external_qa_checks/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {'tests': False}
diff --git a/crowdin_api/api_resources/external_qa_checks/resource.py b/crowdin_api/api_resources/external_qa_checks/resource.py
new file mode 100644
index 00000000..298924a6
--- /dev/null
+++ b/crowdin_api/api_resources/external_qa_checks/resource.py
@@ -0,0 +1,55 @@
+from typing import Optional
+
+from crowdin_api.api_resources.abstract.resources import BaseResource
+
+
+class ExternalQaChecksResource(BaseResource):
+ """
+ Resource for External QA Checks.
+
+ Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/External-QA-Checks
+ """
+
+ def get_external_qa_checks_path(self, externalQaCheckId: Optional[int] = None):
+ if externalQaCheckId is not None:
+ return f"external-qa-checks/{externalQaCheckId}"
+
+ return "external-qa-checks"
+
+ def list_external_qa_checks(
+ self,
+ projectId: Optional[int] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List External QA Checks.
+
+ :param projectId: Filter External QA Checks by Project Identifier
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.external-qa-checks.getMany
+ """
+ params = {"projectId": projectId}
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_external_qa_checks_path(),
+ params=params,
+ )
+
+ def get_external_qa_check(self, externalQaCheckId: int):
+ """
+ Get External QA Check.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.external-qa-checks.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_external_qa_checks_path(externalQaCheckId=externalQaCheckId),
+ )
diff --git a/crowdin_api/api_resources/external_qa_checks/tests/test_external_qa_checks_resources.py b/crowdin_api/api_resources/external_qa_checks/tests/test_external_qa_checks_resources.py
new file mode 100644
index 00000000..04051581
--- /dev/null
+++ b/crowdin_api/api_resources/external_qa_checks/tests/test_external_qa_checks_resources.py
@@ -0,0 +1,54 @@
+from unittest import mock
+
+import pytest
+
+from crowdin_api.api_resources.external_qa_checks.resource import ExternalQaChecksResource
+from crowdin_api.requester import APIRequester
+
+
+class TestExternalQaChecksResource:
+ resource_class = ExternalQaChecksResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({}, "external-qa-checks"),
+ ({"externalQaCheckId": 1}, "external-qa-checks/1"),
+ ),
+ )
+ def test_get_external_qa_checks_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_external_qa_checks_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"projectId": None, "offset": 0, "limit": 25}),
+ (
+ {"projectId": 1, "offset": 10, "limit": 5},
+ {"projectId": 1, "offset": 10, "limit": 5},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_external_qa_checks(self, m_request, in_params, request_params, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_external_qa_checks(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="external-qa-checks",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_external_qa_check(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_external_qa_check(externalQaCheckId=1) == "response"
+ m_request.assert_called_once_with(method="get", path="external-qa-checks/1")
diff --git a/crowdin_api/api_resources/fields/enums.py b/crowdin_api/api_resources/fields/enums.py
index b289904b..7d29a776 100644
--- a/crowdin_api/api_resources/fields/enums.py
+++ b/crowdin_api/api_resources/fields/enums.py
@@ -8,6 +8,8 @@ class FieldEntity(Enum):
FILE = "file"
TRANSLATION = "translation"
STRING = "string"
+ TERM = "term"
+ CONCEPT = "concept"
class FieldType(Enum):
@@ -32,6 +34,7 @@ class FieldPlace(Enum):
PROJECT_SETTINGS = "projectSettings"
PROJECT_TASK_EDIT_CREATE = "projectTaskEditCreate"
PROJECT_TASK_DETAILS = "projectTaskDetails"
+ PROJECT_TASK_BOARD_CARD = "projectTaskBoardCard"
FILE_DETAILS = "fileDetails"
FILE_SETTINGS = "fileSettings"
USER_EDIT_MODAL = "userEditModal"
@@ -40,6 +43,10 @@ class FieldPlace(Enum):
STRING_EDIT_MODAL = "stringEditModal"
STRING_DETAILS = "stringDetails"
TRANSLATION_UNDER_CONTENT = "translationUnderContent"
+ TERM_DETAILS = "termDetails"
+ TERM_EDIT_MODAL = "termEditModal"
+ CONCEPT_DETAILS = "conceptDetails"
+ CONCEPT_EDIT_MODAL = "conceptEditModal"
class FieldOperations(Enum):
@@ -48,3 +55,6 @@ class FieldOperations(Enum):
class FieldsPatchPath(Enum):
NAME = "/name"
+ DESCRIPTION = "/description"
+ CONFIG = "/config"
+ ENTITIES = "/entities"
diff --git a/crowdin_api/api_resources/fields/resource.py b/crowdin_api/api_resources/fields/resource.py
index b526796e..6874d2b9 100644
--- a/crowdin_api/api_resources/fields/resource.py
+++ b/crowdin_api/api_resources/fields/resource.py
@@ -15,7 +15,7 @@ class FieldsResource(BaseResource):
Resource for Fields.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Fields
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Fields
"""
def get_fields_path(self, fieldId: Optional[int] = None):
@@ -35,7 +35,7 @@ def list_fields(
List Fields
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.fields.getMany
"""
params = {"search": search, "entity": entity, "type": type}
params.update(self.get_page_params(limit=limit, offset=offset))
@@ -59,7 +59,7 @@ def add_field(
Add Field
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.fields.post
"""
data = {
"name": name,
@@ -78,8 +78,8 @@ def get_field(self, fieldId: int):
"""
Get Field
- Link to documentaion:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.get
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.fields.get
"""
return self.requester.request(
@@ -90,8 +90,8 @@ def delete_field(self, fieldId: int):
"""
Delete Field
- Link to documetation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.delete
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.fields.delete
"""
return self.requester.request(
@@ -103,7 +103,7 @@ def edit_field(self, fieldId: int, data: Iterable[FieldPatchRequest]):
Edit Field
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.fields.patch
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/fields/tests/test_fields_resources.py b/crowdin_api/api_resources/fields/tests/test_fields_resources.py
index 87d9b20c..44287fe4 100644
--- a/crowdin_api/api_resources/fields/tests/test_fields_resources.py
+++ b/crowdin_api/api_resources/fields/tests/test_fields_resources.py
@@ -218,7 +218,17 @@ def test_edit_field(self, m_request, base_absolut_url):
"op": FieldOperations.REPLACE,
"path": FieldsPatchPath.NAME,
"value": "test",
- }
+ },
+ {
+ "op": FieldOperations.REPLACE,
+ "path": FieldsPatchPath.ENTITIES,
+ "value": [FieldEntity.TERM, FieldEntity.CONCEPT],
+ },
+ {
+ "op": FieldOperations.REPLACE,
+ "path": FieldsPatchPath.CONFIG,
+ "value": {"locations": [{"place": FieldPlace.TERM_DETAILS}]},
+ },
]
fieldId = 1
diff --git a/crowdin_api/api_resources/fields/types.py b/crowdin_api/api_resources/fields/types.py
index 22892a52..0e33875f 100644
--- a/crowdin_api/api_resources/fields/types.py
+++ b/crowdin_api/api_resources/fields/types.py
@@ -18,7 +18,7 @@ class FieldLocation(TypedDict):
class ListFieldConfig(TypedDict):
options: Iterable[FieldOptions]
- locations: Iterable[FieldPlace]
+ locations: Iterable[FieldLocation]
class NumberFieldConfig(TypedDict):
diff --git a/crowdin_api/api_resources/glossaries/enums.py b/crowdin_api/api_resources/glossaries/enums.py
index f655c58a..a74075b6 100644
--- a/crowdin_api/api_resources/glossaries/enums.py
+++ b/crowdin_api/api_resources/glossaries/enums.py
@@ -3,6 +3,9 @@
class GlossaryPatchPath(Enum):
NAME = "/name"
+ LANGUAGE_ID = "/languageId"
+ IS_SHARED = "/isShared"
+ GROUP_ID = "/groupId" # Enterprise only
class GlossaryFormat(Enum):
@@ -21,6 +24,11 @@ class GlossaryExportFields(Enum):
GENDER = "gender"
NOTE = "note"
URL = "url"
+ CONCEPT_DEFINITION = "conceptDefinition"
+ CONCEPT_SUBJECT = "conceptSubject"
+ CONCEPT_NOTE = "conceptNote"
+ CONCEPT_URL = "conceptUrl"
+ CONCEPT_FIGURE = "conceptFigure"
class GlossaryExportType(Enum):
@@ -81,6 +89,7 @@ class TermPatchPath(Enum):
GENDER = "/gender"
URL = "/url"
NOTE = "/note"
+ FIELDS = "/fields" # Enterprise only
class TermPartOfSpeech(Enum):
@@ -106,6 +115,7 @@ class TermStatus(Enum):
ADMITTED = "admitted"
NOT_RECOMMEND = "not recommended"
OBSOLETE = "obsolete"
+ DRAFT = "draft"
class TermType(Enum):
diff --git a/crowdin_api/api_resources/glossaries/resource.py b/crowdin_api/api_resources/glossaries/resource.py
index 8a1b393c..671f008a 100644
--- a/crowdin_api/api_resources/glossaries/resource.py
+++ b/crowdin_api/api_resources/glossaries/resource.py
@@ -1,4 +1,4 @@
-from typing import Dict, Iterable, Optional
+from typing import Any, Dict, Iterable, Optional
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.glossaries.enums import (
@@ -28,7 +28,7 @@ class GlossariesResource(BaseResource):
operations and shall be completed with sequence of API methods.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Glossaries
+ https://support.crowdin.com/developer/api/v2/#tag/Glossaries
"""
# Glossaries
@@ -45,15 +45,22 @@ def list_glossaries(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ userId: Optional[int] = None,
+ filter: Optional[str] = None,
):
"""
List Glossaries.
+ :param groupId: Group Identifier. Set 0 to see glossaries of root group.
+ Crowdin Enterprise only.
+ :param userId: List user glossaries. Crowdin only.
+ :param filter: Filter glossaries by `name`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.getMany
"""
- params = {"orderBy": orderBy, "groupId": groupId}
+ params = {"orderBy": orderBy, "groupId": groupId, "userId": userId, "filter": filter}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
return self._get_entire_data(
@@ -62,18 +69,34 @@ def list_glossaries(
params=params,
)
- def add_glossary(self, name: str, languageId: str):
+ def add_glossary(
+ self,
+ name: str,
+ languageId: str,
+ isShared: Optional[bool] = None,
+ groupId: Optional[int] = None,
+ ):
"""
Add Glossary.
+ :param isShared: Whether the glossary should be shared to all projects within the account
+ (Crowdin) or within the group (Crowdin Enterprise).
+ :param groupId: Group Identifier. If 0 – the glossary will be available for all projects
+ and groups in the workspace. Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.post
"""
return self.requester.request(
method="post",
path=self.get_glossaries_path(),
- request_data={"name": name, "languageId": languageId},
+ request_data={
+ "name": name,
+ "languageId": languageId,
+ "isShared": isShared,
+ "groupId": groupId,
+ },
)
def get_glossary(self, glossaryId: int):
@@ -81,7 +104,7 @@ def get_glossary(self, glossaryId: int):
Get Glossary.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.get
"""
return self.requester.request(
@@ -94,7 +117,7 @@ def delete_glossary(self, glossaryId: int):
Delete Glossary.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.delete
"""
return self.requester.request(
@@ -106,8 +129,10 @@ def edit_glossary(self, glossaryId: int, data: Iterable[GlossaryPatchRequest]):
"""
Edit Glossary.
+ `GlossaryPatchPath.GROUP_ID` (`/groupId`) is Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.patch
"""
return self.requester.request(
@@ -128,7 +153,7 @@ def export_glossary(self, glossaryId: int, data: Optional[GlossarySchemaRequest]
Export Glossary.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.exports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.exports.post
"""
return self.requester.request(
@@ -142,7 +167,7 @@ def check_glossary_export_status(self, glossaryId: int, exportId: str):
Check Glossary Export Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.exports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.exports.get
"""
return self.requester.request(
@@ -155,7 +180,7 @@ def download_glossary(self, glossaryId: int, exportId: str):
Download Glossary.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.exports.download.download
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.exports.download.download
"""
glossary_export_path = self.get_glossary_export_path(
@@ -179,7 +204,7 @@ def import_glossary(
Import Glossary.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.imports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.imports.post
"""
return self.requester.request(
@@ -197,7 +222,7 @@ def check_glossary_import_status(self, glossaryId: int, importId: str):
Check Glossary Import Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.imports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.imports.get
"""
return self.requester.request(
@@ -216,7 +241,7 @@ def concordance_search_in_glossaries(
Concordance search in Glossaries
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.glossaries.concordance.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.glossaries.concordance.post
"""
data = {
"sourceLanguageId": sourceLanguageId,
@@ -237,10 +262,10 @@ def organization_concordance_search(self, request_data: OrganizationConcordanceS
Concordance search in organization glossaries.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.concordance.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.concordance.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.glossaries.concordance.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.glossaries.concordance.post
"""
return self.requester.request(
@@ -266,12 +291,16 @@ def list_terms(
croql: Optional[str] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ translationOfTermId: Optional[int] = None,
):
"""
List Terms.
+ :param translationOfTermId: Filter terms by `termId`. Use for terms that have translations.
+ :param croql: Filter terms by CroQL. Can be used only with `orderBy`, `offset` and `limit`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.terms.getMany
"""
params = {
@@ -279,6 +308,7 @@ def list_terms(
"userId": userId,
"languageId": languageId,
"conceptId": conceptId,
+ "translationOfTermId": translationOfTermId,
"croql": croql,
}
@@ -303,12 +333,17 @@ def add_term(
note: Optional[str] = None,
url: Optional[str] = None,
conceptId: Optional[int] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
Add Term.
+ :param conceptId: Defines whether to add translation to the existing term. If not
+ specified, a new concept will be automatically created for the term.
+ :param fields: Custom fields values. Keys get via List Fields. Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.terms.post
"""
return self.requester.request(
@@ -325,6 +360,7 @@ def add_term(
"note": note,
"url": url,
"conceptId": conceptId,
+ "fields": fields,
},
)
@@ -333,12 +369,18 @@ def clear_glossary(
glossaryId: int,
languageId: Optional[str] = None,
conceptId: Optional[int] = None,
+ translationOfTermId: Optional[int] = None,
):
"""
Clear Glossary.
+ Without a filter every term is deleted.
+
+ :param translationOfTermId: Defines whether to delete specific term along with its
+ translations.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.deleteMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.terms.deleteMany
"""
return self.requester.request(
@@ -347,6 +389,7 @@ def clear_glossary(
params={
"languageId": languageId,
"conceptId": conceptId,
+ "translationOfTermId": translationOfTermId,
},
)
@@ -355,7 +398,7 @@ def get_term(self, glossaryId: int, termId: int):
Get Term.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.terms.get
"""
return self.requester.request(
@@ -368,7 +411,7 @@ def delete_term(self, glossaryId: int, termId: int):
Delete Term.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.terms.delete
"""
return self.requester.request(
@@ -380,8 +423,11 @@ def edit_term(self, glossaryId: int, termId: int, data: Iterable[TermPatchReques
"""
Edit Term.
+ `TermPatchPath.FIELDS` (`/fields`) is Crowdin Enterprise only. A `replace` on `/fields`
+ writes the whole set of custom field values.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.terms.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.terms.patch
"""
return self.requester.request(
@@ -407,7 +453,7 @@ def list_concepts(
List Concepts.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.concepts.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.concepts.getMany
"""
params = {"orderBy": orderBy}
@@ -424,7 +470,7 @@ def get_concept(self, glossaryId: int, conceptId: int):
Get Concept.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.concepts.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.concepts.get
"""
return self.requester.request(
@@ -436,18 +482,23 @@ def update_concept(
self,
glossaryId: int,
conceptId: int,
- languagesDetails: Iterable[LanguagesDetails],
+ languagesDetails: Optional[Iterable[LanguagesDetails]] = None,
subject: Optional[str] = None,
definition: Optional[str] = None,
note: Optional[str] = None,
url: Optional[str] = None,
figure: Optional[str] = None,
+ translatable: Optional[bool] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
- Get Concept.
+ Update Concept.
+
+ :param fields: Custom fields values. Keys get via List Fields. Writes the whole set of
+ custom field values. Crowdin Enterprise only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.concepts.put
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.concepts.put
"""
return self.requester.request(
@@ -460,6 +511,8 @@ def update_concept(
"note": note,
"url": url,
"figure": figure,
+ "translatable": translatable,
+ "fields": fields,
},
)
@@ -468,7 +521,7 @@ def delete_concept(self, glossaryId: int, conceptId: int):
Delete Concept.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.glossaries.concepts.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.glossaries.concepts.delete
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/glossaries/tests/test_glossaries_resources.py b/crowdin_api/api_resources/glossaries/tests/test_glossaries_resources.py
index 5b4095a0..f6c267ab 100644
--- a/crowdin_api/api_resources/glossaries/tests/test_glossaries_resources.py
+++ b/crowdin_api/api_resources/glossaries/tests/test_glossaries_resources.py
@@ -59,6 +59,8 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
{
"orderBy": None,
"groupId": None,
+ "userId": None,
+ "filter": None,
"offset": 0,
"limit": 25,
},
@@ -83,6 +85,8 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
]
),
"groupId": 1,
+ "userId": None,
+ "filter": None,
"offset": 0,
"limit": 25,
},
@@ -107,10 +111,23 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
]
),
"groupId": 1,
+ "userId": None,
+ "filter": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ ),
+ (
+ {"userId": 2, "filter": "name"},
+ {
+ "orderBy": None,
+ "groupId": None,
+ "userId": 2,
+ "filter": "name",
"offset": 0,
"limit": 25,
},
- )
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -125,16 +142,29 @@ def test_list_glossaries(self, m_request, incoming_data, request_params, base_ab
params=request_params,
)
+ @pytest.mark.parametrize(
+ "incoming_data, request_data",
+ (
+ (
+ {"name": "test", "languageId": "fr"},
+ {"name": "test", "languageId": "fr", "isShared": None, "groupId": None},
+ ),
+ (
+ {"name": "test", "languageId": "fr", "isShared": True, "groupId": 2},
+ {"name": "test", "languageId": "fr", "isShared": True, "groupId": 2},
+ ),
+ ),
+ )
@mock.patch("crowdin_api.requester.APIRequester.request")
- def test_add_glossary(self, m_request, base_absolut_url):
+ def test_add_glossary(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_glossary(name="test", languageId="fr") == "response"
+ assert resource.add_glossary(**incoming_data) == "response"
m_request.assert_called_once_with(
method="post",
path=resource.get_glossaries_path(),
- request_data={"name": "test", "languageId": "fr"},
+ request_data=request_data,
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -166,7 +196,22 @@ def test_edit_glossary(self, m_request, base_absolut_url):
"value": "test",
"op": PatchOperation.REPLACE,
"path": GlossaryPatchPath.NAME,
- }
+ },
+ {
+ "value": "uk",
+ "op": PatchOperation.REPLACE,
+ "path": GlossaryPatchPath.LANGUAGE_ID,
+ },
+ {
+ "value": True,
+ "op": PatchOperation.REPLACE,
+ "path": GlossaryPatchPath.IS_SHARED,
+ },
+ {
+ "value": 1,
+ "op": PatchOperation.REPLACE,
+ "path": GlossaryPatchPath.GROUP_ID,
+ },
]
resource = self.get_resource(base_absolut_url)
@@ -258,6 +303,32 @@ def test_get_glossary_export_path(self, in_params, path, base_absolut_url):
"authorId": 12,
},
),
+ (
+ {
+ "text": "value",
+ "caseSensitive": True,
+ "searchStrict": True,
+ "searchFullMatch": False,
+ "dateFrom": "2024-01-23T07:00:14+00:00",
+ "dateTo": "2024-09-27T07:00:14+00:00",
+ "exportFields": [
+ GlossaryExportFields.CONCEPT_DEFINITION,
+ "field_custom-field",
+ ],
+ },
+ {
+ "text": "value",
+ "caseSensitive": True,
+ "searchStrict": True,
+ "searchFullMatch": False,
+ "dateFrom": "2024-01-23T07:00:14+00:00",
+ "dateTo": "2024-09-27T07:00:14+00:00",
+ "exportFields": [
+ GlossaryExportFields.CONCEPT_DEFINITION,
+ "field_custom-field",
+ ],
+ },
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -404,6 +475,7 @@ def test_get_terms_path(self, in_params, path, base_absolut_url):
"userId": None,
"languageId": None,
"conceptId": None,
+ "translationOfTermId": None,
"croql": None,
"offset": 0,
"limit": 25,
@@ -417,6 +489,7 @@ def test_get_terms_path(self, in_params, path, base_absolut_url):
"userId": 1,
"languageId": "ua",
"conceptId": 2,
+ "translationOfTermId": 3,
"croql": "status = 'preferred'",
},
{
@@ -426,6 +499,7 @@ def test_get_terms_path(self, in_params, path, base_absolut_url):
"userId": 1,
"languageId": "ua",
"conceptId": 2,
+ "translationOfTermId": 3,
"croql": "status = 'preferred'",
"offset": 0,
"limit": 25,
@@ -460,6 +534,7 @@ def test_list_terms(self, m_request, incoming_data, request_params, base_absolut
"note": None,
"url": None,
"conceptId": None,
+ "fields": None,
},
),
(
@@ -468,24 +543,26 @@ def test_list_terms(self, m_request, incoming_data, request_params, base_absolut
"text": "text",
"description": "description",
"partOfSpeech": TermPartOfSpeech.PARTICLE,
- "status": TermStatus.ADMITTED,
+ "status": TermStatus.DRAFT,
"type": TermType.SHORT_FORM,
"gender": TermGender.MASCULINE,
"note": "text",
"url": "https://test.test.com",
"conceptId": 1,
+ "fields": {"some-field": "value"},
},
{
"languageId": "ua",
"text": "text",
"description": "description",
"partOfSpeech": TermPartOfSpeech.PARTICLE,
- "status": TermStatus.ADMITTED,
+ "status": TermStatus.DRAFT,
"type": TermType.SHORT_FORM,
"gender": TermGender.MASCULINE,
"note": "text",
"url": "https://test.test.com",
"conceptId": 1,
+ "fields": {"some-field": "value"},
},
),
),
@@ -507,11 +584,11 @@ def test_add_term(self, m_request, incoming_data, request_data, base_absolut_url
(
(
{},
- {"languageId": None, "conceptId": None},
+ {"languageId": None, "conceptId": None, "translationOfTermId": None},
),
(
- {"languageId": "ua", "conceptId": 1},
- {"languageId": "ua", "conceptId": 1},
+ {"languageId": "ua", "conceptId": 1, "translationOfTermId": 2},
+ {"languageId": "ua", "conceptId": 1, "translationOfTermId": 2},
),
),
)
@@ -556,7 +633,12 @@ def test_edit_term(self, m_request, base_absolut_url):
"value": "test",
"op": PatchOperation.REPLACE,
"path": TermPatchPath.TEXT,
- }
+ },
+ {
+ "value": {"some-field": "value"},
+ "op": PatchOperation.REPLACE,
+ "path": TermPatchPath.FIELDS,
+ },
]
resource = self.get_resource(base_absolut_url)
@@ -648,6 +730,21 @@ def test_get_concept(self, m_request, base_absolut_url):
"note": None,
"url": None,
"figure": None,
+ "translatable": None,
+ "fields": None,
+ },
+ ),
+ (
+ {},
+ {
+ "languagesDetails": None,
+ "subject": None,
+ "definition": None,
+ "note": None,
+ "url": None,
+ "figure": None,
+ "translatable": None,
+ "fields": None,
},
),
(
@@ -660,6 +757,8 @@ def test_get_concept(self, m_request, base_absolut_url):
"note": "Any concept-level note information",
"url": "https://test.test.com",
"figure": "string",
+ "translatable": False,
+ "fields": {"some-field": 12},
},
{
"languagesDetails": [
@@ -670,6 +769,8 @@ def test_get_concept(self, m_request, base_absolut_url):
"note": "Any concept-level note information",
"url": "https://test.test.com",
"figure": "string",
+ "translatable": False,
+ "fields": {"some-field": 12},
},
),
),
diff --git a/crowdin_api/api_resources/glossaries/types.py b/crowdin_api/api_resources/glossaries/types.py
index df5861e5..c6b42fe9 100644
--- a/crowdin_api/api_resources/glossaries/types.py
+++ b/crowdin_api/api_resources/glossaries/types.py
@@ -1,4 +1,4 @@
-from typing import Any, Iterable, Optional
+from typing import Any, Iterable, Optional, Union
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.api_resources.glossaries.enums import (
@@ -23,7 +23,8 @@ class GlossaryPatchRequest(TypedDict):
class GlossarySchemaRequest(TypedDict):
format: Optional[GlossaryFormat]
- exportFields: Optional[Iterable[GlossaryExportFields]]
+ # Enterprise also accepts custom field slugs: `field_{fieldSlug}` / `conceptField_{fieldSlug}`
+ exportFields: Optional[Iterable[Union[GlossaryExportFields, str]]]
exportType: Optional[GlossaryExportType]
statuses: Optional[Iterable[GlossaryExportStatus]]
partsOfSpeech: Optional[Iterable[GlossaryExportPartOfSpeech]]
@@ -31,6 +32,14 @@ class GlossarySchemaRequest(TypedDict):
genders: Optional[Iterable[GlossaryExportGender]]
authorIds: Optional[Iterable[int]]
languageIds: Optional[Iterable[str]]
+ text: Optional[str]
+ # caseSensitive, searchStrict and searchFullMatch must be used together with `text`
+ caseSensitive: Optional[bool]
+ searchStrict: Optional[bool]
+ searchFullMatch: Optional[bool]
+ # UTC, ISO 8601, e.g. "2024-01-23T07:00:14+00:00"
+ dateFrom: Optional[str]
+ dateTo: Optional[str]
# Deprecated in favor of the plural filters above; the API rejects requests combining a
# singular filter with its plural counterpart.
status: Optional[GlossaryExportStatus]
@@ -44,7 +53,7 @@ class OrganizationConcordanceSearchRequest(TypedDict):
sourceLanguageId: str
targetLanguageId: str
expressions: Iterable[str]
- userId: Optional[int]
+ userId: Optional[int] # Crowdin only
class TermPatchRequest(TypedDict):
diff --git a/crowdin_api/api_resources/groups/enums.py b/crowdin_api/api_resources/groups/enums.py
index c2f09b43..dd469ecf 100644
--- a/crowdin_api/api_resources/groups/enums.py
+++ b/crowdin_api/api_resources/groups/enums.py
@@ -5,6 +5,7 @@ class GroupPatchPath(Enum):
NAME = "/name"
DESCRIPTION = "/description"
PARENT_ID = "/parentId"
+ SAVINGS_REPORT_SETTINGS_TEMPLATE_ID = "/savingsReportSettingsTemplateId"
class ListGroupsOrderBy(Enum):
diff --git a/crowdin_api/api_resources/groups/resource.py b/crowdin_api/api_resources/groups/resource.py
index 339e4dbf..a5d72cf6 100644
--- a/crowdin_api/api_resources/groups/resource.py
+++ b/crowdin_api/api_resources/groups/resource.py
@@ -16,7 +16,7 @@ class GroupsResource(BaseResource):
organization if required.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Projects-and-Groups
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Projects-and-Groups
"""
# Glossaries
@@ -31,7 +31,7 @@ def get_group(self, groupId: int):
Get Group.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.get
"""
return self.requester.request(
@@ -43,13 +43,14 @@ def add_group(
self,
name: str,
parentId: Optional[int] = None,
- description: Optional[str] = None
+ description: Optional[str] = None,
+ savingsReportSettingsTemplateId: Optional[int] = None,
):
"""
Add Group.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.post
"""
return self.requester.request(
@@ -58,7 +59,8 @@ def add_group(
request_data={
"name": name,
"parentId": parentId,
- "description": description
+ "description": description,
+ "savingsReportSettingsTemplateId": savingsReportSettingsTemplateId,
}
)
@@ -67,16 +69,19 @@ def list_groups(
orderBy: Optional[Sorting] = None,
parentId: Optional[int] = None,
limit: Optional[int] = None,
- offset: Optional[int] = None
+ offset: Optional[int] = None,
+ filter: Optional[str] = None,
):
"""
List Groups.
+ :param filter: Filter groups by `name`
+
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.getMany
"""
- params = {"orderBy": orderBy, "parentId": parentId}
+ params = {"orderBy": orderBy, "parentId": parentId, "filter": filter}
params.update(self.get_page_params(offset=offset, limit=limit))
return self._get_entire_data(
@@ -90,7 +95,7 @@ def edit_group(self, groupId: int, data: Iterable[GroupPatchRequest]):
Edit Group.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.patch
"""
return self.requester.request(
@@ -104,7 +109,7 @@ def delete_group(self, groupId: int):
Delete Group.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.delete
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/groups/tests/test_groups_resources.py b/crowdin_api/api_resources/groups/tests/test_groups_resources.py
index 3ec8adcf..35910a99 100644
--- a/crowdin_api/api_resources/groups/tests/test_groups_resources.py
+++ b/crowdin_api/api_resources/groups/tests/test_groups_resources.py
@@ -54,6 +54,7 @@ def test_delete_group(self, m_request, base_absolut_url):
{
"orderBy": None,
"parentId": None,
+ "filter": None,
"limit": 25,
"offset": 0,
},
@@ -64,6 +65,7 @@ def test_delete_group(self, m_request, base_absolut_url):
[SortingRule(ListGroupsOrderBy.NAME, SortingOrder.DESC)]
),
"parentId": "test",
+ "filter": "name",
"limit": 10,
"offset": 2,
},
@@ -72,6 +74,7 @@ def test_delete_group(self, m_request, base_absolut_url):
[SortingRule(ListGroupsOrderBy.NAME, SortingOrder.DESC)]
),
"parentId": "test",
+ "filter": "name",
"limit": 10,
"offset": 2,
},
@@ -99,6 +102,7 @@ def test_list_groups(self, m_request, incoming_data, request_params, base_absolu
"name": "test_name",
"parentId": None,
"description": None,
+ "savingsReportSettingsTemplateId": None,
},
),
(
@@ -106,11 +110,13 @@ def test_list_groups(self, m_request, incoming_data, request_params, base_absolu
"name": "test_name",
"parentId": 2,
"description": "some text",
+ "savingsReportSettingsTemplateId": 3,
},
{
"name": "test_name",
"parentId": 2,
"description": "some text",
+ "savingsReportSettingsTemplateId": 3,
},
),
),
diff --git a/crowdin_api/api_resources/labels/resource.py b/crowdin_api/api_resources/labels/resource.py
index 2caef915..9729e678 100644
--- a/crowdin_api/api_resources/labels/resource.py
+++ b/crowdin_api/api_resources/labels/resource.py
@@ -1,8 +1,9 @@
-from typing import Iterable, Optional
+from typing import Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.labels.types import LabelsPatchRequest
from crowdin_api.sorting import Sorting
+from crowdin_api.utils import convert_to_query_list
class LabelsResource(BaseResource):
@@ -10,7 +11,10 @@ class LabelsResource(BaseResource):
Resource for Labels.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Labels
+ https://support.crowdin.com/developer/api/v2/#tag/Labels
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Labels
"""
def get_labels_path(self, projectId: int, labelId: Optional[int] = None):
@@ -26,16 +30,23 @@ def list_labels(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ isSystem: Optional[Union[bool, int]] = None,
):
"""
List Labels.
+ `isSystem` (filter by system labels) is for string-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.getMany
+ https://support.crowdin.com/developer/api/v2/string-based/#operation/api.projects.labels.getMany
"""
projectId = projectId or self.get_project_id()
- params = {"orderBy": orderBy}
+ params = {
+ "orderBy": orderBy,
+ "isSystem": None if isSystem is None else int(isSystem),
+ }
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
return self._get_entire_data(
@@ -49,7 +60,7 @@ def add_label(self, title: str, projectId: Optional[int] = None):
Add Label.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.post
"""
projectId = projectId or self.get_project_id()
@@ -65,7 +76,7 @@ def get_label(self, labelId: int, projectId: Optional[int] = None):
Get Label.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.get
"""
projectId = projectId or self.get_project_id()
@@ -80,7 +91,7 @@ def delete_label(self, labelId: int, projectId: Optional[int] = None):
Delete Label.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.delete
"""
projectId = projectId or self.get_project_id()
@@ -100,7 +111,7 @@ def edit_label(
Edit Label.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.patch
"""
projectId = projectId or self.get_project_id()
@@ -117,15 +128,15 @@ def get_screenshots_path(self, project_id: int, label_id: int):
def assign_label_to_screenshots(
self,
label_id: int,
- screenshot_ids: Iterable[int],
+ screenshot_ids: Union[int, Iterable[int]],
project_id: Optional[int] = None,
):
"""
Assign Label to Screenshots
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.screenshots.post
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.labels.screenshots.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.screenshots.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.labels.screenshots.post
"""
project_id = project_id or self.get_project_id()
@@ -134,64 +145,78 @@ def assign_label_to_screenshots(
method="post",
path=self.get_screenshots_path(project_id, label_id),
request_data={
- "screenshotIds": screenshot_ids
+ "screenshotIds": [screenshot_ids] if isinstance(screenshot_ids, int) else screenshot_ids
}
)
def unassign_label_from_screenshots(
self,
label_id: int,
- screenshot_ids: Iterable[int],
+ screenshot_ids: Union[int, Iterable[int]],
project_id: Optional[int] = None,
):
"""
Unassign Label from Screenshots
+ `screenshot_ids` can be a single screenshot identifier or an iterable of them
+ (up to 500 at a time).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.screenshots.deleteMany
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.labels.screenshots.deleteMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.screenshots.deleteMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.labels.screenshots.deleteMany
"""
project_id = project_id or self.get_project_id()
- query = ",".join(str(screenshot_id) for screenshot_id in screenshot_ids)
return self.requester.request(
method="delete",
- path=f"{self.get_screenshots_path(project_id, label_id)}?screenshotIds={query}"
+ params={"screenshotIds": convert_to_query_list(screenshot_ids)},
+ path=self.get_screenshots_path(project_id, label_id),
)
def assign_label_to_strings(
- self, labelId: int, stringIds: Iterable[int], projectId: Optional[int] = None
+ self,
+ labelId: int,
+ stringIds: Union[int, Iterable[int]],
+ projectId: Optional[int] = None,
):
"""
Assign Label to Strings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.strings.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.strings.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.labels.strings.post
"""
projectId = projectId or self.get_project_id()
return self.requester.request(
method="post",
- request_data={"stringIds": stringIds},
+ request_data={"stringIds": [stringIds] if isinstance(stringIds, int) else stringIds},
path=f"{self.get_labels_path(projectId=projectId, labelId=labelId)}/strings",
)
def unassign_label_from_strings(
- self, labelId: int, stringIds: Iterable[int], projectId: Optional[int] = None
+ self,
+ labelId: int,
+ stringIds: Union[int, Iterable[int]],
+ projectId: Optional[int] = None,
):
"""
Unassign Label from Strings.
+ `stringIds` can be a single string identifier or an iterable of them
+ (up to 500 at a time).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.labels.strings.deleteMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.labels.strings.deleteMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.labels.strings.deleteMany
"""
projectId = projectId or self.get_project_id()
return self.requester.request(
method="delete",
- params={"stringIds": ",".join(str(stringId) for stringId in stringIds)},
+ params={"stringIds": convert_to_query_list(stringIds)},
path=f"{self.get_labels_path(projectId=projectId, labelId=labelId)}/strings",
)
diff --git a/crowdin_api/api_resources/labels/tests/test_labels_resources.py b/crowdin_api/api_resources/labels/tests/test_labels_resources.py
index 035dfd30..a0288c9e 100644
--- a/crowdin_api/api_resources/labels/tests/test_labels_resources.py
+++ b/crowdin_api/api_resources/labels/tests/test_labels_resources.py
@@ -39,6 +39,25 @@ def test_get_labels_path(self, in_params, path, base_absolut_url):
{},
{
"orderBy": None,
+ "isSystem": None,
+ "limit": 25,
+ "offset": 0,
+ },
+ ),
+ (
+ {"isSystem": True},
+ {
+ "orderBy": None,
+ "isSystem": 1,
+ "limit": 25,
+ "offset": 0,
+ },
+ ),
+ (
+ {"isSystem": 0},
+ {
+ "orderBy": None,
+ "isSystem": 0,
"limit": 25,
"offset": 0,
},
@@ -55,6 +74,7 @@ def test_get_labels_path(self, in_params, path, base_absolut_url):
"orderBy": Sorting(
[SortingRule(ListLabelsOrderBy.ID, SortingOrder.DESC)]
),
+ "isSystem": None,
"limit": 25,
"offset": 0,
},
@@ -62,7 +82,7 @@ def test_get_labels_path(self, in_params, path, base_absolut_url):
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
- def test_list_translation_approvals(
+ def test_list_labels(
self, m_request, incoming_data, request_params, base_absolut_url
):
m_request.return_value = "response"
@@ -135,7 +155,13 @@ def test_edit_edit_label(self, m_request, base_absolut_url):
{
"screenshotIds": [1, 2, 3]
}
- )
+ ),
+ (
+ 5,
+ {
+ "screenshotIds": [5]
+ }
+ ),
]
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -158,7 +184,15 @@ def test_assign_label_to_screenshots(self, m_request, in_params, body, base_abso
(
[1, 2, 3],
"1,2,3"
- )
+ ),
+ (
+ (4, 5),
+ "4,5"
+ ),
+ (
+ 7,
+ 7
+ ),
]
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -171,7 +205,8 @@ def test_unassign_label_to_screenshots(self, m_request, in_params, query_string,
)
m_request.assert_called_once_with(
method="delete",
- path=f"{resource.get_screenshots_path(1, 2)}?screenshotIds={query_string}"
+ params={"screenshotIds": query_string},
+ path=resource.get_screenshots_path(1, 2),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -202,3 +237,39 @@ def test_unassign_label_from_strings(self, m_request, base_absolut_url):
method="delete",
path=f"{resource.get_labels_path(projectId=1, labelId=2)}/strings",
)
+
+ @pytest.mark.parametrize(
+ "in_params, query_string",
+ [
+ ([1, 2], "1,2"),
+ (3, 3),
+ ]
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_unassign_label_from_strings_single_and_many(
+ self, m_request, in_params, query_string, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.unassign_label_from_strings(projectId=1, labelId=2, stringIds=in_params)
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ params={"stringIds": query_string},
+ method="delete",
+ path="projects/1/labels/2/strings",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_assign_label_to_strings_single_id(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.assign_label_to_strings(projectId=1, labelId=2, stringIds=3) == "response"
+ m_request.assert_called_once_with(
+ request_data={"stringIds": [3]},
+ method="post",
+ path="projects/1/labels/2/strings",
+ )
diff --git a/crowdin_api/api_resources/languages/resource.py b/crowdin_api/api_resources/languages/resource.py
index bc245743..2189cc95 100644
--- a/crowdin_api/api_resources/languages/resource.py
+++ b/crowdin_api/api_resources/languages/resource.py
@@ -14,7 +14,7 @@ class LanguagesResource(BaseResource):
Use API to get the list of all supported languages and retrieve additional details
(e.g. text direction, internal code) on specific language.
- Link to documentation: https://developer.crowdin.com/api/v2/#tag/Languages
+ Link to documentation: https://support.crowdin.com/developer/api/v2/#tag/Languages
"""
def get_languages_path(self, languageId: Optional[str] = None):
@@ -33,7 +33,7 @@ def list_supported_languages(
List Supported Languages.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.languages.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.languages.getMany
"""
return self._get_entire_data(
@@ -57,7 +57,7 @@ def add_custom_language(
Add Custom Language.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.languages.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.languages.post
"""
return self.requester.request(
@@ -80,7 +80,7 @@ def get_language(self, languageId: str):
Get Language.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.languages.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.languages.get
"""
return self.requester.request(
@@ -92,7 +92,7 @@ def delete_custom_language(self, languageId: str):
Delete Custom Language.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.languages.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.languages.delete
"""
return self.requester.request(
@@ -104,7 +104,7 @@ def edit_custom_language(self, languageId: str, data: Iterable[LanguagesPatchReq
Edit Custom Language.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.languages.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.languages.patch
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/machine_translation_engines/enums.py b/crowdin_api/api_resources/machine_translation_engines/enums.py
index 35749430..08edc4db 100644
--- a/crowdin_api/api_resources/machine_translation_engines/enums.py
+++ b/crowdin_api/api_resources/machine_translation_engines/enums.py
@@ -4,3 +4,22 @@
class LanguageRecognitionProvider(Enum):
CROWDIN = "crowdin"
ENGINE = "engine"
+
+
+class MachineTranslationEngineType(Enum):
+ GOOGLE = "google"
+ GOOGLE_AUTOML = "google_automl"
+ MICROSOFT = "microsoft"
+ DEEPL = "deepl"
+ AMAZON = "amazon"
+ MODERNMT = "modernmt"
+ CUSTOM_MT = "custom_mt"
+
+
+class MachineTranslationEnginePatchPath(Enum):
+ NAME = "/name"
+ TYPE = "/type"
+ CREDENTIALS = "/credentials"
+ ENABLED_LANGUAGE_IDS = "/enabledLanguageIds"
+ ENABLED_PROJECT_IDS = "/enabledProjectIds"
+ IS_ENABLED = "/isEnabled"
diff --git a/crowdin_api/api_resources/machine_translation_engines/resource.py b/crowdin_api/api_resources/machine_translation_engines/resource.py
index affc3ca9..f43b0a34 100644
--- a/crowdin_api/api_resources/machine_translation_engines/resource.py
+++ b/crowdin_api/api_resources/machine_translation_engines/resource.py
@@ -1,7 +1,8 @@
from typing import Optional, Iterable
from crowdin_api.api_resources.abstract.resources import BaseResource
-from .enums import LanguageRecognitionProvider
+from .enums import LanguageRecognitionProvider, MachineTranslationEngineType
+from .types import MachineTranslationEngineCredentials, MachineTranslationEnginePatchRequest
class MachineTranslationEnginesResource(BaseResource):
@@ -13,7 +14,7 @@ class MachineTranslationEnginesResource(BaseResource):
Use API to add, update, and delete specific MTE.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Machine-Translation-Engines
+ https://support.crowdin.com/developer/api/v2/#tag/Machine-Translation-Engines
"""
def get_mts_path(self, mtId: Optional[int] = None):
@@ -27,7 +28,7 @@ def list_mts(self, limit: Optional[int] = None, offset: Optional[int] = None):
List MTs.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.mts.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.mts.getMany
"""
return self._get_entire_data(
@@ -41,7 +42,7 @@ def get_mt(self, mtId: int):
Get MT.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.mts.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.mts.get
"""
return self.requester.request(method="get", path=self.get_mts_path(mtId=mtId))
@@ -58,7 +59,7 @@ def translate_via_mt(
Create Translate via MT.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.mts.translations.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.mts.translations.post
"""
return self.requester.request(
method="post",
@@ -70,3 +71,94 @@ def translate_via_mt(
"strings": strings,
},
)
+
+
+class EnterpriseMachineTranslationEnginesResource(MachineTranslationEnginesResource):
+ """
+ Resource for Machine Translation Engines (Crowdin Enterprise).
+
+ Machine Translation Engines (MTE) are the sources for pre-translations.
+
+ Use API to add, update, and delete specific MTE.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Machine-Translation-Engines
+ """
+
+ def list_mts(
+ self,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ groupId: Optional[int] = None,
+ ):
+ """
+ List MTs.
+
+ :param groupId: Group Identifier. Set 0 to see MTs of root group
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.mts.getMany
+ """
+ params = {"groupId": groupId}
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_mts_path(),
+ params=params,
+ )
+
+ def add_mt(
+ self,
+ name: str,
+ type: MachineTranslationEngineType,
+ credentials: MachineTranslationEngineCredentials,
+ groupId: Optional[int] = None,
+ enabledLanguageIds: Optional[Iterable[str]] = None,
+ enabledProjectIds: Optional[Iterable[int]] = None,
+ isEnabled: Optional[bool] = None,
+ ):
+ """
+ Add MT.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.mts.post
+ """
+ return self.requester.request(
+ method="post",
+ path=self.get_mts_path(),
+ request_data={
+ "name": name,
+ "type": type,
+ "credentials": credentials,
+ "groupId": groupId,
+ "enabledLanguageIds": enabledLanguageIds,
+ "enabledProjectIds": enabledProjectIds,
+ "isEnabled": isEnabled,
+ },
+ )
+
+ def edit_mt(self, mtId: int, data: Iterable[MachineTranslationEnginePatchRequest]):
+ """
+ Edit MT.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.mts.patch
+ """
+ return self.requester.request(
+ method="patch",
+ path=self.get_mts_path(mtId=mtId),
+ request_data=data,
+ )
+
+ def delete_mt(self, mtId: int):
+ """
+ Delete MT.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.mts.delete
+ """
+ return self.requester.request(
+ method="delete",
+ path=self.get_mts_path(mtId=mtId),
+ )
diff --git a/crowdin_api/api_resources/machine_translation_engines/tests/test_machine_translation_engines_resources.py b/crowdin_api/api_resources/machine_translation_engines/tests/test_machine_translation_engines_resources.py
index 54e48012..22023fb4 100644
--- a/crowdin_api/api_resources/machine_translation_engines/tests/test_machine_translation_engines_resources.py
+++ b/crowdin_api/api_resources/machine_translation_engines/tests/test_machine_translation_engines_resources.py
@@ -3,6 +3,14 @@
import pytest
from crowdin_api.api_resources import MachineTranslationEnginesResource
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.machine_translation_engines.enums import (
+ MachineTranslationEnginePatchPath,
+ MachineTranslationEngineType,
+)
+from crowdin_api.api_resources.machine_translation_engines.resource import (
+ EnterpriseMachineTranslationEnginesResource,
+)
from crowdin_api.requester import APIRequester
@@ -87,3 +95,112 @@ def test_translate_via_mt(
path=resource.get_mts_path(mtId=1) + "/translations",
request_data=request_data,
)
+
+
+class TestEnterpriseMachineTranslationEnginesResource:
+ resource_class = EnterpriseMachineTranslationEnginesResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"groupId": None, "offset": 0, "limit": 25}),
+ (
+ {"groupId": 0, "offset": 10, "limit": 5},
+ {"groupId": 0, "offset": 10, "limit": 5},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_mts(self, m_request, in_params, request_params, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_mts(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ params=request_params,
+ path="mts",
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {
+ "name": "DeepL",
+ "type": MachineTranslationEngineType.DEEPL,
+ "credentials": {"apiKey": "key"},
+ },
+ {
+ "name": "DeepL",
+ "type": MachineTranslationEngineType.DEEPL,
+ "credentials": {"apiKey": "key"},
+ "groupId": None,
+ "enabledLanguageIds": None,
+ "enabledProjectIds": None,
+ "isEnabled": None,
+ },
+ ),
+ (
+ {
+ "name": "Amazon",
+ "type": MachineTranslationEngineType.AMAZON,
+ "credentials": {"accessKey": "a", "secretKey": "s"},
+ "groupId": 2,
+ "enabledLanguageIds": ["uk"],
+ "enabledProjectIds": [22],
+ "isEnabled": True,
+ },
+ {
+ "name": "Amazon",
+ "type": MachineTranslationEngineType.AMAZON,
+ "credentials": {"accessKey": "a", "secretKey": "s"},
+ "groupId": 2,
+ "enabledLanguageIds": ["uk"],
+ "enabledProjectIds": [22],
+ "isEnabled": True,
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_mt(self, m_request, in_params, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_mt(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="mts",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_mt(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": MachineTranslationEnginePatchPath.IS_ENABLED,
+ "value": False,
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_mt(mtId=1, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="mts/1",
+ request_data=data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_mt(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_mt(mtId=1) == "response"
+ m_request.assert_called_once_with(method="delete", path="mts/1")
diff --git a/crowdin_api/api_resources/machine_translation_engines/types.py b/crowdin_api/api_resources/machine_translation_engines/types.py
new file mode 100644
index 00000000..4d0f05d2
--- /dev/null
+++ b/crowdin_api/api_resources/machine_translation_engines/types.py
@@ -0,0 +1,48 @@
+from typing import Any, Union
+
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.machine_translation_engines.enums import (
+ MachineTranslationEnginePatchPath,
+)
+from crowdin_api.typing import TypedDict
+
+
+class MachineTranslationEngineApiKeyCredentials(TypedDict):
+ """Google Translate, DeepL Pro and ModernMT credentials."""
+
+ apiKey: str
+
+
+class MachineTranslationEngineGoogleAutoMLCredentials(TypedDict):
+ """Google AutoML Translate credentials (JSON file, base64 encoded)."""
+
+ credentials: str
+
+
+class MachineTranslationEngineMicrosoftCredentials(TypedDict, total=False):
+ apiKey: str
+ model: str
+
+
+class MachineTranslationEngineAmazonCredentials(TypedDict):
+ accessKey: str
+ secretKey: str
+
+
+class MachineTranslationEngineCustomMTCredentials(TypedDict):
+ url: str
+
+
+MachineTranslationEngineCredentials = Union[
+ MachineTranslationEngineApiKeyCredentials,
+ MachineTranslationEngineGoogleAutoMLCredentials,
+ MachineTranslationEngineMicrosoftCredentials,
+ MachineTranslationEngineAmazonCredentials,
+ MachineTranslationEngineCustomMTCredentials,
+]
+
+
+class MachineTranslationEnginePatchRequest(TypedDict):
+ op: PatchOperation
+ path: MachineTranslationEnginePatchPath
+ value: Any
diff --git a/crowdin_api/api_resources/notifications/enums.py b/crowdin_api/api_resources/notifications/enums.py
index 166ca50a..24bb27b7 100644
--- a/crowdin_api/api_resources/notifications/enums.py
+++ b/crowdin_api/api_resources/notifications/enums.py
@@ -2,5 +2,18 @@
class MemberRole(Enum):
+ """
+ Roles for project notifications (`POST /projects/{projectId}/notify`).
+ """
+
OWNER = "owner"
MANAGER = "manager"
+
+
+class OrganizationMemberRole(Enum):
+ """
+ Roles for organization notifications (`POST /notify`, Crowdin Enterprise only).
+ """
+
+ OWNER = "owner"
+ ADMIN = "admin"
diff --git a/crowdin_api/api_resources/notifications/resource.py b/crowdin_api/api_resources/notifications/resource.py
index 8d1013a2..7a811b05 100644
--- a/crowdin_api/api_resources/notifications/resource.py
+++ b/crowdin_api/api_resources/notifications/resource.py
@@ -4,15 +4,22 @@
from crowdin_api.api_resources.notifications.types import (
ByRoleRequestScehme,
ByUserIdsRequestScheme,
+ ByOrganizationRoleRequestScheme,
)
class NotificationResource(BaseResource):
"""
- Resource for Notifications
+ Resource for Notifications.
- Link to documetation:
- https://developer.crowdin.com/api/v2/#tag/Notifications
+ Available on both Crowdin and Crowdin Enterprise.
+ `send_notification_to_organization_members` is Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Notifications
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Notifications
"""
def send_notification_to_authenticated_user(self, message: str):
@@ -20,7 +27,10 @@ def send_notification_to_authenticated_user(self, message: str):
Send Notification to Authenticated User
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.notify.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.notify.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.notify.post
"""
return self.requester.request(
method="post", path="notify", request_data={"message": message}
@@ -35,11 +45,10 @@ def send_notification_to_project_members(
Send Notification To Project Members
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.notify.post
-
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.notify.post
Link to documentation (Enterprise):
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.notify.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.notify.post
"""
projectId = projectId or self.get_project_id()
@@ -52,13 +61,16 @@ def send_notification_to_project_members(
def send_notification_to_organization_members(
self,
- request_data: Union[ByUserIdsRequestScheme, ByRoleRequestScehme],
+ request_data: Union[ByUserIdsRequestScheme, ByOrganizationRoleRequestScheme],
):
"""
Send Notification To Organization Members
+ Crowdin Enterprise only. Notify organization members by `userIds` or by `role`
+ (`owner` or `admin`).
+
Link to documentation (Enterprise):
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.notify.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.notify.post
"""
return self.requester.request(
method="post", path="notify", request_data=request_data
diff --git a/crowdin_api/api_resources/notifications/tests/test_notification_resources.py b/crowdin_api/api_resources/notifications/tests/test_notification_resources.py
index f619b4ba..03b3ba2d 100644
--- a/crowdin_api/api_resources/notifications/tests/test_notification_resources.py
+++ b/crowdin_api/api_resources/notifications/tests/test_notification_resources.py
@@ -3,7 +3,7 @@
from crowdin_api.requester import APIRequester
from crowdin_api.api_resources.notifications.resource import NotificationResource
-from crowdin_api.api_resources.notifications.enums import MemberRole
+from crowdin_api.api_resources.notifications.enums import MemberRole, OrganizationMemberRole
class TestNotificationResource:
@@ -114,21 +114,21 @@ def test_send_notification_to_project_members(
),
(
{
- "role": MemberRole.OWNER,
+ "role": OrganizationMemberRole.OWNER,
"message": "TEST MESSAGE",
},
{
- "role": MemberRole.OWNER,
+ "role": OrganizationMemberRole.OWNER,
"message": "TEST MESSAGE",
},
),
(
{
- "role": MemberRole.MANAGER,
+ "role": OrganizationMemberRole.ADMIN,
"message": "TEST MESSAGE",
},
{
- "role": MemberRole.MANAGER,
+ "role": OrganizationMemberRole.ADMIN,
"message": "TEST MESSAGE",
},
),
diff --git a/crowdin_api/api_resources/notifications/types.py b/crowdin_api/api_resources/notifications/types.py
index aab665be..885fbbe0 100644
--- a/crowdin_api/api_resources/notifications/types.py
+++ b/crowdin_api/api_resources/notifications/types.py
@@ -1,5 +1,7 @@
-from typing import TypedDict, Iterable
-from crowdin_api.api_resources.notifications.enums import MemberRole
+from typing import Iterable
+
+from crowdin_api.api_resources.notifications.enums import MemberRole, OrganizationMemberRole
+from crowdin_api.typing import TypedDict
class ByRoleRequestScehme(TypedDict):
@@ -10,3 +12,8 @@ class ByRoleRequestScehme(TypedDict):
class ByUserIdsRequestScheme(TypedDict):
userIds: Iterable[int]
message: str
+
+
+class ByOrganizationRoleRequestScheme(TypedDict):
+ role: OrganizationMemberRole
+ message: str
diff --git a/crowdin_api/api_resources/organization/__init__.py b/crowdin_api/api_resources/organization/__init__.py
new file mode 100644
index 00000000..9f77d639
--- /dev/null
+++ b/crowdin_api/api_resources/organization/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {'tests': False}
diff --git a/crowdin_api/api_resources/organization/resource.py b/crowdin_api/api_resources/organization/resource.py
new file mode 100644
index 00000000..467ba1a6
--- /dev/null
+++ b/crowdin_api/api_resources/organization/resource.py
@@ -0,0 +1,36 @@
+from crowdin_api.api_resources.abstract.resources import BaseResource
+
+
+class OrganizationResource(BaseResource):
+ """
+ Resource for Organization.
+
+ Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Organization
+ """
+
+ def get_organization_path(self):
+ return "organization"
+
+ def get_organization(self):
+ """
+ Get Organization.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.organization.get
+ """
+ return self.requester.request(method="get", path=self.get_organization_path())
+
+ def get_organization_auth_settings(self):
+ """
+ Get Organization Auth Settings.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.organization.auth-settings.get
+ """
+ return self.requester.request(
+ method="get",
+ path=f"{self.get_organization_path()}/auth-settings",
+ )
diff --git a/crowdin_api/api_resources/organization/tests/test_organization_resources.py b/crowdin_api/api_resources/organization/tests/test_organization_resources.py
new file mode 100644
index 00000000..a7c6b24b
--- /dev/null
+++ b/crowdin_api/api_resources/organization/tests/test_organization_resources.py
@@ -0,0 +1,31 @@
+from unittest import mock
+
+from crowdin_api.api_resources.organization.resource import OrganizationResource
+from crowdin_api.requester import APIRequester
+
+
+class TestOrganizationResource:
+ resource_class = OrganizationResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ def test_get_organization_path(self, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_organization_path() == "organization"
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_organization(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_organization() == "response"
+ m_request.assert_called_once_with(method="get", path="organization")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_organization_auth_settings(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_organization_auth_settings() == "response"
+ m_request.assert_called_once_with(method="get", path="organization/auth-settings")
diff --git a/crowdin_api/api_resources/project_placeholders/__init__.py b/crowdin_api/api_resources/project_placeholders/__init__.py
new file mode 100644
index 00000000..f5406199
--- /dev/null
+++ b/crowdin_api/api_resources/project_placeholders/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {"tests": False}
diff --git a/crowdin_api/api_resources/project_placeholders/enums.py b/crowdin_api/api_resources/project_placeholders/enums.py
new file mode 100644
index 00000000..2a18c673
--- /dev/null
+++ b/crowdin_api/api_resources/project_placeholders/enums.py
@@ -0,0 +1,13 @@
+from enum import Enum
+
+
+class ProjectPlaceholderType(Enum):
+ HIGH = "high"
+ LOW = "low"
+
+
+class ProjectPlaceholderPatchPath(Enum):
+ INDEX = "/index"
+ TYPE = "/type"
+ IS_BLOCKING = "/isBlocking"
+ FORMATS = "/formats"
diff --git a/crowdin_api/api_resources/project_placeholders/resource.py b/crowdin_api/api_resources/project_placeholders/resource.py
new file mode 100644
index 00000000..4933d58d
--- /dev/null
+++ b/crowdin_api/api_resources/project_placeholders/resource.py
@@ -0,0 +1,136 @@
+from typing import Iterable, Optional
+
+from crowdin_api.api_resources.abstract.resources import BaseResource
+from crowdin_api.api_resources.project_placeholders.enums import ProjectPlaceholderType
+from crowdin_api.api_resources.project_placeholders.types import ProjectPlaceholderPatchRequest
+
+
+class ProjectPlaceholdersResource(BaseResource):
+ """
+ Resource for Project Placeholders.
+
+ Custom placeholders of the organization assigned to a project. Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Placeholders
+ """
+
+ def get_project_placeholders_path(
+ self, projectId: int, projectPlaceholderId: Optional[int] = None
+ ):
+ if projectPlaceholderId is not None:
+ return f"projects/{projectId}/placeholders/{projectPlaceholderId}"
+
+ return f"projects/{projectId}/placeholders"
+
+ def list_project_placeholders(
+ self,
+ projectId: Optional[int] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List Project Placeholders.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.placeholders.getMany
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_project_placeholders_path(projectId=projectId),
+ params=self.get_page_params(limit=limit, offset=offset),
+ )
+
+ def add_project_placeholder(
+ self,
+ customPlaceholderId: int,
+ projectId: Optional[int] = None,
+ type: Optional[ProjectPlaceholderType] = None,
+ index: Optional[int] = None,
+ isBlocking: Optional[bool] = None,
+ formats: Optional[Iterable[str]] = None,
+ ):
+ """
+ Add Project Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.placeholders.post
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_project_placeholders_path(projectId=projectId),
+ request_data={
+ "customPlaceholderId": customPlaceholderId,
+ "type": type,
+ "index": index,
+ "isBlocking": isBlocking,
+ "formats": formats,
+ },
+ )
+
+ def get_project_placeholder(
+ self, projectPlaceholderId: int, projectId: Optional[int] = None
+ ):
+ """
+ Get Project Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.placeholders.get
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_project_placeholders_path(
+ projectId=projectId, projectPlaceholderId=projectPlaceholderId
+ ),
+ )
+
+ def delete_project_placeholder(
+ self, projectPlaceholderId: int, projectId: Optional[int] = None
+ ):
+ """
+ Delete Project Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.placeholders.delete
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="delete",
+ path=self.get_project_placeholders_path(
+ projectId=projectId, projectPlaceholderId=projectPlaceholderId
+ ),
+ )
+
+ def edit_project_placeholder(
+ self,
+ projectPlaceholderId: int,
+ data: Iterable[ProjectPlaceholderPatchRequest],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Edit Project Placeholder.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.placeholders.patch
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="patch",
+ path=self.get_project_placeholders_path(
+ projectId=projectId, projectPlaceholderId=projectPlaceholderId
+ ),
+ request_data=data,
+ )
diff --git a/crowdin_api/api_resources/project_placeholders/tests/test_project_placeholders_resources.py b/crowdin_api/api_resources/project_placeholders/tests/test_project_placeholders_resources.py
new file mode 100644
index 00000000..07710ac4
--- /dev/null
+++ b/crowdin_api/api_resources/project_placeholders/tests/test_project_placeholders_resources.py
@@ -0,0 +1,135 @@
+from unittest import mock
+
+import pytest
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.project_placeholders.enums import (
+ ProjectPlaceholderPatchPath,
+ ProjectPlaceholderType,
+)
+from crowdin_api.api_resources.project_placeholders.resource import ProjectPlaceholdersResource
+from crowdin_api.requester import APIRequester
+
+
+class TestProjectPlaceholdersResource:
+ resource_class = ProjectPlaceholdersResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"projectId": 1}, "projects/1/placeholders"),
+ ({"projectId": 1, "projectPlaceholderId": 2}, "projects/1/placeholders/2"),
+ ),
+ )
+ def test_get_project_placeholders_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_project_placeholders_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"limit": 25, "offset": 0}),
+ ({"limit": 10, "offset": 2}, {"limit": 10, "offset": 2}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_project_placeholders(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_project_placeholders(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/placeholders",
+ params=request_params,
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {"customPlaceholderId": 3},
+ {
+ "customPlaceholderId": 3,
+ "type": None,
+ "index": None,
+ "isBlocking": None,
+ "formats": None,
+ },
+ ),
+ (
+ {
+ "customPlaceholderId": 3,
+ "type": ProjectPlaceholderType.HIGH,
+ "index": 1,
+ "isBlocking": True,
+ "formats": ["docbook", "adoc"],
+ },
+ {
+ "customPlaceholderId": 3,
+ "type": ProjectPlaceholderType.HIGH,
+ "index": 1,
+ "isBlocking": True,
+ "formats": ["docbook", "adoc"],
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_project_placeholder(self, m_request, in_params, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_project_placeholder(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/placeholders",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_project_placeholder(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_project_placeholder(projectId=1, projectPlaceholderId=2) == "response"
+ m_request.assert_called_once_with(method="get", path="projects/1/placeholders/2")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_project_placeholder(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_project_placeholder(projectId=1, projectPlaceholderId=2) == "response"
+ m_request.assert_called_once_with(method="delete", path="projects/1/placeholders/2")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_project_placeholder(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": ProjectPlaceholderPatchPath.IS_BLOCKING,
+ "value": True,
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": ProjectPlaceholderPatchPath.FORMATS,
+ "value": ["adoc"],
+ },
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_project_placeholder(
+ projectId=1, projectPlaceholderId=2, data=data
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="projects/1/placeholders/2",
+ request_data=data,
+ )
diff --git a/crowdin_api/api_resources/project_placeholders/types.py b/crowdin_api/api_resources/project_placeholders/types.py
new file mode 100644
index 00000000..a1747d1a
--- /dev/null
+++ b/crowdin_api/api_resources/project_placeholders/types.py
@@ -0,0 +1,15 @@
+from typing import Any
+
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.project_placeholders.enums import ProjectPlaceholderPatchPath
+from crowdin_api.typing import TypedDict
+
+
+class ProjectPlaceholderPatchRequest(TypedDict):
+ """
+ `op` accepts `replace` or `test` only.
+ """
+
+ op: PatchOperation
+ path: ProjectPlaceholderPatchPath
+ value: Any
diff --git a/crowdin_api/api_resources/projects/enums.py b/crowdin_api/api_resources/projects/enums.py
index ebaf414a..c71651a4 100644
--- a/crowdin_api/api_resources/projects/enums.py
+++ b/crowdin_api/api_resources/projects/enums.py
@@ -22,6 +22,22 @@ class ProjectLanguageAccessPolicy(Enum):
class ProjectPatchPath(Enum):
+ """
+ JSON Patch paths for Edit Project.
+
+ Crowdin only: `/visibility`, `/languageAccessPolicy`, `/exportApprovedOnly`, `/useGlobalTm`,
+ `/tmPreTranslate`, `/mtPreTranslate`, `/aiPreTranslate`.
+
+ Crowdin Enterprise only: `/groupId`, `/taskReviewerIds`, `/exportWithMinApprovalsCount`,
+ `/exportStringsThatPassedWorkflow`, `/qaApprovalsCount`, `/fields`, `/fields/{fieldSlug}`, `/tm`,
+ `/alignmentActionAiPromptId`, `/templateId`, `/steps`, `/vendorId`, `/mtEngineId`.
+
+ File-based projects only: `/skipUntranslatedFiles`, `/saveMetaInfoInSource`, `/tmContextType`.
+
+ Deprecated: `/glossaryAccess` (use `/glossaryAccessOption` instead) and
+ `/preTranslationAiPromptId`.
+ """
+
NAME = "/name"
TARGET_LANGUAGE_IDS = "/targetLanguageIds"
CNAME = "/cname"
@@ -37,8 +53,8 @@ class ProjectPatchPath(Enum):
AUTO_TRANSLATE_DIALECTS = "/autoTranslateDialects"
PUBLIC_DOWNLOADS = "/publicDownloads"
USE_GLOBAL_TM = "/useGlobalTm"
- NORMALIZE_PLACEHOLDER = "normalizePlaceholder"
- SAVE_META_INFO_IN_SOURCE = "saveMetaInfoInSource"
+ NORMALIZE_PLACEHOLDER = "/normalizePlaceholder"
+ SAVE_META_INFO_IN_SOURCE = "/saveMetaInfoInSource"
IN_CONTEXT = "/inContext"
IN_CONTEXT_PSEUDO_LANGUAGE_ID = "/inContextPseudoLanguageId"
QA_CHECK_IS_ACTIVE = "/qaCheckIsActive"
@@ -51,6 +67,47 @@ class ProjectPatchPath(Enum):
LANGUAGE_MAPPING_KEY = "/languageMapping/{languageId}/{mappingKey}"
DEFAULT_TM_ID = "/defaultTmId"
DEFAULT_GLOSSARY_ID = "/defaultGlossaryId"
+ TASK_BASED_ACCESS_CONTROL = "/taskBasedAccessControl"
+ SHOW_TM_SUGGESTIONS_DIALECTS = "/showTmSuggestionsDialects"
+ TM_APPROVED_SUGGESTIONS_ONLY = "/tmApprovedSuggestionsOnly"
+ GLOSSARY_ACCESS = "/glossaryAccess" # deprecated, use GLOSSARY_ACCESS_OPTION instead
+ GLOSSARY_ACCESS_OPTION = "/glossaryAccessOption"
+ HIDDEN_STRINGS_PROOFREADERS_ACCESS = "/hiddenStringsProofreadersAccess"
+ IN_CONTEXT_PROCESS_HIDDEN_STRINGS = "/inContextProcessHiddenStrings"
+ NOTIFICATION_SETTINGS_TRANSLATOR_NEW_STRINGS = "/notificationSettings/translatorNewStrings"
+ NOTIFICATION_SETTINGS_MANAGER_NEW_STRINGS = "/notificationSettings/managerNewStrings"
+ NOTIFICATION_SETTINGS_MANAGER_LANGUAGE_COMPLETED = (
+ "/notificationSettings/managerLanguageCompleted"
+ )
+ PSEUDO_LANGUAGE_ID = "/pseudoLanguageId"
+ ASSIGNED_STYLE_GUIDES = "/assignedStyleGuides"
+ ASSIGNED_GLOSSARIES = "/assignedGlossaries"
+ ASSIGNED_TMS = "/assignedTms"
+ ASSIGNED_TM = "/assignedTms/{tmId}"
+ TM_PENALTIES_KEY = "/tmPenalties/{penaltyKey}"
+ TM_CONTEXT_TYPE = "/tmContextType"
+ TM_PRE_TRANSLATE = "/tmPreTranslate"
+ MT_PRE_TRANSLATE = "/mtPreTranslate"
+ AI_PRE_TRANSLATE = "/aiPreTranslate"
+ PRE_TRANSLATION_AI_PROMPT_ID = "/preTranslationAiPromptId" # deprecated
+ EDITOR_SUGGESTION_AI_PROMPT_ID = "/editorSuggestionAiPromptId"
+ QA_CHECK_ACTION_AI_PROMPT_ID = "/qaCheckActionAiPromptId"
+ CONTEXT_REVIEW_AI_PROMPT_ID = "/contextReviewAiPromptId"
+ SAVINGS_REPORT_SETTINGS_TEMPLATE_ID = "/savingsReportSettingsTemplateId"
+ # Crowdin Enterprise only
+ GROUP_ID = "/groupId"
+ TASK_REVIEWER_IDS = "/taskReviewerIds"
+ EXPORT_WITH_MIN_APPROVALS_COUNT = "/exportWithMinApprovalsCount"
+ EXPORT_STRINGS_THAT_PASSED_WORKFLOW = "/exportStringsThatPassedWorkflow"
+ QA_APPROVALS_COUNT = "/qaApprovalsCount"
+ FIELDS = "/fields"
+ FIELD = "/fields/{fieldSlug}"
+ TM = "/tm"
+ ALIGNMENT_ACTION_AI_PROMPT_ID = "/alignmentActionAiPromptId"
+ TEMPLATE_ID = "/templateId"
+ STEPS = "/steps"
+ VENDOR_ID = "/vendorId"
+ MT_ENGINE_ID = "/mtEngineId"
class ProjectTranslateDuplicates(Enum):
@@ -103,3 +160,43 @@ class ListProjectsOrderBy(Enum):
CREATED_AT = "createdAt"
UPDATED_AT = "updatedAt"
LAST_ACTIVITY = "lastActivity"
+
+
+class ProjectTagsDetection(Enum):
+ AUTO = 0
+ COUNT_TAGS = 1
+ SKIP_TAGS = 2
+
+
+class ProjectGlossaryAccessOption(Enum):
+ READ_ONLY = "readOnly"
+ FULL_ACCESS = "fullAccess"
+ MANAGE_DRAFTS = "manageDrafts"
+
+
+class ProjectTmContextType(Enum):
+ """
+ TM perfect match searching mode (file-based projects only).
+ """
+
+ SEGMENT_CONTEXT = "segmentContext"
+ AUTO = "auto"
+ PREV_AND_NEXT_SEGMENT = "prevAndNextSegment"
+
+
+class TmPreTranslateAutoApproveOption(Enum):
+ ALL = "all"
+ PERFECT_MATCH_ONLY = "perfectMatchOnly"
+ EXCEPT_AUTO_SUBSTITUTED = "exceptAutoSubstituted"
+ PERFECT_MATCH_APPROVED_ONLY = "perfectMatchApprovedOnly"
+ NONE = "none"
+
+
+class TmPreTranslateMinimumMatchRatio(Enum):
+ PERFECT = "perfect"
+ HUNDRED = "100"
+
+
+class StringsExporterSettingsPatchPath(Enum):
+ FORMAT = "/format"
+ SETTINGS = "/settings"
diff --git a/crowdin_api/api_resources/projects/resource.py b/crowdin_api/api_resources/projects/resource.py
index 9ee5b532..cea538f1 100644
--- a/crowdin_api/api_resources/projects/resource.py
+++ b/crowdin_api/api_resources/projects/resource.py
@@ -1,14 +1,31 @@
-from typing import Dict, Iterable, Optional, Union
+import warnings
+from typing import Any, Dict, Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
+from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.api_resources.projects.enums import (
HasManagerAccess,
+ ProjectGlossaryAccessOption,
ProjectLanguageAccessPolicy,
+ ProjectTagsDetection,
+ ProjectTmContextType,
ProjectTranslateDuplicates,
ProjectType,
ProjectVisibility,
+ StringsExporterSettingsPatchPath,
)
from crowdin_api.api_resources.projects.types import (
+ AiPreTranslate,
+ MtPreTranslate,
+ TmPreTranslate,
+ WorkflowStepSettings,
+ MdFileFormatSettings,
+ MdxFileFormatSettings,
+ JsonFileFormatSettings,
+ JavaScriptFileFormatSettings,
+ StringCatalogFileFormatSettings,
+ VdfFileFormatSettings,
+ StringsExporterSettingsPatchRequest,
NotificationSettings,
ProjectPatchRequest,
QACheckCategories,
@@ -28,6 +45,23 @@
from crowdin_api.sorting import Sorting
+def _warn_deprecated_project_params(
+ glossaryAccess: Optional[bool] = None,
+ preTranslationAiPromptId: Optional[int] = None,
+):
+ if glossaryAccess is not None:
+ warnings.warn(
+ "`glossaryAccess` is deprecated, use `glossaryAccessOption` instead",
+ DeprecationWarning,
+ )
+
+ if preTranslationAiPromptId is not None:
+ warnings.warn(
+ "`preTranslationAiPromptId` is deprecated, use `aiPreTranslate` instead",
+ DeprecationWarning,
+ )
+
+
class ProjectsResource(BaseResource):
"""
Resource for Storages.
@@ -36,7 +70,7 @@ class ProjectsResource(BaseResource):
Use API to manage projects, change their settings, or remove them if required.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Projects
+ https://support.crowdin.com/developer/api/v2/#tag/Projects
"""
@@ -55,13 +89,17 @@ def list_projects(
groupId: Optional[int] = None,
userId: Optional[Union[int, str]] = None,
hasManagerAccess: Optional[HasManagerAccess] = None,
- type: Optional[ProjectType] = None
+ type: Optional[ProjectType] = None,
+ filter: Optional[str] = None,
):
"""
List Projects.
+ Note: `groupId` is supported by Crowdin Enterprise only, `userId` and `type` by Crowdin only.
+ `filter` filters projects by `name`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.getMany
"""
params = {
@@ -69,7 +107,8 @@ def list_projects(
"userId": userId,
"hasManagerAccess": hasManagerAccess,
"groupId": groupId,
- "type": type.value if type is not None else None
+ "type": type.value if type is not None else None,
+ "filter": filter,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
@@ -80,7 +119,7 @@ def add_project(self, request_data: Dict):
Add Project.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.post
"""
return self.requester.request(
@@ -109,16 +148,70 @@ def add_file_based_project(
skipUntranslatedFiles: Optional[bool] = None,
exportApprovedOnly: Optional[bool] = None,
defaultTmId: Optional[int] = None,
- defaultGlossaryId: Optional[None] = None,
+ defaultGlossaryId: Optional[int] = None,
tmApprovedSuggestionsOnly: Optional[bool] = None,
+ tagsDetection: Optional[ProjectTagsDetection] = None,
+ taskBasedAccessControl: Optional[bool] = None,
+ publicDownloads: Optional[bool] = None,
+ hiddenStringsProofreadersAccess: Optional[bool] = None,
+ useGlobalTm: Optional[bool] = None,
+ showTmSuggestionsDialects: Optional[bool] = None,
+ qaCheckIsActive: Optional[bool] = None,
+ qaCheckCategories: Optional[QACheckCategories] = None,
+ qaChecksIgnorableCategories: Optional[QAChecksIgnorableCategories] = None,
+ languageMapping: Optional[Dict] = None,
+ glossaryAccess: Optional[bool] = None,
+ glossaryAccessOption: Optional[ProjectGlossaryAccessOption] = None,
+ tmPreTranslate: Optional[TmPreTranslate] = None,
+ mtPreTranslate: Optional[MtPreTranslate] = None,
+ aiPreTranslate: Optional[AiPreTranslate] = None,
+ preTranslationAiPromptId: Optional[int] = None,
+ editorSuggestionAiPromptId: Optional[int] = None,
+ qaCheckActionAiPromptId: Optional[int] = None,
+ contextReviewAiPromptId: Optional[int] = None,
+ savingsReportSettingsTemplateId: Optional[int] = None,
+ assignedStyleGuides: Optional[Iterable[int]] = None,
+ inContext: Optional[bool] = None,
+ inContextProcessHiddenStrings: Optional[bool] = None,
+ inContextPseudoLanguageId: Optional[str] = None,
+ tmContextType: Optional[ProjectTmContextType] = None,
+ groupId: Optional[int] = None,
+ templateId: Optional[int] = None,
+ steps: Optional[Iterable[WorkflowStepSettings]] = None,
+ vendorId: Optional[int] = None,
+ mtEngineId: Optional[int] = None,
+ taskReviewerIds: Optional[Iterable[int]] = None,
+ delayedWorkflowStart: Optional[bool] = None,
+ exportWithMinApprovalsCount: Optional[int] = None,
+ exportStringsThatPassedWorkflow: Optional[bool] = None,
+ qaApprovalsCount: Optional[int] = None,
+ customQaCheckIds: Optional[Iterable[int]] = None,
+ externalQaCheckIds: Optional[Iterable[int]] = None,
+ fields: Optional[Dict[str, Any]] = None,
+ alignmentActionAiPromptId: Optional[int] = None,
):
"""
Add Project(Files Based Project Form).
+ Crowdin only: `identifier`, `visibility`, `languageAccessPolicy`, `cname`,
+ `exportApprovedOnly`, `useGlobalTm`, `tmPreTranslate`, `mtPreTranslate`, `aiPreTranslate`.
+
+ Crowdin Enterprise only: `groupId`, `templateId`, `steps`, `vendorId`, `mtEngineId`,
+ `taskReviewerIds`, `delayedWorkflowStart`, `exportWithMinApprovalsCount`,
+ `exportStringsThatPassedWorkflow`, `qaApprovalsCount`, `customQaCheckIds`,
+ `externalQaCheckIds`, `fields`, `alignmentActionAiPromptId`.
+
+ Deprecated: `glossaryAccess` (use `glossaryAccessOption` instead) and
+ `preTranslationAiPromptId` (use `aiPreTranslate` instead).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.post
"""
+ _warn_deprecated_project_params(
+ glossaryAccess=glossaryAccess, preTranslationAiPromptId=preTranslationAiPromptId
+ )
+
return self.add_project(
request_data={
"name": name,
@@ -143,6 +236,45 @@ def add_file_based_project(
"defaultTmId": defaultTmId,
"defaultGlossaryId": defaultGlossaryId,
"tmApprovedSuggestionsOnly": tmApprovedSuggestionsOnly,
+ "tagsDetection": tagsDetection,
+ "taskBasedAccessControl": taskBasedAccessControl,
+ "publicDownloads": publicDownloads,
+ "hiddenStringsProofreadersAccess": hiddenStringsProofreadersAccess,
+ "useGlobalTm": useGlobalTm,
+ "showTmSuggestionsDialects": showTmSuggestionsDialects,
+ "qaCheckIsActive": qaCheckIsActive,
+ "qaCheckCategories": qaCheckCategories,
+ "qaChecksIgnorableCategories": qaChecksIgnorableCategories,
+ "languageMapping": languageMapping,
+ "glossaryAccess": glossaryAccess,
+ "glossaryAccessOption": glossaryAccessOption,
+ "tmPreTranslate": tmPreTranslate,
+ "mtPreTranslate": mtPreTranslate,
+ "aiPreTranslate": aiPreTranslate,
+ "preTranslationAiPromptId": preTranslationAiPromptId,
+ "editorSuggestionAiPromptId": editorSuggestionAiPromptId,
+ "qaCheckActionAiPromptId": qaCheckActionAiPromptId,
+ "contextReviewAiPromptId": contextReviewAiPromptId,
+ "savingsReportSettingsTemplateId": savingsReportSettingsTemplateId,
+ "assignedStyleGuides": assignedStyleGuides,
+ "inContext": inContext,
+ "inContextProcessHiddenStrings": inContextProcessHiddenStrings,
+ "inContextPseudoLanguageId": inContextPseudoLanguageId,
+ "tmContextType": tmContextType,
+ "groupId": groupId,
+ "templateId": templateId,
+ "steps": steps,
+ "vendorId": vendorId,
+ "mtEngineId": mtEngineId,
+ "taskReviewerIds": taskReviewerIds,
+ "delayedWorkflowStart": delayedWorkflowStart,
+ "exportWithMinApprovalsCount": exportWithMinApprovalsCount,
+ "exportStringsThatPassedWorkflow": exportStringsThatPassedWorkflow,
+ "qaApprovalsCount": qaApprovalsCount,
+ "customQaCheckIds": customQaCheckIds,
+ "externalQaCheckIds": externalQaCheckIds,
+ "fields": fields,
+ "alignmentActionAiPromptId": alignmentActionAiPromptId,
},
)
@@ -176,16 +308,60 @@ def add_strings_based_project(
glossaryAccess: Optional[bool] = None,
notificationSettings: Optional[NotificationSettings] = None,
defaultTmId: Optional[int] = None,
- defaultGlossaryId: Optional[None] = None,
+ defaultGlossaryId: Optional[int] = None,
tmApprovedSuggestionsOnly: Optional[bool] = None,
+ tagsDetection: Optional[ProjectTagsDetection] = None,
+ taskBasedAccessControl: Optional[bool] = None,
+ showTmSuggestionsDialects: Optional[bool] = None,
+ normalizePlaceholder: Optional[bool] = None,
+ glossaryAccessOption: Optional[ProjectGlossaryAccessOption] = None,
+ tmPreTranslate: Optional[TmPreTranslate] = None,
+ mtPreTranslate: Optional[MtPreTranslate] = None,
+ aiPreTranslate: Optional[AiPreTranslate] = None,
+ preTranslationAiPromptId: Optional[int] = None,
+ editorSuggestionAiPromptId: Optional[int] = None,
+ qaCheckActionAiPromptId: Optional[int] = None,
+ contextReviewAiPromptId: Optional[int] = None,
+ savingsReportSettingsTemplateId: Optional[int] = None,
+ assignedStyleGuides: Optional[Iterable[int]] = None,
+ inContext: Optional[bool] = None,
+ groupId: Optional[int] = None,
+ templateId: Optional[int] = None,
+ steps: Optional[Iterable[WorkflowStepSettings]] = None,
+ vendorId: Optional[int] = None,
+ mtEngineId: Optional[int] = None,
+ taskReviewerIds: Optional[Iterable[int]] = None,
+ delayedWorkflowStart: Optional[bool] = None,
+ exportWithMinApprovalsCount: Optional[int] = None,
+ exportStringsThatPassedWorkflow: Optional[bool] = None,
+ qaApprovalsCount: Optional[int] = None,
+ customQaCheckIds: Optional[Iterable[int]] = None,
+ externalQaCheckIds: Optional[Iterable[int]] = None,
+ fields: Optional[Dict[str, Any]] = None,
+ alignmentActionAiPromptId: Optional[int] = None,
):
"""
Add Project(Strings Based Project Form).
+ Crowdin only: `identifier`, `visibility`, `languageAccessPolicy`, `cname`,
+ `exportApprovedOnly`, `useGlobalTm`, `tmPreTranslate`, `mtPreTranslate`, `aiPreTranslate`.
+
+ Crowdin Enterprise only: `groupId`, `templateId`, `steps`, `vendorId`, `mtEngineId`,
+ `taskReviewerIds`, `delayedWorkflowStart`, `exportWithMinApprovalsCount`,
+ `exportStringsThatPassedWorkflow`, `qaApprovalsCount`, `customQaCheckIds`,
+ `externalQaCheckIds`, `fields`, `alignmentActionAiPromptId`.
+
+ Deprecated: `glossaryAccess` (use `glossaryAccessOption` instead) and
+ `preTranslationAiPromptId` (use `aiPreTranslate` instead).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.post
+ https://support.crowdin.com/developer/api/v2/string-based/#operation/api.projects.post
"""
+ _warn_deprecated_project_params(
+ glossaryAccess=glossaryAccess, preTranslationAiPromptId=preTranslationAiPromptId
+ )
+
return self.add_project(
request_data={
"name": name,
@@ -218,6 +394,35 @@ def add_strings_based_project(
"defaultTmId": defaultTmId,
"defaultGlossaryId": defaultGlossaryId,
"tmApprovedSuggestionsOnly": tmApprovedSuggestionsOnly,
+ "tagsDetection": tagsDetection,
+ "taskBasedAccessControl": taskBasedAccessControl,
+ "showTmSuggestionsDialects": showTmSuggestionsDialects,
+ "normalizePlaceholder": normalizePlaceholder,
+ "glossaryAccessOption": glossaryAccessOption,
+ "tmPreTranslate": tmPreTranslate,
+ "mtPreTranslate": mtPreTranslate,
+ "aiPreTranslate": aiPreTranslate,
+ "preTranslationAiPromptId": preTranslationAiPromptId,
+ "editorSuggestionAiPromptId": editorSuggestionAiPromptId,
+ "qaCheckActionAiPromptId": qaCheckActionAiPromptId,
+ "contextReviewAiPromptId": contextReviewAiPromptId,
+ "savingsReportSettingsTemplateId": savingsReportSettingsTemplateId,
+ "assignedStyleGuides": assignedStyleGuides,
+ "inContext": inContext,
+ "groupId": groupId,
+ "templateId": templateId,
+ "steps": steps,
+ "vendorId": vendorId,
+ "mtEngineId": mtEngineId,
+ "taskReviewerIds": taskReviewerIds,
+ "delayedWorkflowStart": delayedWorkflowStart,
+ "exportWithMinApprovalsCount": exportWithMinApprovalsCount,
+ "exportStringsThatPassedWorkflow": exportStringsThatPassedWorkflow,
+ "qaApprovalsCount": qaApprovalsCount,
+ "customQaCheckIds": customQaCheckIds,
+ "externalQaCheckIds": externalQaCheckIds,
+ "fields": fields,
+ "alignmentActionAiPromptId": alignmentActionAiPromptId,
},
)
@@ -226,7 +431,7 @@ def get_project(self, projectId: Optional[int] = None):
Get Project.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.get
"""
projectId = projectId or self.get_project_id()
@@ -240,7 +445,7 @@ def delete_project(self, projectId: Optional[int] = None):
Delete Project.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.delete
"""
projectId = projectId or self.get_project_id()
@@ -255,8 +460,11 @@ def edit_project(
"""
Edit Project.
+ See `ProjectPatchPath` for the available paths, including the Crowdin Enterprise only
+ and file-based only ones.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.patch
"""
projectId = projectId or self.get_project_id()
@@ -284,7 +492,7 @@ def download_project_file_custom_segmentation(
Download Project File Format Settings Custom Segmentation.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.custom-segmentations.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.custom-segmentations.get
"""
projectId = projectId or self.get_project_id()
@@ -306,7 +514,7 @@ def reset_project_file_custom_segmentation(
Reset Project File Format Settings Custom Segmentation.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.custom-segmentations.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.custom-segmentations.delete
"""
projectId = projectId or self.get_project_id()
@@ -331,7 +539,7 @@ def list_project_file_format_settings(
List Project File Format Settings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.getMany
"""
projectId = projectId or self.get_project_id()
@@ -349,7 +557,9 @@ def add_project_file_format_settings(
settings: Union[
PropertyFileFormatSettings, XmlFileFormatSettings, SpecificFileFormatSettings,
DocxFileFormatSettings, MediaWikiFileFormatSettings, TxtFileFormatSettings,
- OtherFileFormatSettings
+ OtherFileFormatSettings, MdFileFormatSettings, MdxFileFormatSettings,
+ JsonFileFormatSettings, JavaScriptFileFormatSettings, StringCatalogFileFormatSettings,
+ VdfFileFormatSettings, Dict[str, Any]
],
projectId: Optional[int] = None,
):
@@ -357,7 +567,7 @@ def add_project_file_format_settings(
Add Project File Format Settings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.post
"""
projectId = projectId or self.get_project_id()
@@ -375,7 +585,7 @@ def get_project_file_format_settings(
Get Project File Format Settings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.get
"""
projectId = projectId or self.get_project_id()
@@ -395,7 +605,7 @@ def delete_project_file_format_settings(
Delete Project File Format Settings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.delete
"""
projectId = projectId or self.get_project_id()
@@ -418,7 +628,7 @@ def edit_project_file_format_settings(
Edit Project File Format Settings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.file-format-settings.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.file-format-settings.patch
"""
projectId = projectId or self.get_project_id()
@@ -448,8 +658,8 @@ def list_project_strings_exporter_settings(
"""
List Project Strings Exporter Settings.
- Link to documetation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings-exporter-settings.getMany
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings-exporter-settings.getMany
"""
projectId = projectId or self.get_project_id()
@@ -472,8 +682,8 @@ def add_project_strings_exporter_settings(
"""
Add Project Strings Exporter Settings.
- Link to documetation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings-exporter-settings.post
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings-exporter-settings.post
"""
projectId = projectId or self.get_project_id()
@@ -490,8 +700,8 @@ def get_project_strings_exporter_settings(
"""
Get Project Strings Exporter Settings
- Link to documetation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings-exporter-settings.get
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings-exporter-settings.get
"""
projectId = projectId or self.get_project_id()
@@ -510,8 +720,8 @@ def delete_project_strings_exporter_settings(
"""
Delete Project Strings Exporter Settings.
- Link to documetation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings-exporter-settings.delete
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings-exporter-settings.delete
"""
projectId = projectId or self.get_project_id()
@@ -527,28 +737,51 @@ def delete_project_strings_exporter_settings(
def edit_project_strings_exporter_settings(
self,
systemStringExporterSettingsId: int,
- format: str,
- settings: Union[
+ format: Optional[str] = None,
+ settings: Optional[Union[
AndroidStringsExporterSettings,
MacOSXStringsExporterSettings,
XliffStringsExporterSettings,
- ],
+ ]] = None,
projectId: Optional[int] = None,
+ data: Optional[Iterable[StringsExporterSettingsPatchRequest]] = None,
):
"""
Edit Project Strings Exporter Settings.
- Link to documetation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings-exporter-settings.patch
+ The API expects a JSON Patch document. Pass it via `data`, or pass `format` and/or
+ `settings` and they will be sent as `replace` operations on `/format` and `/settings`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings-exporter-settings.patch
"""
projectId = projectId or self.get_project_id()
+ if data is None:
+ data = []
+ if format is not None:
+ data.append(
+ {
+ "op": PatchOperation.REPLACE.value,
+ "path": StringsExporterSettingsPatchPath.FORMAT.value,
+ "value": format,
+ }
+ )
+ if settings is not None:
+ data.append(
+ {
+ "op": PatchOperation.REPLACE.value,
+ "path": StringsExporterSettingsPatchPath.SETTINGS.value,
+ "value": settings,
+ }
+ )
+
return self.requester.request(
method="patch",
path=self.get_strings_exporter_path(
projectId=projectId,
systemStringExporterSettingsId=systemStringExporterSettingsId
),
- request_data={"format": format, "settings": settings},
+ request_data=data,
)
diff --git a/crowdin_api/api_resources/projects/tests/test_projects_resources.py b/crowdin_api/api_resources/projects/tests/test_projects_resources.py
index 21d1e496..1080840d 100644
--- a/crowdin_api/api_resources/projects/tests/test_projects_resources.py
+++ b/crowdin_api/api_resources/projects/tests/test_projects_resources.py
@@ -6,6 +6,12 @@
from crowdin_api.api_resources.projects.enums import (
HasManagerAccess,
ListProjectsOrderBy,
+ ProjectGlossaryAccessOption,
+ ProjectTagsDetection,
+ ProjectTmContextType,
+ StringsExporterSettingsPatchPath,
+ TmPreTranslateAutoApproveOption,
+ TmPreTranslateMinimumMatchRatio,
ProjectLanguageAccessPolicy,
ProjectPatchPath,
ProjectTranslateDuplicates,
@@ -21,6 +27,134 @@
from crowdin_api.requester import APIRequester
from crowdin_api.sorting import Sorting, SortingOrder, SortingRule
+FILE_BASED_NEW_FIELDS = {
+ "tagsDetection": None,
+ "taskBasedAccessControl": None,
+ "showTmSuggestionsDialects": None,
+ "glossaryAccessOption": None,
+ "tmPreTranslate": None,
+ "mtPreTranslate": None,
+ "aiPreTranslate": None,
+ "preTranslationAiPromptId": None,
+ "editorSuggestionAiPromptId": None,
+ "qaCheckActionAiPromptId": None,
+ "contextReviewAiPromptId": None,
+ "savingsReportSettingsTemplateId": None,
+ "assignedStyleGuides": None,
+ "inContext": None,
+ "groupId": None,
+ "templateId": None,
+ "steps": None,
+ "vendorId": None,
+ "mtEngineId": None,
+ "taskReviewerIds": None,
+ "delayedWorkflowStart": None,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
+ "qaApprovalsCount": None,
+ "customQaCheckIds": None,
+ "externalQaCheckIds": None,
+ "fields": None,
+ "alignmentActionAiPromptId": None,
+ "publicDownloads": None,
+ "hiddenStringsProofreadersAccess": None,
+ "useGlobalTm": None,
+ "qaCheckIsActive": None,
+ "qaCheckCategories": None,
+ "qaChecksIgnorableCategories": None,
+ "languageMapping": None,
+ "glossaryAccess": None,
+ "inContextProcessHiddenStrings": None,
+ "inContextPseudoLanguageId": None,
+ "tmContextType": None,
+}
+
+STRINGS_BASED_NEW_FIELDS = {
+ "tagsDetection": None,
+ "taskBasedAccessControl": None,
+ "showTmSuggestionsDialects": None,
+ "glossaryAccessOption": None,
+ "tmPreTranslate": None,
+ "mtPreTranslate": None,
+ "aiPreTranslate": None,
+ "preTranslationAiPromptId": None,
+ "editorSuggestionAiPromptId": None,
+ "qaCheckActionAiPromptId": None,
+ "contextReviewAiPromptId": None,
+ "savingsReportSettingsTemplateId": None,
+ "assignedStyleGuides": None,
+ "inContext": None,
+ "groupId": None,
+ "templateId": None,
+ "steps": None,
+ "vendorId": None,
+ "mtEngineId": None,
+ "taskReviewerIds": None,
+ "delayedWorkflowStart": None,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
+ "qaApprovalsCount": None,
+ "customQaCheckIds": None,
+ "externalQaCheckIds": None,
+ "fields": None,
+ "alignmentActionAiPromptId": None,
+ "normalizePlaceholder": None,
+}
+
+ADD_FILE_BASED_PROJECT_BASE_FIELDS = (
+ "identifier",
+ "type",
+ "normalizePlaceholder",
+ "saveMetaInfoInSource",
+ "notificationSettings",
+ "targetLanguageIds",
+ "visibility",
+ "languageAccessPolicy",
+ "cname",
+ "description",
+ "skipUntranslatedStrings",
+ "skipUntranslatedFiles",
+ "exportApprovedOnly",
+ "translateDuplicates",
+ "isMtAllowed",
+ "autoSubstitution",
+ "autoTranslateDialects",
+ "defaultTmId",
+ "defaultGlossaryId",
+ "tmApprovedSuggestionsOnly",
+)
+
+ADD_STRINGS_BASED_PROJECT_BASE_FIELDS = (
+ "identifier",
+ "type",
+ "targetLanguageIds",
+ "visibility",
+ "languageAccessPolicy",
+ "cname",
+ "description",
+ "skipUntranslatedStrings",
+ "skipUntranslatedFiles",
+ "exportApprovedOnly",
+ "translateDuplicates",
+ "isMtAllowed",
+ "autoSubstitution",
+ "autoTranslateDialects",
+ "publicDownloads",
+ "hiddenStringsProofreadersAccess",
+ "useGlobalTm",
+ "inContextProcessHiddenStrings",
+ "inContextPseudoLanguageId",
+ "qaCheckIsActive",
+ "qaCheckCategories",
+ "qaChecksIgnorableCategories",
+ "languageMapping",
+ "glossaryAccess",
+ "notificationSettings",
+ "defaultTmId",
+ "defaultGlossaryId",
+ "tmApprovedSuggestionsOnly",
+)
+
class TestProjectsResource:
resource_class = ProjectsResource
@@ -60,7 +194,8 @@ def test_get_projects_path(self, projectId, path, base_absolut_url):
"userId": 1,
"groupId": 1,
"hasManagerAccess": HasManagerAccess.TRUE,
- "type": ProjectType.STRING_BASED
+ "type": ProjectType.STRING_BASED,
+ "filter": "name",
},
{
"orderBy": Sorting(
@@ -71,7 +206,8 @@ def test_get_projects_path(self, projectId, path, base_absolut_url):
"userId": 1,
"groupId": 1,
"hasManagerAccess": HasManagerAccess.TRUE,
- "type": 1
+ "type": 1,
+ "filter": "name",
},
),
(
@@ -83,7 +219,8 @@ def test_get_projects_path(self, projectId, path, base_absolut_url):
"userId": None,
"groupId": None,
"hasManagerAccess": None,
- "type": None
+ "type": None,
+ "filter": None,
},
),
),
@@ -121,6 +258,7 @@ def test_add_project(self, m_request, base_absolut_url):
"sourceLanguageId": "ua",
},
{
+ **FILE_BASED_NEW_FIELDS,
"name": "name",
"sourceLanguageId": "ua",
"identifier": None,
@@ -175,6 +313,7 @@ def test_add_project(self, m_request, base_absolut_url):
"tmApprovedSuggestionsOnly": True,
},
{
+ **FILE_BASED_NEW_FIELDS,
"name": "name",
"sourceLanguageId": "ua",
"identifier": "identifier",
@@ -222,6 +361,7 @@ def test_add_file_based_project(self, m_add_project, in_params, request_data, ba
"sourceLanguageId": "ua",
},
{
+ **STRINGS_BASED_NEW_FIELDS,
"name": "name",
"sourceLanguageId": "ua",
"identifier": None,
@@ -279,36 +419,36 @@ def test_add_file_based_project(self, m_add_project, in_params, request_data, ba
"inContextPseudoLanguageId": "ua",
"qaCheckIsActive": True,
"qaCheckCategories": QACheckCategories(
- EMPTY=True,
- SIZE=True,
- TAGS=True,
- SPACES=True,
- VARIABLES=True,
- PUNCTUATION=True,
- SYMBOLREGISTER=True,
- SPECIALSYMBOLS=True,
- WRONGTRANSLATION=True,
- SPELLCHECK=True,
- ICU=True,
- TERMS=True,
- DUPLICATE=True,
+ empty=True,
+ size=True,
+ tags=True,
+ spaces=True,
+ variables=True,
+ punctuation=True,
+ symbolRegister=True,
+ specialSymbols=True,
+ wrongTranslation=True,
+ spellcheck=True,
+ icu=True,
+ terms=True,
+ duplicate=True,
),
"qaChecksIgnorableCategories": QAChecksIgnorableCategories(
- EMPTY=True,
- SIZE=True,
- TAGS=True,
- SPACES=True,
- VARIABLES=True,
- PUNCTUATION=True,
- SYMBOLREGISTER=True,
- SPECIALSYMBOLS=True,
- WRONGTRANSLATION=True,
- SPELLCHECK=True,
- ICU=True,
- TERMS=True,
- DUPLICATE=True,
- FTL=True,
- ANDROID=True
+ empty=True,
+ size=True,
+ tags=True,
+ spaces=True,
+ variables=True,
+ punctuation=True,
+ symbolRegister=True,
+ specialSymbols=True,
+ wrongTranslation=True,
+ spellcheck=True,
+ icu=True,
+ terms=True,
+ duplicate=True,
+ ftl=True,
+ android=True
),
"languageMapping": {},
"glossaryAccess": True,
@@ -322,6 +462,7 @@ def test_add_file_based_project(self, m_add_project, in_params, request_data, ba
"tmApprovedSuggestionsOnly": True,
},
{
+ **STRINGS_BASED_NEW_FIELDS,
"name": "name",
"sourceLanguageId": "ua",
"identifier": "identifier",
@@ -345,36 +486,36 @@ def test_add_file_based_project(self, m_add_project, in_params, request_data, ba
"inContextPseudoLanguageId": "ua",
"qaCheckIsActive": True,
"qaCheckCategories": QACheckCategories(
- EMPTY=True,
- SIZE=True,
- TAGS=True,
- SPACES=True,
- VARIABLES=True,
- PUNCTUATION=True,
- SYMBOLREGISTER=True,
- SPECIALSYMBOLS=True,
- WRONGTRANSLATION=True,
- SPELLCHECK=True,
- ICU=True,
- TERMS=True,
- DUPLICATE=True,
+ empty=True,
+ size=True,
+ tags=True,
+ spaces=True,
+ variables=True,
+ punctuation=True,
+ symbolRegister=True,
+ specialSymbols=True,
+ wrongTranslation=True,
+ spellcheck=True,
+ icu=True,
+ terms=True,
+ duplicate=True,
),
"qaChecksIgnorableCategories": QAChecksIgnorableCategories(
- EMPTY=True,
- SIZE=True,
- TAGS=True,
- SPACES=True,
- VARIABLES=True,
- PUNCTUATION=True,
- SYMBOLREGISTER=True,
- SPECIALSYMBOLS=True,
- WRONGTRANSLATION=True,
- SPELLCHECK=True,
- ICU=True,
- TERMS=True,
- DUPLICATE=True,
- FTL=True,
- ANDROID=True,
+ empty=True,
+ size=True,
+ tags=True,
+ spaces=True,
+ variables=True,
+ punctuation=True,
+ symbolRegister=True,
+ specialSymbols=True,
+ wrongTranslation=True,
+ spellcheck=True,
+ icu=True,
+ terms=True,
+ duplicate=True,
+ ftl=True,
+ android=True,
),
"languageMapping": {},
"glossaryAccess": True,
@@ -400,6 +541,136 @@ def test_add_strings_based_projectt(
assert resource.add_strings_based_project(**in_params) == "response"
m_add_project.assert_called_once_with(request_data=request_data)
+ @mock.patch("crowdin_api.api_resources.projects.resource.ProjectsResource.add_project")
+ def test_add_file_based_project_new_fields(self, m_add_project, base_absolut_url):
+ m_add_project.return_value = "response"
+
+ new_fields = {
+ "tagsDetection": ProjectTagsDetection.SKIP_TAGS,
+ "taskBasedAccessControl": True,
+ "publicDownloads": True,
+ "hiddenStringsProofreadersAccess": False,
+ "useGlobalTm": True,
+ "showTmSuggestionsDialects": True,
+ "qaCheckIsActive": True,
+ "qaCheckCategories": QACheckCategories(empty=True, unifiedPlaceholders=False),
+ "qaChecksIgnorableCategories": QAChecksIgnorableCategories(numbers=True),
+ "languageMapping": {"uk": {"locale": "uk-UA"}},
+ "glossaryAccessOption": ProjectGlossaryAccessOption.MANAGE_DRAFTS,
+ "tmPreTranslate": {
+ "enabled": True,
+ "autoApproveOption": TmPreTranslateAutoApproveOption.ALL,
+ "minimumMatchRatio": TmPreTranslateMinimumMatchRatio.PERFECT,
+ },
+ "mtPreTranslate": {"enabled": True, "mts": [{"mtId": 1, "languageIds": ["uk"]}]},
+ "aiPreTranslate": {"enabled": True, "aiPrompts": [{"aiPromptId": 2, "languageIds": ["uk"]}]},
+ "editorSuggestionAiPromptId": 3,
+ "qaCheckActionAiPromptId": 4,
+ "contextReviewAiPromptId": 5,
+ "savingsReportSettingsTemplateId": 6,
+ "assignedStyleGuides": [7, 8],
+ "inContext": True,
+ "inContextProcessHiddenStrings": True,
+ "inContextPseudoLanguageId": "ach",
+ "tmContextType": ProjectTmContextType.PREV_AND_NEXT_SEGMENT,
+ "groupId": 9,
+ "templateId": 10,
+ "steps": [{"id": 1, "languages": ["uk"], "config": {"assignees": {"uk": [1]}}}],
+ "vendorId": 11,
+ "mtEngineId": 12,
+ "taskReviewerIds": [13],
+ "delayedWorkflowStart": True,
+ "exportWithMinApprovalsCount": 1,
+ "exportStringsThatPassedWorkflow": False,
+ "qaApprovalsCount": 2,
+ "customQaCheckIds": [14],
+ "externalQaCheckIds": [15],
+ "fields": {"some-field": "value"},
+ "alignmentActionAiPromptId": 16,
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_file_based_project(
+ name="name", sourceLanguageId="en", **new_fields
+ ) == "response"
+
+ expected = {key: None for key in ADD_FILE_BASED_PROJECT_BASE_FIELDS}
+ expected.update(FILE_BASED_NEW_FIELDS)
+ expected.update({"name": "name", "sourceLanguageId": "en"})
+ expected.update(new_fields)
+ m_add_project.assert_called_once_with(request_data=expected)
+
+ @mock.patch("crowdin_api.api_resources.projects.resource.ProjectsResource.add_project")
+ def test_add_strings_based_project_new_fields(self, m_add_project, base_absolut_url):
+ m_add_project.return_value = "response"
+
+ new_fields = {
+ "tagsDetection": ProjectTagsDetection.AUTO,
+ "taskBasedAccessControl": True,
+ "showTmSuggestionsDialects": False,
+ "normalizePlaceholder": True,
+ "glossaryAccessOption": ProjectGlossaryAccessOption.READ_ONLY,
+ "tmPreTranslate": {"enabled": False},
+ "mtPreTranslate": {"enabled": False},
+ "aiPreTranslate": {"enabled": False},
+ "editorSuggestionAiPromptId": 1,
+ "qaCheckActionAiPromptId": 2,
+ "contextReviewAiPromptId": 3,
+ "savingsReportSettingsTemplateId": 4,
+ "assignedStyleGuides": [5],
+ "inContext": False,
+ "groupId": 6,
+ "templateId": 7,
+ "steps": [{"id": 1, "mtId": 2}],
+ "vendorId": 8,
+ "mtEngineId": 9,
+ "taskReviewerIds": [10],
+ "delayedWorkflowStart": False,
+ "exportWithMinApprovalsCount": 0,
+ "exportStringsThatPassedWorkflow": True,
+ "qaApprovalsCount": 1,
+ "customQaCheckIds": [11],
+ "externalQaCheckIds": [12],
+ "fields": {"some-field": 1},
+ "alignmentActionAiPromptId": 13,
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_strings_based_project(
+ name="name", sourceLanguageId="en", **new_fields
+ ) == "response"
+
+ expected = {key: None for key in ADD_STRINGS_BASED_PROJECT_BASE_FIELDS}
+ expected.update(STRINGS_BASED_NEW_FIELDS)
+ expected.update({"name": "name", "sourceLanguageId": "en"})
+ expected.update(new_fields)
+ m_add_project.assert_called_once_with(request_data=expected)
+
+ @pytest.mark.parametrize(
+ "method_name, deprecated_params",
+ (
+ ("add_file_based_project", {"glossaryAccess": True}),
+ ("add_file_based_project", {"preTranslationAiPromptId": 1}),
+ ("add_strings_based_project", {"glossaryAccess": False}),
+ ("add_strings_based_project", {"preTranslationAiPromptId": 1}),
+ ),
+ )
+ @mock.patch("crowdin_api.api_resources.projects.resource.ProjectsResource.add_project")
+ def test_add_project_deprecated_params(
+ self, m_add_project, method_name, deprecated_params, base_absolut_url
+ ):
+ m_add_project.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ with pytest.warns(DeprecationWarning):
+ assert getattr(resource, method_name)(
+ name="name", sourceLanguageId="en", **deprecated_params
+ ) == "response"
+
+ request_data = m_add_project.call_args.kwargs["request_data"]
+ for key, value in deprecated_params.items():
+ assert request_data[key] == value
+
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_get_project(self, m_request, base_absolut_url):
m_request.return_value = "response"
@@ -699,53 +970,59 @@ def test_delete_project_strings_exporter_settings(self, m_request, base_absolut_
"format": "android",
"settings": {
"convertPlaceholders": True,
+ "useCdataForStringsWithTags": True,
},
},
- {
- "format": "android",
- "settings": {
- "convertPlaceholders": True,
+ [
+ {"op": "replace", "path": "/format", "value": "android"},
+ {
+ "op": "replace",
+ "path": "/settings",
+ "value": {
+ "convertPlaceholders": True,
+ "useCdataForStringsWithTags": True,
+ },
},
- },
+ ],
),
(
- {
- "format": "macosx",
- "settings": {
- "convertPlaceholders": True,
- },
- },
- {
- "format": "macosx",
- "settings": {
- "convertPlaceholders": True,
- },
- },
+ {"format": "macosx"},
+ [{"op": "replace", "path": "/format", "value": "macosx"}],
+ ),
+ (
+ {"settings": {"copySourceToEmptyTarget": True}},
+ [
+ {
+ "op": "replace",
+ "path": "/settings",
+ "value": {"copySourceToEmptyTarget": True},
+ }
+ ],
),
(
{
- "format": "xliff",
- "settings": {
- "languagePaitMapping": {
- "uk": "es",
- "de": "en",
- },
- },
- },
- {
- "format": "xliff",
- "settings": {
- "languagePaitMapping": {
- "uk": "es",
- "de": "en",
- },
- },
+ "data": [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": StringsExporterSettingsPatchPath.SETTINGS,
+ "value": {"exportContext": True},
+ }
+ ]
},
+ [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": StringsExporterSettingsPatchPath.SETTINGS,
+ "value": {"exportContext": True},
+ }
+ ],
),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
- def test_edit_project_strings_exporter_settings(self, m_request, in_params, request_data, base_absolut_url):
+ def test_edit_project_strings_exporter_settings(
+ self, m_request, in_params, request_data, base_absolut_url
+ ):
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
diff --git a/crowdin_api/api_resources/projects/types.py b/crowdin_api/api_resources/projects/types.py
index 99319391..8041598a 100644
--- a/crowdin_api/api_resources/projects/types.py
+++ b/crowdin_api/api_resources/projects/types.py
@@ -6,6 +6,9 @@
EscapeQuotes,
EscapeSpecialCharacters,
ProjectFilePatchPath,
+ StringsExporterSettingsPatchPath,
+ TmPreTranslateAutoApproveOption,
+ TmPreTranslateMinimumMatchRatio,
)
from crowdin_api.typing import TypedDict
@@ -22,75 +25,226 @@ class NotificationSettings(TypedDict):
managerLanguageCompleted: bool
-class QACheckCategories(TypedDict):
- EMPTY: bool
- SIZE: bool
- TAGS: bool
- SPACES: bool
- VARIABLES: bool
- PUNCTUATION: bool
- SYMBOLREGISTER: bool
- SPECIALSYMBOLS: bool
- WRONGTRANSLATION: bool
- SPELLCHECK: bool
- ICU: bool
- TERMS: bool
- DUPLICATE: bool
-
-
-class QAChecksIgnorableCategories(TypedDict):
- EMPTY: bool
- SIZE: bool
- TAGS: bool
- SPACES: bool
- VARIABLES: bool
- PUNCTUATION: bool
- SYMBOLREGISTER: bool
- SPECIALSYMBOLS: bool
- WRONGTRANSLATION: bool
- SPELLCHECK: bool
- ICU: bool
- TERMS: bool
- DUPLICATE: bool
- FTL: bool
- ANDROID: bool
-
-
-class PropertyFileFormatSettings(TypedDict):
+class QACheckCategories(TypedDict, total=False):
+ empty: bool
+ size: bool
+ tags: bool
+ spaces: bool
+ variables: bool
+ punctuation: bool
+ symbolRegister: bool
+ specialSymbols: bool
+ wrongTranslation: bool
+ spellcheck: bool
+ icu: bool
+ terms: bool
+ duplicate: bool
+ ftl: bool
+ android: bool
+ numbers: bool
+ ai: bool
+ outdated: bool
+ mdx: bool
+ unifiedPlaceholders: bool
+
+
+class QAChecksIgnorableCategories(QACheckCategories, total=False):
+ pass
+
+
+class TmPreTranslate(TypedDict, total=False):
+ """
+ Crowdin only.
+ """
+
+ enabled: bool
+ autoApproveOption: TmPreTranslateAutoApproveOption
+ minimumMatchRatio: TmPreTranslateMinimumMatchRatio
+
+
+class MtPreTranslateEngineSettings(TypedDict, total=False):
+ mtId: int
+ languageIds: Iterable[str]
+
+
+class MtPreTranslate(TypedDict, total=False):
+ """
+ Crowdin only.
+ """
+
+ enabled: bool
+ mts: Iterable[MtPreTranslateEngineSettings]
+
+
+class AiPreTranslatePromptSettings(TypedDict, total=False):
+ aiPromptId: int
+ languageIds: Iterable[str]
+
+
+class AiPreTranslate(TypedDict, total=False):
+ """
+ Crowdin only.
+ """
+
+ enabled: bool
+ aiPrompts: Iterable[AiPreTranslatePromptSettings]
+
+
+class WorkflowStepConfig(TypedDict, total=False):
+ """
+ Keys are Language Identifiers, values are User (`assignees`) or Team (`assignedTeams`) Identifiers.
+ """
+
+ assignees: Dict[str, Iterable[int]]
+ assignedTeams: Dict[str, Iterable[int]]
+
+
+class _WorkflowStepSettingsRequired(TypedDict):
+ id: int
+
+
+class WorkflowStepSettings(_WorkflowStepSettingsRequired, total=False):
+ """
+ Crowdin Enterprise only. Workflow Template Step configuration.
+
+ Use `config` for Translation and Proofreading steps, `vendorId` for vendor steps,
+ `mtId` for machine translation steps and `promptId` for AI steps.
+ """
+
+ languages: Iterable[str]
+ config: WorkflowStepConfig
+ vendorId: int
+ mtId: int
+ promptId: int
+
+
+class PropertyFileFormatSettings(TypedDict, total=False):
escapeQuotes: EscapeQuotes
escapeSpecialCharacters: EscapeSpecialCharacters
exportPattern: str
-class XmlFileFormatSettings(TypedDict):
+class XmlFileFormatSettings(TypedDict, total=False):
translateContent: bool
translateAttributes: bool
translatableElements: Iterable[str]
contentSegmentation: bool
srxStorageId: int
+ inlineTags: Iterable[str]
exportPattern: str
-class SpecificFileFormatSettings(TypedDict):
+class SpecificFileFormatSettings(TypedDict, total=False):
"""
Includes kind standard file format settings:
- - WebXml file
- - Html file
- - Adoc file
- - Md file
+ - WebXml file (`inlineTags`)
+ - Html file (`inlineTags`, `excludedElements`)
+ - Adoc file (`excludeIncludeDirectives`)
+ - Android file
- FmMd file
- - FmHtml file
- - MadcapFisnp file
- - Idml file
+ - FmHtml file (`inlineTags`, `excludedElements`, `excludedFrontMatterElements`)
+ - MadcapFlsnp file
+ - Idml file (`inlineHyperlinkText`)
- Mif file
- Dita file
+ - Arb file
+ - Fjs file
+ - MacOSX file
+ - Chrome file
+ - CSV file
+ - XLSX file
+ - Xliff file
+ - Xliff 2.0 file
+ - React intl file
+ """
+ contentSegmentation: bool
+ srxStorageId: int
+ exportPattern: str
+ inlineTags: Iterable[str]
+ excludedElements: Iterable[str]
+ excludedFrontMatterElements: Iterable[str]
+ excludeIncludeDirectives: bool
+ inlineHyperlinkText: bool
+
+
+class MdFileFormatSettings(TypedDict, total=False):
+ """
+ Markdown file format settings.
+
+ Values available:
+ - strongMarker, emphasisMarker: "asterisk", "underscore"
+ - unorderedListBullet: "asterisks", "plus", "dash"
+ - tableColumnWidth: "consolidate", "evenly_distribute_cells"
+ - frontMatterQuotes: "auto", "single", "double"
+ """
+ contentSegmentation: bool
+ srxStorageId: int
+ exportPattern: str
+ inlineTags: Iterable[str]
+ strongMarker: str
+ emphasisMarker: str
+ unorderedListBullet: str
+ tableColumnWidth: str
+ frontMatterQuotes: str
+
+
+class MdxFileFormatSettings(TypedDict, total=False):
+ """
+ Mdx v1 / Mdx v2 file format settings.
+
+ Values available:
+ - type (Mdx v1 only): "mdx_v1", "mdx_v2"
+ - strongMarker, emphasisMarker: "asterisk", "underscore"
+ - unorderedListBullet: "asterisks", "plus", "dash"
+ - tableColumnWidth: "consolidate", "evenly_distribute_cells"
+ """
+ contentSegmentation: bool
+ srxStorageId: int
+ exportPattern: str
+ type: str
+ excludedFrontMatterElements: Iterable[str]
+ excludeCodeBlocks: bool
+ strongMarker: str
+ emphasisMarker: str
+ unorderedListBullet: str
+ tableColumnWidth: str
+
+
+class JsonFileFormatSettings(TypedDict, total=False):
+ """
+ Json file format settings.
+
+ Values available for `type`: "i18next_json", "nestjs_i18n".
"""
contentSegmentation: bool
srxStorageId: int
exportPattern: str
+ type: str
+
+
+class JavaScriptFileFormatSettings(TypedDict, total=False):
+ """
+ JavaScript file format settings.
+
+ Values available for `exportQuotes`: "single", "double".
+ """
+ exportPattern: str
+ exportQuotes: str
-class DocxFileFormatSettings(TypedDict):
+class StringCatalogFileFormatSettings(TypedDict, total=False):
+ importKeyAsSource: bool
+ importTranslations: bool
+ exportPattern: str
+
+
+class VdfFileFormatSettings(TypedDict, total=False):
+ convertIcu: bool
+ addGenderArgument: bool
+ exportPattern: str
+
+
+class DocxFileFormatSettings(TypedDict, total=False):
cleanTagsAggressively: bool
translateHiddenText: bool
translateHyperlinkUrls: bool
@@ -100,35 +254,75 @@ class DocxFileFormatSettings(TypedDict):
contentSegmentation: bool
srxStorageId: int
exportPattern: str
+ translateDocProperties: bool
+ translateComments: bool
+ ignoreWhitespaceStyles: bool
+ addTabAsCharacter: bool
+ addLineSeparatorAsCharacter: bool
+ lineSeparatorReplacement: str
+ replaceNoBreakHyphenTag: bool
+ ignoreSoftHyphenTag: bool
+ complexFieldDefinitionsToExtract: Iterable[str]
+ translateWordHeadersFooters: bool
+ translateWordGraphicName: bool
+ translateWordGraphicDescription: bool
+ ignoreWordFontColors: bool
+ wordFontColorsMinIgnoranceThreshold: str
+ wordFontColorsMaxIgnoranceThreshold: str
+ excludeWordStyles: Iterable[str]
+ translateWordInExcludeStyleMode: bool
+ wordHighlightColors: Iterable[str]
+ translateWordInExcludeHighlightMode: bool
+ translateWordExcludeColors: bool
+ wordExcludedColors: Iterable[str]
+ translateExcelCellsCopied: bool
+ translateExcelSheetNames: bool
+ excelExcludedColors: Iterable[str]
+ translateExcelDiagramData: bool
+ translateExcelDrawings: bool
+ allowWordStyleOptimization: bool
+ translateExcelExcludeColors: bool
-class MediaWikiFileFormatSettings(TypedDict):
+class MediaWikiFileFormatSettings(TypedDict, total=False):
srxStorageId: int
exportPattern: str
-class TxtFileFormatSettings(TypedDict):
+class TxtFileFormatSettings(TypedDict, total=False):
srxStorageId: int
exportPattern: str
-class OtherFileFormatSettings(TypedDict):
+class OtherFileFormatSettings(TypedDict, total=False):
exportPattern: str
-class AndroidStringsExporterSettings(TypedDict):
+class AndroidStringsExporterSettings(TypedDict, total=False):
convertPlaceholders: bool
+ convertLineBreaks: bool
+ useCdataForStringsWithTags: bool
-class MacOSXStringsExporterSettings(TypedDict):
+class MacOSXStringsExporterSettings(TypedDict, total=False):
convertPlaceholders: bool
+ convertLineBreaks: bool
+ exportContext: bool
-class XliffStringsExporterSettings(TypedDict):
+class XliffStringsExporterSettings(TypedDict, total=False):
languagePairMapping: Dict[str, str]
+ copySourceToEmptyTarget: bool
+ exportTranslatorsComment: bool
+
+
+class StringsExporterSettingsPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: StringsExporterSettingsPatchPath
class ProjectFilePatchRequest(TypedDict):
- value: Union[str, List[str]]
+ value: Union[str, List[str], Dict[str, Any]]
op: PatchOperation
path: ProjectFilePatchPath
diff --git a/crowdin_api/api_resources/reports/enums.py b/crowdin_api/api_resources/reports/enums.py
index 8888004e..77c04105 100644
--- a/crowdin_api/api_resources/reports/enums.py
+++ b/crowdin_api/api_resources/reports/enums.py
@@ -10,17 +10,47 @@ class ExportFormat(Enum):
class ScopeType(Enum):
ORGANIZATION = "organization"
GRPOUP = "group"
+ GROUP = "group"
PROJECT = "project"
class ReportName(Enum):
PRE_TRANSLATE_ACCURACY = "pre-translate-accuracy"
+ # Deprecated: no longer supported by the API
COSTS_ESTIMATION = "costs-estimation"
+ # Deprecated: no longer supported by the API
TRANSLATION_COSTS = "translation-costs"
TOP_MEMBERS = "top-members"
CONTRIBUTION_RAW_DATA = "contribution-raw-data"
COSTS_ESTIMATION_POST_EDITING = "costs-estimation-pe"
TRANSLATION_COSTS_POST_EDITING = "translation-costs-pe"
+ TRANSLATOR_ACCURACY = "translator-accuracy"
+ SOURCE_CONTENT_UPDATES = "source-content-updates"
+ PROJECT_MEMBERS = "project-members"
+ EDITOR_ISSUES = "editor-issues"
+ QA_CHECK_ISSUES = "qa-check-issues"
+ SAVING_ACTIVITY = "saving-activity"
+ TRANSLATION_ACTIVITY = "translation-activity"
+ TIME_SPENT = "time-spent"
+ # Enterprise only
+ TASK_USAGE = "task-usage"
+
+
+class GroupReportName(Enum):
+ """
+ Report names for Enterprise group and organization reports.
+ """
+
+ GROUP_TRANSLATION_COSTS_POST_EDITING = "group-translation-costs-pe"
+ GROUP_TOP_MEMBERS = "group-top-members"
+ GROUP_TASK_USAGE = "group-task-usage"
+ GROUP_QA_CHECK_ISSUES = "group-qa-check-issues"
+ GROUP_TRANSLATION_ACTIVITY = "group-translation-activity"
+ GROUP_SOURCE_CONTENT_UPDATES = "group-source-content-updates"
+ GROUP_TIME_SPENT = "group-time-spent"
+ GROUP_PRE_TRANSLATE_ACCURACY = "group-pre-translate-accuracy"
+ GROUP_TRANSLATOR_ACCURACY = "group-translator-accuracy"
+ GROUP_SAVING_ACTIVITY = "group-saving-activity"
class Unit(Enum):
@@ -28,6 +58,8 @@ class Unit(Enum):
WORDS = "words"
CHARS = "chars"
CHARS_WITH_SPACES = "chars_with_spaces"
+ # Only for hourly report settings templates
+ HOURS = "hours"
class Currency(Enum):
@@ -53,6 +85,8 @@ class Currency(Enum):
ZAR = "ZAR"
GEL = "GEL"
UAH = "UAH"
+ DDK = "DDK"
+ PLN = "PLN"
class SchemaMode(Enum):
@@ -85,6 +119,12 @@ class FuzzyRateMode(Enum):
class GroupBy(Enum):
USER = "user"
LANGUAGE = "language"
+ # Only for time spent reports
+ TASK = "task"
+ # Only for task usage reports
+ TYPE = "type"
+ # Only for group and organization reports
+ PROJECT = "project"
class ContributionMode:
@@ -94,11 +134,14 @@ class ContributionMode:
class ReportSettingsTemplatesPatchPath(Enum):
- NAME = "name"
- CURRENCY = "currency"
- UNIT = "unit"
- MODE = "mode"
- CONFIG = "config"
+ NAME = "/name"
+ CURRENCY = "/currency"
+ UNIT = "/unit"
+ # Crowdin project report settings templates only
+ MODE = "/mode"
+ CONFIG = "/config"
+ # Enterprise organization report settings templates only
+ IS_PUBLIC = "/isPublic"
class ReportLabelIncludeType(Enum):
@@ -111,3 +154,44 @@ class MatchType(Enum):
OPTION_100 = "100"
OPTION_99_82 = "99-82"
OPTION_81_60 = "81-60"
+
+
+class SavingActivityMode(Enum):
+ CURRENCY = "currency"
+ RELATIVE = "relative"
+
+
+class EditorIssueType(Enum):
+ GENERAL_QUESTION = "general_question"
+ TRANSLATION_MISTAKE = "translation_mistake"
+ CONTEXT_REQUEST = "context_request"
+ SOURCE_MISTAKE = "source_mistake"
+
+
+class TaskType(Enum):
+ """
+ Task type for `typeTasks` report schema field.
+
+ 0 - translate, 1 - proofread, 2 - translate by vendor, 3 - proofread by vendor.
+ """
+
+ TRANSLATE = 0
+ PROOFREAD = 1
+ TRANSLATE_BY_VENDOR = 2
+ PROOFREAD_BY_VENDOR = 3
+
+
+class TaskUsageReportType(Enum):
+ WORKLOAD = "workload"
+ CREATED_VS_RESOLVED = "created-vs-resolved"
+ PERFORMANCE = "performance"
+ TIME = "time"
+ COST = "cost"
+
+
+class TaskUsageStatus(Enum):
+ TODO = "todo"
+ IN_PROGRESS = "in_progress"
+ DONE = "done"
+ CLOSED = "closed"
+ REVIEW = "review"
diff --git a/crowdin_api/api_resources/reports/resource.py b/crowdin_api/api_resources/reports/resource.py
index 3b6757a1..39451a4f 100644
--- a/crowdin_api/api_resources/reports/resource.py
+++ b/crowdin_api/api_resources/reports/resource.py
@@ -1,6 +1,6 @@
import abc
from datetime import datetime
-from typing import Dict, Iterable, Optional
+from typing import Dict, Iterable, Optional, Union
from deprecated import deprecated
from crowdin_api.api_resources.abstract.resources import BaseResource
@@ -13,6 +13,11 @@
GroupBy,
Unit,
ReportLabelIncludeType,
+ SavingActivityMode,
+ EditorIssueType,
+ TaskType,
+ TaskUsageReportType,
+ TaskUsageStatus,
)
from crowdin_api.api_resources.reports.requests.cost_estimation_post_editing import (
IndividualRate as CostEstimationPeIndividualRate,
@@ -30,7 +35,11 @@
StepTypes,
ReportSettingsTemplatesPatchRequest,
Config,
- BaseRates
+ BaseRates,
+ HourlyBaseRates,
+ HourlyIndividualRate,
+ PostEditingConfig,
+ HourlyConfig,
)
@@ -46,10 +55,10 @@ def generate_report(self, request_data: Dict, projectId: Optional[int] = None):
Generate Report.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.download.download
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -96,15 +105,16 @@ def generate_top_members_report(
format: Optional[Format] = Format.XLSX,
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ userIds: Optional[Iterable[int]] = None,
):
"""
Generate Report(Top Members).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -119,6 +129,7 @@ def generate_top_members_report(
"format": format,
"dateFrom": dateFrom,
"dateTo": dateTo,
+ "userIds": userIds,
},
},
)
@@ -132,15 +143,24 @@ def generate_contribution_raw_data_report(
userId: Optional[int] = None,
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ columns: Optional[Iterable[str]] = None,
+ tmIds: Optional[Iterable[int]] = None,
+ mtIds: Optional[Iterable[int]] = None,
+ aiPromptIds: Optional[Iterable[int]] = None,
+ fileIds: Optional[Iterable[int]] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ branchIds: Optional[Iterable[int]] = None,
):
"""
Generate Report(Contribution Raw Data).
+ `fileIds` and `directoryIds` are available for file-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -156,6 +176,56 @@ def generate_contribution_raw_data_report(
"userId": userId,
"dateFrom": dateFrom,
"dateTo": dateTo,
+ "columns": columns,
+ "tmIds": tmIds,
+ "mtIds": mtIds,
+ "aiPromptIds": aiPromptIds,
+ "fileIds": fileIds,
+ "directoryIds": directoryIds,
+ "branchIds": branchIds,
+ },
+ },
+ )
+
+ def generate_contribution_raw_data_by_task_report(
+ self,
+ mode: ContributionMode,
+ task_id: int,
+ project_id: Optional[int] = None,
+ unit: Optional[Unit] = None,
+ columns: Optional[Iterable[str]] = None,
+ tm_ids: Optional[Iterable[int]] = None,
+ mt_ids: Optional[Iterable[int]] = None,
+ ai_prompt_ids: Optional[Iterable[int]] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ ):
+ """
+ Generate Report(Contribution Raw Data By Task).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
+ """
+
+ project_id = project_id or self.get_project_id()
+
+ return self.generate_report(
+ projectId=project_id,
+ request_data={
+ "name": "contribution-raw-data",
+ "schema": {
+ "mode": mode,
+ "unit": unit,
+ "taskId": task_id,
+ "columns": columns,
+ "tmIds": tm_ids,
+ "mtIds": mt_ids,
+ "aiPromptIds": ai_prompt_ids,
+ "dateFrom": date_from,
+ "dateTo": date_to,
},
},
)
@@ -169,13 +239,13 @@ def generate_source_content_updates_report(
date_to: Optional[datetime] = None,
):
"""
- Generate Report(Contribution Raw Data).
+ Generate Report(Source Content Updates).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -204,10 +274,10 @@ def generate_project_members_report(
Generate Report(Project Members).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -230,16 +300,16 @@ def generate_editor_issues_report(
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
format: Optional[Format] = Format.XLSX,
- issue_type: Optional[str] = None,
+ issue_type: Optional[Union[EditorIssueType, str]] = None,
):
"""
Generate Report(Editor Issues).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -263,15 +333,16 @@ def generate_qa_check_issues_report(
format: Optional[Format] = Format.XLSX,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
+ language_id: Optional[str] = None,
):
"""
Generate Report(Qa Check Issues).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -282,6 +353,7 @@ def generate_qa_check_issues_report(
"name": "qa-check-issues",
"schema": {
"format": format,
+ "languageId": language_id,
"dateFrom": date_from,
"dateTo": date_to,
},
@@ -296,15 +368,22 @@ def generate_saving_activity_report(
format: Optional[Format] = Format.XLSX,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
+ mode: Optional[SavingActivityMode] = None,
+ file_ids: Optional[Iterable[int]] = None,
+ directory_ids: Optional[Iterable[int]] = None,
+ branch_ids: Optional[Iterable[int]] = None,
+ user_ids: Optional[Iterable[int]] = None,
):
"""
Generate Report(Saving Activity).
+ `file_ids` and `directory_ids` are available for file-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -319,6 +398,11 @@ def generate_saving_activity_report(
"format": format,
"dateFrom": date_from,
"dateTo": date_to,
+ "mode": mode,
+ "fileIds": file_ids,
+ "directoryIds": directory_ids,
+ "branchIds": branch_ids,
+ "userIds": user_ids,
},
},
)
@@ -331,15 +415,16 @@ def generate_translation_activity_report(
format: Optional[Format] = Format.XLSX,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
):
"""
Generate Report(Translation Activity).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -354,6 +439,7 @@ def generate_translation_activity_report(
"format": format,
"dateFrom": date_from,
"dateTo": date_to,
+ "userIds": user_ids,
},
},
)
@@ -367,15 +453,26 @@ def generate_pre_translate_accuracy_general_report(
languageId: Optional[str] = None,
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ matchScoreCategories: Optional[Iterable[str]] = None,
+ fileIds: Optional[Iterable[int]] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ branchIds: Optional[Iterable[int]] = None,
+ labelIds: Optional[Iterable[int]] = None,
+ labelIncludeType: Optional[ReportLabelIncludeType] = None,
+ skipArchiving: Optional[bool] = None,
):
"""
- Generate Report.
+ Generate Report(Pre-Translate Accuracy General).
+
+ `postEditingCategories` is deprecated by the API, use `matchScoreCategories` instead.
+
+ `fileIds` and `directoryIds` are available for file-based projects only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -388,9 +485,16 @@ def generate_pre_translate_accuracy_general_report(
"unit": unit,
"format": format,
"postEditingCategories": postEditingCategories,
+ "matchScoreCategories": matchScoreCategories,
"languageId": languageId,
"dateFrom": dateFrom,
"dateTo": dateTo,
+ "fileIds": fileIds,
+ "directoryIds": directoryIds,
+ "branchIds": branchIds,
+ "labelIds": labelIds,
+ "labelIncludeType": labelIncludeType,
+ "skipArchiving": skipArchiving,
},
},
)
@@ -402,15 +506,19 @@ def generate_pre_translate_accuracy_by_task_report(
format: Optional[Format] = None,
postEditingCategories: Optional[Iterable[str]] = None,
taskId: Optional[int] = None,
+ matchScoreCategories: Optional[Iterable[str]] = None,
+ skipArchiving: Optional[bool] = None,
):
"""
- Generate Report.
+ Generate Report(Pre-Translate Accuracy By Task).
+
+ `postEditingCategories` is deprecated by the API, use `matchScoreCategories` instead.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -423,7 +531,62 @@ def generate_pre_translate_accuracy_by_task_report(
"unit": unit,
"format": format,
"postEditingCategories": postEditingCategories,
+ "matchScoreCategories": matchScoreCategories,
"taskId": taskId,
+ "skipArchiving": skipArchiving,
+ },
+ },
+ )
+
+ def generate_translator_accuracy_report(
+ self,
+ project_id: Optional[int] = None,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ language_id: Optional[str] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ file_ids: Optional[Iterable[int]] = None,
+ directory_ids: Optional[Iterable[int]] = None,
+ branch_ids: Optional[Iterable[int]] = None,
+ label_ids: Optional[Iterable[int]] = None,
+ label_include_type: Optional[ReportLabelIncludeType] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Report(Translator Accuracy).
+
+ `file_ids` and `directory_ids` are available for file-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
+ """
+
+ project_id = project_id or self.get_project_id()
+
+ return self.generate_report(
+ projectId=project_id,
+ request_data={
+ "name": "translator-accuracy",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "matchScoreCategories": match_score_categories,
+ "languageId": language_id,
+ "userIds": user_ids,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "fileIds": file_ids,
+ "directoryIds": directory_ids,
+ "branchIds": branch_ids,
+ "labelIds": label_ids,
+ "labelIncludeType": label_include_type,
+ "skipArchiving": skip_archiving,
},
},
)
@@ -446,16 +609,22 @@ def generate_costs_estimation_post_editing_general_report(
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
label_ids: Optional[Iterable[int]] = None,
- label_include_type: Optional[ReportLabelIncludeType] = None
+ label_include_type: Optional[ReportLabelIncludeType] = None,
+ skip_archiving: Optional[bool] = None,
+ workflow_step_id: Optional[int] = None,
):
"""
- Generate Report.
+ Generate Report(Costs Estimation Post-Editing General).
+
+ `workflow_step_id` is available for Crowdin Enterprise only.
+
+ `file_ids` and `directory_ids` are available for file-based projects only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -480,7 +649,9 @@ def generate_costs_estimation_post_editing_general_report(
"dateFrom": date_from,
"dateTo": date_to,
"labelIds": label_ids,
- "labelIncludeType": label_include_type
+ "labelIncludeType": label_include_type,
+ "skipArchiving": skip_archiving,
+ "workflowStepId": workflow_step_id,
}
}
)
@@ -496,16 +667,20 @@ def generate_costs_estimation_post_editing_by_task_report(
net_rate_schemes: Optional[CostEstimationPeNetRateSchemes] = None,
calculate_internal_matches: Optional[bool] = None,
include_pre_translated_strings: Optional[bool] = None,
- task_id: Optional[int] = None
+ task_id: Optional[int] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
):
"""
- Generate Report.
+ Generate Report(Costs Estimation Post-Editing By Task).
+
+ `task_id` is deprecated by the API, use `task_ids` instead.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -523,7 +698,9 @@ def generate_costs_estimation_post_editing_by_task_report(
"netRateSchemes": net_rate_schemes,
"calculateInternalMatches": calculate_internal_matches,
"includePreTranslatedStrings": include_pre_translated_strings,
- "taskId": task_id
+ "taskId": task_id,
+ "taskIds": task_ids,
+ "skipArchiving": skip_archiving,
}
}
)
@@ -544,16 +721,26 @@ def generate_translation_costs_post_editing_general_report(
user_ids: Optional[Iterable[int]] = None,
file_ids: Optional[Iterable[int]] = None,
directory_ids: Optional[Iterable[int]] = None,
- branch_ids: Optional[Iterable[int]] = None
+ branch_ids: Optional[Iterable[int]] = None,
+ use_category_based_proofread_rates: Optional[bool] = None,
+ use_tm_edit_distance: Optional[bool] = None,
+ label_ids: Optional[Iterable[int]] = None,
+ label_include_type: Optional[ReportLabelIncludeType] = None,
+ skip_archiving: Optional[bool] = None,
+ workflow_step_id: Optional[int] = None,
):
"""
- Generate Report.
+ Generate Report(Translation Costs Post-Editing General).
+
+ `workflow_step_id` is available for Crowdin Enterprise only.
+
+ `file_ids` and `directory_ids` are available for file-based projects only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -576,7 +763,13 @@ def generate_translation_costs_post_editing_general_report(
"userIds": user_ids,
"fileIds": file_ids,
"directoryIds": directory_ids,
- "branchIds": branch_ids
+ "branchIds": branch_ids,
+ "useCategoryBasedProofreadRates": use_category_based_proofread_rates,
+ "useTmEditDistance": use_tm_edit_distance,
+ "labelIds": label_ids,
+ "labelIncludeType": label_include_type,
+ "skipArchiving": skip_archiving,
+ "workflowStepId": workflow_step_id,
}
}
)
@@ -590,16 +783,22 @@ def generate_translation_costs_post_editing_by_task_report(
unit: Optional[Unit] = None,
currency: Optional[Currency] = None,
format: Optional[Format] = None,
- task_id: Optional[int] = None
+ task_id: Optional[int] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ use_category_based_proofread_rates: Optional[bool] = None,
+ use_tm_edit_distance: Optional[bool] = None,
+ skip_archiving: Optional[bool] = None,
):
"""
- Generate Report.
+ Generate Report(Translation Costs Post-Editing By Task).
+
+ `task_id` is deprecated by the API, use `task_ids` instead.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
project_id = project_id or self.get_project_id()
@@ -615,11 +814,66 @@ def generate_translation_costs_post_editing_by_task_report(
"baseRates": base_rates,
"individualRates": individual_rates,
"netRateSchemes": net_rate_schemes,
- "taskId": task_id
+ "taskId": task_id,
+ "taskIds": task_ids,
+ "useCategoryBasedProofreadRates": use_category_based_proofread_rates,
+ "useTmEditDistance": use_tm_edit_distance,
+ "skipArchiving": skip_archiving,
}
}
)
+ def generate_time_spent_report(
+ self,
+ project_id: Optional[int] = None,
+ format: Optional[Format] = None,
+ group_by: Optional[GroupBy] = None,
+ base_rates: Optional[HourlyBaseRates] = None,
+ individual_rates: Optional[Iterable[HourlyIndividualRate]] = None,
+ language_id: Optional[str] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ type_tasks: Optional[TaskType] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ workflow_step_id: Optional[int] = None,
+ ):
+ """
+ Generate Report(Time Spent).
+
+ `workflow_step_id` is available for Crowdin Enterprise only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
+ """
+
+ project_id = project_id or self.get_project_id()
+
+ return self.generate_report(
+ projectId=project_id,
+ request_data={
+ "name": "time-spent",
+ "schema": {
+ "format": format,
+ "groupBy": group_by,
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "languageId": language_id,
+ "userIds": user_ids,
+ "typeTasks": type_tasks,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "taskIds": task_ids,
+ "workflowStepId": workflow_step_id,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
def check_report_generation_status(
self, reportId: str, projectId: Optional[int] = None
):
@@ -627,10 +881,10 @@ def check_report_generation_status(
Check Report Generation Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.get
"""
projectId = projectId or self.get_project_id()
@@ -645,10 +899,10 @@ def download_report(self, reportId: str, projectId: Optional[int] = None):
Download Report.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.download.download
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.download.download
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.download.download
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.download.download
"""
projectId = projectId or self.get_project_id()
@@ -680,10 +934,10 @@ def list_report_settings_template(
List Report Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.settings-templates.getMany
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.settings-templates.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.settings-templates.getMany
"""
projectId = projectId or self.get_project_id()
@@ -699,18 +953,21 @@ def add_report_settings_template(
name: str,
currency: Currency,
unit: Unit,
- config: Config,
+ config: Union[PostEditingConfig, HourlyConfig, Config],
isPublic: Optional[bool] = None,
projectId: Optional[int] = None,
+ isGlobal: Optional[bool] = None,
):
"""
Add Report Settings Templates.
+ `isGlobal` is available for Crowdin only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.settings-templates.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.settings-templates.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.settings-templates.post
"""
projectId = projectId or self.get_project_id()
@@ -724,9 +981,9 @@ def add_report_settings_template(
"name": name,
"currency": currency,
"unit": unit,
- "mode": "simple",
"config": config,
"isPublic": isPublic,
+ "isGlobal": isGlobal,
}
)
@@ -739,10 +996,10 @@ def get_report_settings_template(
Get Report Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.settings-templates.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.settings-templates.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.settings-templates.get
"""
projectId = projectId or self.get_project_id()
@@ -765,10 +1022,10 @@ def edit_report_settings_template(
Edit Report Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.settings-templates.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.settings-templates.patch
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.settings-templates.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.settings-templates.patch
"""
projectId = projectId or self.get_project_id()
@@ -791,10 +1048,10 @@ def delete_report_settings_template(
Delete Report Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.settings-templates.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.settings-templates.delete
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.settings-templates.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.settings-templates.delete
"""
projectId = projectId or self.get_project_id()
@@ -808,118 +1065,257 @@ def delete_report_settings_template(
)
-class ReportsResource(BaseReportsResource, BaseReportSettingsTemplatesResource):
+class UserReportSettingsTemplatesResource(BaseReportSettingsTemplatesResource):
"""
- Resource for Reports.
+ Resource for User Report Settings Templates API.
- Reports help to estimate costs, calculate translation costs, and identify the top members.
+ Supporting the endpoints for managing user report settings templates.
- Use API to generate Cost Estimate, Translation Cost, and Top Members reports. You can then
- export reports in .xlsx or .csv file formats. Report generation is an asynchronous operation
- and shall be completed with a sequence of API methods.
+ These methods are also available on `ReportsResource` and `EnterpriseReportsResource`
+ (both platforms support `/users/{userId}/reports/settings-templates`).
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Reports
+ https://support.crowdin.com/developer/api/v2/#tag/User-Report-Settings-Templates
"""
- def get_report_archive_path(self, userId: int, archiveId: Optional[int] = None):
- if archiveId is not None:
- return f"users/{userId}/reports/archives/{archiveId}"
-
- return f"users/{userId}/reports/archives"
-
- def get_report_archive_export_path(
+ def get_user_report_settings_templates_path(
self,
userId: int,
- archiveId: int,
- exportId: Optional[str] = None,
+ reportSettingsTemplateId: Optional[int] = None
):
- if exportId is not None:
- return f"users/{userId}/reports/archives/{archiveId}/exports/{exportId}"
+ if reportSettingsTemplateId is not None:
+ return f"users/{userId}/reports/settings-templates/{reportSettingsTemplateId}"
- return f"users/{userId}/reports/archives/{archiveId}/exports"
+ return f"users/{userId}/reports/settings-templates"
- def list_report_archives(
+ def list_user_report_settings_template(
self,
userId: int,
- scopeType: Optional[str] = None,
- scopeId: Optional[int] = None,
- limit: Optional[int] = None,
offset: Optional[int] = None,
+ limit: Optional[int] = None
):
"""
- List Report Archives
+ List User Report Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.reports.archives.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.settings-templates.getMany
"""
- params = {"scopeType": scopeType, "scopeId": scopeId}
- params.update(self.get_page_params(limit=limit, offset=offset))
-
- return self.requester.request(
+ return self._get_entire_data(
method="get",
- path=self.get_report_archive_path(userId=userId),
- params=params,
+ path=self.get_user_report_settings_templates_path(userId=userId),
+ params=self.get_page_params(offset=offset, limit=limit),
)
- def get_report_archive(self, userId: int, archiveId: int):
+ def add_user_report_settings_template(
+ self,
+ userId: int,
+ name: str,
+ currency: Currency,
+ unit: Unit,
+ config: Union[PostEditingConfig, HourlyConfig, Config],
+ ):
"""
- Get Report Archive
+ Add User Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.archives.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.settings-templates.post
"""
return self.requester.request(
- method="get",
- path=self.get_report_archive_path(userId=userId, archiveId=archiveId),
+ method="post",
+ path=self.get_user_report_settings_templates_path(
+ userId=userId,
+ ),
+ request_data={
+ "name": name,
+ "currency": currency,
+ "unit": unit,
+ "config": config,
+ }
)
- def delete_report_archive(self, userId: int, archiveId: int):
+ def get_user_report_settings_template(
+ self,
+ userId: int,
+ reportSettingsTemplateId: int,
+ ):
"""
- Delete Report Archive
+ Get User Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.archives.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.settings-templates.get
"""
return self.requester.request(
- method="delete",
- path=self.get_report_archive_path(userId=userId, archiveId=archiveId),
+ method="get",
+ path=self.get_user_report_settings_templates_path(
+ userId=userId,
+ reportSettingsTemplateId=reportSettingsTemplateId
+ ),
)
- def export_report_archive(
- self, userId: int, archiveId: int, format: Optional[ExportFormat] = None
+ def edit_user_report_settings_template(
+ self,
+ userId: int,
+ reportSettingsTemplateId: int,
+ data: Iterable[ReportSettingsTemplatesPatchRequest],
):
"""
- Export Report Archive
+ Edit User Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.reports.archives.exports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.settings-templates.patch
"""
- format = format or ExportFormat.XLSX
return self.requester.request(
- method="post",
- path=self.get_report_archive_export_path(
- userId=userId, archiveId=archiveId
+ method="patch",
+ path=self.get_user_report_settings_templates_path(
+ userId=userId,
+ reportSettingsTemplateId=reportSettingsTemplateId
),
- request_data={"format": format},
+ request_data=data,
)
- def check_report_archive_export_status(
- self, userId: int, archiveId: int, exportId: str
+ def delete_user_report_settings_template(
+ self,
+ userId: int,
+ reportSettingsTemplateId: int,
):
"""
- Check Report Archive Status
+ Delete User Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.archives.exports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.settings-templates.delete
"""
return self.requester.request(
- method="get",
- path=self.get_report_archive_export_path(
- userId=userId, archiveId=archiveId, exportId=exportId
- ),
- )
-
+ method="delete",
+ path=self.get_user_report_settings_templates_path(
+ userId=userId,
+ reportSettingsTemplateId=reportSettingsTemplateId
+ ),
+ )
+
+
+class ReportsResource(BaseReportsResource, UserReportSettingsTemplatesResource):
+ """
+ Resource for Reports.
+
+ Reports help to estimate costs, calculate translation costs, and identify the top members.
+
+ Use API to generate Cost Estimate, Translation Cost, and Top Members reports. You can then
+ export reports in .xlsx or .csv file formats. Report generation is an asynchronous operation
+ and shall be completed with a sequence of API methods.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Reports
+ """
+
+ def get_report_archive_path(self, userId: int, archiveId: Optional[int] = None):
+ if archiveId is not None:
+ return f"users/{userId}/reports/archives/{archiveId}"
+
+ return f"users/{userId}/reports/archives"
+
+ def get_report_archive_export_path(
+ self,
+ userId: int,
+ archiveId: int,
+ exportId: Optional[str] = None,
+ ):
+ if exportId is not None:
+ return f"users/{userId}/reports/archives/{archiveId}/exports/{exportId}"
+
+ return f"users/{userId}/reports/archives/{archiveId}/exports"
+
+ def list_report_archives(
+ self,
+ userId: int,
+ scopeType: Optional[str] = None,
+ scopeId: Optional[int] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ taskId: Optional[int] = None,
+ name: Optional[str] = None,
+ dateFrom: Optional[datetime] = None,
+ dateTo: Optional[datetime] = None,
+ ):
+ """
+ List Report Archives
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.reports.archives.getMany
+ """
+ params = {
+ "scopeType": scopeType,
+ "scopeId": scopeId,
+ "taskId": taskId,
+ "name": name,
+ "dateFrom": dateFrom,
+ "dateTo": dateTo,
+ }
+ params.update(self.get_page_params(limit=limit, offset=offset))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_report_archive_path(userId=userId),
+ params=params,
+ )
+
+ def get_report_archive(self, userId: int, archiveId: int):
+ """
+ Get Report Archive
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.archives.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_report_archive_path(userId=userId, archiveId=archiveId),
+ )
+
+ def delete_report_archive(self, userId: int, archiveId: int):
+ """
+ Delete Report Archive
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.archives.delete
+ """
+ return self.requester.request(
+ method="delete",
+ path=self.get_report_archive_path(userId=userId, archiveId=archiveId),
+ )
+
+ def export_report_archive(
+ self, userId: int, archiveId: int, format: Optional[ExportFormat] = None
+ ):
+ """
+ Export Report Archive
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.reports.archives.exports.post
+ """
+ format = format or ExportFormat.XLSX
+ return self.requester.request(
+ method="post",
+ path=self.get_report_archive_export_path(
+ userId=userId, archiveId=archiveId
+ ),
+ request_data={"format": format},
+ )
+
+ def check_report_archive_export_status(
+ self, userId: int, archiveId: int, exportId: str
+ ):
+ """
+ Check Report Archive Status
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.archives.exports.get
+ """
+ return self.requester.request(
+ method="get",
+ path=self.get_report_archive_export_path(
+ userId=userId, archiveId=archiveId, exportId=exportId
+ ),
+ )
+
def download_report_archive(
self, userId: int, archiveId: int, exportId: str
):
@@ -927,7 +1323,7 @@ def download_report_archive(
Download Report Archive
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.archives.exports.download.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.reports.archives.exports.download.get
"""
path = str(
self.get_report_archive_export_path(
@@ -959,7 +1355,7 @@ def generate_simple_cost_estimate_report(
Generate Report(Cost Estimate Schema).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -1003,7 +1399,7 @@ def generate_fuzzy_cost_estimate_report(
Generate Report(Cost Estimate Fuzzy Mode).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -1045,7 +1441,7 @@ def generate_simple_translation_cost_report(
Generate Report(Translation Cost).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -1085,7 +1481,7 @@ def generate_fuzzy_translation_cost_report(
Generate Report(Translation Fuzzy Cost).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
@@ -1109,7 +1505,7 @@ def generate_fuzzy_translation_cost_report(
)
-class EnterpriseReportsResource(BaseReportsResource, BaseReportSettingsTemplatesResource):
+class EnterpriseReportsResource(BaseReportsResource, UserReportSettingsTemplatesResource):
"""
Resource for Enterprise Reports.
@@ -1120,7 +1516,7 @@ class EnterpriseReportsResource(BaseReportsResource, BaseReportSettingsTemplates
and shall be completed with a sequence of API methods.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Reports
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Reports
"""
@staticmethod
def _prepare_stepTypes(step_types_const: dict, stepTypes: Optional[Iterable[StepTypes]] = None):
@@ -1150,7 +1546,7 @@ def generate_simple_cost_estimate_report(
Generate Report(Cost Estimate schema).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
step_types_const = {
@@ -1194,7 +1590,7 @@ def generate_fuzzy_cost_estimate_report(
Generate Report(Cost Estimate Fuzzy Mode).
Links to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
step_types_const = {
@@ -1237,7 +1633,7 @@ def generate_simple_translation_cost_report(
Generate Report(Translation Cost).
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
step_types_const = {
@@ -1278,7 +1674,7 @@ def generate_fuzzy_translation_cost_report(
Generate Report(Translation Cost Fuzzy Mode).
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
projectId = projectId or self.get_project_id()
step_types_const = {
@@ -1325,17 +1721,30 @@ def list_report_archives(
scopeId: Optional[int] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
+ userId: Optional[int] = None,
+ taskId: Optional[int] = None,
+ name: Optional[str] = None,
+ dateFrom: Optional[datetime] = None,
+ dateTo: Optional[datetime] = None,
):
"""
List Report Archives
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.archives.getMany
- """
- params = {"scopeType": scopeType, "scopeId": scopeId}
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.archives.getMany
+ """
+ params = {
+ "scopeType": scopeType,
+ "scopeId": scopeId,
+ "userId": userId,
+ "taskId": taskId,
+ "name": name,
+ "dateFrom": dateFrom,
+ "dateTo": dateTo,
+ }
params.update(self.get_page_params(limit=limit, offset=offset))
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_report_archive_path(),
params=params,
@@ -1346,7 +1755,7 @@ def get_report_archive(self, archiveId: int):
Get Report Archive
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.archives.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.archives.get
"""
return self.requester.request(
method="get",
@@ -1358,7 +1767,7 @@ def delete_report_archive(self, archiveId: int):
Delete Report Archive
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.archives.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.archives.delete
"""
return self.requester.request(
method="delete",
@@ -1372,7 +1781,7 @@ def export_report_archive(
Export Report Archive
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.archives.exports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.archives.exports.post
"""
format = format or ExportFormat.XLSX
return self.requester.request(
@@ -1386,7 +1795,7 @@ def check_report_archive_export_status(self, archiveId: int, exportId: str):
Check Report Archive Status
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.archives.exports.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.archives.exports.get
"""
return self.requester.request(
method="get",
@@ -1400,7 +1809,7 @@ def download_report_archive(self, archiveId: int, exportId: str):
Download Report Archive
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.archives.exports.download.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.archives.exports.download.get
"""
path = str(
self.get_report_archive_export_path(archiveId=archiveId, exportId=exportId)
@@ -1411,337 +1820,1221 @@ def download_report_archive(self, archiveId: int, exportId: str):
path=path,
)
- @staticmethod
- def get_group_reports_path(group_id: int, report_id: Optional[str] = None):
- if report_id is not None:
- return f"groups/{group_id}/reports/{group_id}"
-
- return f"groups/{group_id}/reports"
-
- def generate_group_report(self, group_id: int, request_data: Dict):
- """
- Generate Group Report.
-
- Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.post
- """
-
- return self.requester.request(
- method="post",
- path=self.get_group_reports_path(group_id=group_id),
- request_data=request_data,
- )
-
- def generate_group_translation_costs_post_editing_general_report(
+ def generate_task_usage_report(
self,
- group_id: int,
- base_rates: BaseRates,
- individual_rates: Iterable[TranslationCostsPeIndividualRate],
- net_rate_schemes: TranslationCostsPeNetRateSchemes,
- project_ids: Optional[Iterable[int]] = None,
- unit: Optional[Unit] = None,
- currency: Optional[Currency] = None,
+ project_id: Optional[int] = None,
format: Optional[Format] = None,
- group_by: Optional[GroupBy] = None,
+ type: Optional[TaskUsageReportType] = None,
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
- user_ids: Optional[Iterable[int]] = None
+ group_by: Optional[GroupBy] = None,
+ type_tasks: Optional[TaskType] = None,
+ language_id: Optional[str] = None,
+ creator_id: Optional[int] = None,
+ assignee_id: Optional[int] = None,
+ words_count_from: Optional[int] = None,
+ words_count_to: Optional[int] = None,
+ statuses: Optional[Iterable[TaskUsageStatus]] = None,
):
"""
- Generate Group Report (General).
-
- Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.post
- """
-
- return self.generate_group_report(
- group_id=group_id,
- request_data={
- "name": "group-translation-costs-pe",
- "schema": {
- "projectIds": project_ids,
- "unit": unit,
- "currency": currency,
- "format": format,
- "baseRates": base_rates,
- "individualRates": individual_rates,
- "netRateSchemes": net_rate_schemes,
- "groupBy": group_by,
- "dateFrom": date_from,
- "dateTo": date_to,
- "userIds": user_ids
- },
- },
- )
+ Generate Report(Task Usage).
- def generate_group_task_usage_report(
- self,
- group_id: int,
- format: Optional[Format] = None,
- type: Optional[str] = None,
- project_ids: Optional[Iterable[int]] = None,
- date_from: Optional[datetime] = None,
- date_to: Optional[datetime] = None,
- group_by: Optional[GroupBy] = None,
- type_task: Optional[int] = None,
- language_id: Optional[str] = None,
- creator_id: Optional[int] = None,
- assignee_id: Optional[int] = None,
- ):
- """
- Generate Group Report (Task Usage Report).
+ `words_count_from` and `words_count_to` are used only with `time` type, `statuses` only
+ with `cost` type.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.reports.post
"""
- return self.generate_group_report(
- group_id=group_id,
+ project_id = project_id or self.get_project_id()
+
+ return self.generate_report(
+ projectId=project_id,
request_data={
- "name": "group-task-usage",
+ "name": "task-usage",
"schema": {
- "projectIds": project_ids,
"format": format,
"type": type,
"dateFrom": date_from,
"dateTo": date_to,
"groupBy": group_by,
- "typeTask": type_task,
+ "typeTasks": type_tasks,
"languageId": language_id,
"creatorId": creator_id,
"assigneeId": assignee_id,
+ "wordsCountFrom": words_count_from,
+ "wordsCountTo": words_count_to,
+ "statuses": statuses,
},
},
)
- def generate_group_qa_check_issues_report(
- self,
- group_id: int,
- project_ids: Optional[Iterable[int]] = None,
- format: Optional[Format] = None,
- date_from: Optional[datetime] = None,
- date_to: Optional[datetime] = None,
+ # Project report settings templates are not available in Crowdin Enterprise API anymore
+ @deprecated("Use `list_organization_report_settings_templates` instead")
+ def list_report_settings_template(
+ self,
+ projectId: Optional[int] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None
):
"""
- Generate Group Report (Group Qa Check Issues Report).
+ List Report Settings Templates.
+
+ Deprecated: use `list_organization_report_settings_templates` instead.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.getMany
"""
-
- return self.generate_group_report(
- group_id=group_id,
- request_data={
- "name": "group-qa-check-issues",
- "schema": {
- "projectIds": project_ids,
- "format": format,
- "dateFrom": date_from,
- "dateTo": date_to,
- },
- },
+ return super().list_report_settings_template(
+ projectId=projectId, offset=offset, limit=limit
)
- def generate_group_translation_activity_report(
- self,
- group_id: int,
- unit: Optional[Unit] = None,
- project_ids: Optional[Iterable[int]] = None,
- format: Optional[Format] = None,
- date_from: Optional[datetime] = None,
- date_to: Optional[datetime] = None,
+ @deprecated("Use `add_organization_report_settings_template` instead")
+ def add_report_settings_template(
+ self,
+ name: str,
+ currency: Currency,
+ unit: Unit,
+ config: Union[PostEditingConfig, HourlyConfig, Config],
+ isPublic: Optional[bool] = None,
+ projectId: Optional[int] = None,
+ isGlobal: Optional[bool] = None,
):
"""
- Generate Group Report (Group translation consumption).
+ Add Report Settings Templates.
- Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.reports.post
- """
+ Deprecated: use `add_organization_report_settings_template` instead.
- return self.generate_group_report(
- group_id=group_id,
- request_data={
- "name": "group-translation-activity",
- "schema": {
- "unit": unit,
- "projectIds": project_ids,
- "format": format,
- "dateFrom": date_from,
- "dateTo": date_to,
- },
- },
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.post
+ """
+ return super().add_report_settings_template(
+ name=name,
+ currency=currency,
+ unit=unit,
+ config=config,
+ isPublic=isPublic,
+ projectId=projectId,
+ isGlobal=isGlobal,
)
- @staticmethod
- def get_organization_reports_path(report_id: Optional[str] = None):
- if report_id is not None:
- return f"reports/{report_id}"
-
- return "reports"
-
- def generate_organization_translation_costs_post_editing_general_report(
+ @deprecated("Use `get_organization_report_settings_template` instead")
+ def get_report_settings_template(
self,
- base_rates: BaseRates,
- individual_rates: Iterable[TranslationCostsPeIndividualRate],
- net_rate_schemes: TranslationCostsPeNetRateSchemes,
- project_ids: Optional[Iterable[int]] = None,
- unit: Optional[Unit] = None,
- currency: Optional[Currency] = None,
- format: Optional[Format] = None,
- group_by: Optional[GroupBy] = None,
- date_from: Optional[datetime] = None,
- date_to: Optional[datetime] = None,
- user_ids: Optional[Iterable[int]] = None
+ reportSettingsTemplateId: int,
+ projectId: Optional[int] = None,
):
"""
- Generate Organization Report (General).
+ Get Report Settings Templates.
+
+ Deprecated: use `get_organization_report_settings_template` instead.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.reports.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.get
"""
+ return super().get_report_settings_template(
+ reportSettingsTemplateId=reportSettingsTemplateId, projectId=projectId
+ )
- return self.requester.request(
- method="post",
- path=self.get_organization_reports_path(),
- request_data={
- "name": "group-translation-costs-pe",
- "schema": {
- "projectIds": project_ids,
- "unit": unit,
- "currency": currency,
- "format": format,
- "baseRates": base_rates,
- "individualRates": individual_rates,
- "netRateSchemes": net_rate_schemes,
- "groupBy": group_by,
- "dateFrom": date_from,
- "dateTo": date_to,
- "userIds": user_ids
- },
- },
+ @deprecated("Use `edit_organization_report_settings_template` instead")
+ def edit_report_settings_template(
+ self,
+ reportSettingsTemplateId: int,
+ data: Iterable[ReportSettingsTemplatesPatchRequest],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Edit Report Settings Templates.
+
+ Deprecated: use `edit_organization_report_settings_template` instead.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.patch
+ """
+ return super().edit_report_settings_template(
+ reportSettingsTemplateId=reportSettingsTemplateId, data=data, projectId=projectId
)
+ @deprecated("Use `delete_organization_report_settings_template` instead")
+ def delete_report_settings_template(
+ self,
+ reportSettingsTemplateId: int,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Delete Report Settings Templates.
-class UserReportSettingsTemplatesResource(BaseReportSettingsTemplatesResource):
- """
- Resource for User Report Settings Templates API.
+ Deprecated: use `delete_organization_report_settings_template` instead.
- Supporting the endpoints for managing user report settings templates.
- Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/User-Report-Settings-Templates
- """
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.delete
+ """
+ return super().delete_report_settings_template(
+ reportSettingsTemplateId=reportSettingsTemplateId, projectId=projectId
+ )
- def get_user_report_settings_templates_path(
- self,
- userId: int,
- reportSettingsTemplateId: Optional[int] = None
+ @staticmethod
+ def get_organization_report_settings_templates_path(
+ reportSettingsTemplateId: Optional[int] = None,
):
if reportSettingsTemplateId is not None:
- return f"users/{userId}/reports/settings-templates/{reportSettingsTemplateId}"
+ return f"reports/settings-templates/{reportSettingsTemplateId}"
- return f"users/{userId}/reports/settings-templates"
+ return "reports/settings-templates"
- def list_user_report_settings_template(
+ def list_organization_report_settings_templates(
self,
- userId: int,
+ project_id: Optional[int] = None,
+ group_id: Optional[int] = None,
offset: Optional[int] = None,
- limit: Optional[int] = None
+ limit: Optional[int] = None,
):
"""
- List User Report Settings Templates.
+ List Organization Report Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.settings-templates.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.getMany
"""
+ params = {"projectId": project_id, "groupId": group_id}
+ params.update(self.get_page_params(offset=offset, limit=limit))
+
return self._get_entire_data(
method="get",
- path=self.get_user_report_settings_templates_path(userId=userId),
- params=self.get_page_params(offset=offset, limit=limit),
+ path=self.get_organization_report_settings_templates_path(),
+ params=params,
)
- def add_user_report_settings_template(
+ def add_organization_report_settings_template(
self,
- userId: int,
name: str,
currency: Currency,
unit: Unit,
- config: Config,
+ config: Union[PostEditingConfig, HourlyConfig],
+ project_id: Optional[int] = None,
+ group_id: Optional[int] = None,
+ is_public: Optional[bool] = None,
):
"""
- Add User Report Settings Template.
+ Add Organization Report Settings Template.
+
+ `project_id` and `group_id` can't be used together.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.settings-templates.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.post
"""
return self.requester.request(
method="post",
- path=self.get_user_report_settings_templates_path(
- userId=userId,
- ),
+ path=self.get_organization_report_settings_templates_path(),
request_data={
+ "projectId": project_id,
+ "groupId": group_id,
"name": name,
"currency": currency,
"unit": unit,
- "mode": "simple",
"config": config,
- }
+ "isPublic": is_public,
+ },
)
- def get_user_report_settings_template(
- self,
- userId: int,
- reportSettingsTemplateId: int,
- ):
+ def get_organization_report_settings_template(self, reportSettingsTemplateId: int):
"""
- Get User Report Settings Template.
+ Get Organization Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.settings-templates.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.get
"""
return self.requester.request(
method="get",
- path=self.get_user_report_settings_templates_path(
- userId=userId,
+ path=self.get_organization_report_settings_templates_path(
reportSettingsTemplateId=reportSettingsTemplateId
),
)
- def edit_user_report_settings_template(
+ def edit_organization_report_settings_template(
self,
- userId: int,
reportSettingsTemplateId: int,
data: Iterable[ReportSettingsTemplatesPatchRequest],
):
"""
- Edit User Report Settings Template.
+ Edit Organization Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.settings-templates.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.patch
"""
return self.requester.request(
method="patch",
- path=self.get_user_report_settings_templates_path(
- userId=userId,
+ path=self.get_organization_report_settings_templates_path(
reportSettingsTemplateId=reportSettingsTemplateId
),
request_data=data,
)
- def delete_user_report_settings_template(
- self,
- userId: int,
- reportSettingsTemplateId: int,
- ):
+ def delete_organization_report_settings_template(self, reportSettingsTemplateId: int):
"""
- Delete User Report Settings Template.
+ Delete Organization Report Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.reports.settings-templates.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.settings-templates.delete
"""
return self.requester.request(
method="delete",
- path=self.get_user_report_settings_templates_path(
- userId=userId,
+ path=self.get_organization_report_settings_templates_path(
reportSettingsTemplateId=reportSettingsTemplateId
),
)
+
+ @staticmethod
+ def get_group_reports_path(group_id: int, report_id: Optional[str] = None):
+ if report_id is not None:
+ return f"groups/{group_id}/reports/{report_id}"
+
+ return f"groups/{group_id}/reports"
+
+ def generate_group_report(self, group_id: int, request_data: Dict):
+ """
+ Generate Group Report.
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.requester.request(
+ method="post",
+ path=self.get_group_reports_path(group_id=group_id),
+ request_data=request_data,
+ )
+
+ def check_group_report_generation_status(self, group_id: int, report_id: str):
+ """
+ Check Group Report Generation Status.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_group_reports_path(group_id=group_id, report_id=report_id),
+ )
+
+ def download_group_report(self, group_id: int, report_id: str):
+ """
+ Download Group Report.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.download.download
+ """
+
+ return self.requester.request(
+ method="get",
+ path=f"{self.get_group_reports_path(group_id=group_id, report_id=report_id)}/download",
+ )
+
+ @staticmethod
+ def get_organization_reports_path(report_id: Optional[str] = None):
+ if report_id is not None:
+ return f"reports/{report_id}"
+
+ return "reports"
+
+ def generate_organization_report(self, request_data: Dict):
+ """
+ Generate Organization Report.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.requester.request(
+ method="post",
+ path=self.get_organization_reports_path(),
+ request_data=request_data,
+ )
+
+ def check_organization_report_generation_status(self, report_id: str):
+ """
+ Check Organization Report Generation Status.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.get
+ """
+
+ return self.requester.request(
+ method="get",
+ path=self.get_organization_reports_path(report_id=report_id),
+ )
+
+ def download_organization_report(self, report_id: str):
+ """
+ Download Organization Report.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.download.download
+ """
+
+ return self.requester.request(
+ method="get",
+ path=f"{self.get_organization_reports_path(report_id=report_id)}/download",
+ )
+
+ def generate_group_translation_costs_post_editing_general_report(
+ self,
+ group_id: int,
+ base_rates: BaseRates,
+ individual_rates: Iterable[TranslationCostsPeIndividualRate],
+ net_rate_schemes: TranslationCostsPeNetRateSchemes,
+ project_ids: Optional[Iterable[int]] = None,
+ unit: Optional[Unit] = None,
+ currency: Optional[Currency] = None,
+ format: Optional[Format] = None,
+ group_by: Optional[GroupBy] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ use_category_based_proofread_rates: Optional[bool] = None,
+ use_tm_edit_distance: Optional[bool] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Group Report (Translation Costs Post-Editing General).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-translation-costs-pe",
+ "schema": {
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "netRateSchemes": net_rate_schemes,
+ "projectIds": project_ids,
+ "unit": unit,
+ "currency": currency,
+ "format": format,
+ "groupBy": group_by,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "userIds": user_ids,
+ "useCategoryBasedProofreadRates": use_category_based_proofread_rates,
+ "useTmEditDistance": use_tm_edit_distance,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_group_translation_costs_post_editing_by_task_report(
+ self,
+ group_id: int,
+ base_rates: BaseRates,
+ individual_rates: Iterable[TranslationCostsPeIndividualRate],
+ net_rate_schemes: TranslationCostsPeNetRateSchemes,
+ unit: Optional[Unit] = None,
+ currency: Optional[Currency] = None,
+ format: Optional[Format] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ use_category_based_proofread_rates: Optional[bool] = None,
+ use_tm_edit_distance: Optional[bool] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Group Report (Translation Costs Post-Editing By Task).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-translation-costs-pe",
+ "schema": {
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "netRateSchemes": net_rate_schemes,
+ "unit": unit,
+ "currency": currency,
+ "format": format,
+ "taskIds": task_ids,
+ "useCategoryBasedProofreadRates": use_category_based_proofread_rates,
+ "useTmEditDistance": use_tm_edit_distance,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_group_top_members_report(
+ self,
+ group_id: int,
+ project_ids: Optional[Iterable[int]] = None,
+ unit: Optional[Unit] = None,
+ language_id: Optional[str] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ ):
+ """
+ Generate Group Report (Top Members).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-top-members",
+ "schema": {
+ "projectIds": project_ids,
+ "unit": unit,
+ "languageId": language_id,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "userIds": user_ids,
+ },
+ },
+ )
+
+ def generate_group_task_usage_report(
+ self,
+ group_id: int,
+ format: Optional[Format] = None,
+ type: Optional[TaskUsageReportType] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ group_by: Optional[GroupBy] = None,
+ type_task: Optional[TaskType] = None,
+ language_id: Optional[str] = None,
+ creator_id: Optional[int] = None,
+ assignee_id: Optional[int] = None,
+ words_count_from: Optional[int] = None,
+ words_count_to: Optional[int] = None,
+ statuses: Optional[Iterable[TaskUsageStatus]] = None,
+ ):
+ """
+ Generate Group Report (Task Usage).
+
+ `words_count_from` and `words_count_to` are used only with `time` type, `statuses` only
+ with `cost` type. `type_task` is sent as `typeTasks`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-task-usage",
+ "schema": {
+ "format": format,
+ "type": type,
+ "projectIds": project_ids,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "groupBy": group_by,
+ "typeTasks": type_task,
+ "languageId": language_id,
+ "creatorId": creator_id,
+ "assigneeId": assignee_id,
+ "wordsCountFrom": words_count_from,
+ "wordsCountTo": words_count_to,
+ "statuses": statuses,
+ },
+ },
+ )
+
+ def generate_group_qa_check_issues_report(
+ self,
+ group_id: int,
+ project_ids: Optional[Iterable[int]] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ ):
+ """
+ Generate Group Report (Qa Check Issues).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-qa-check-issues",
+ "schema": {
+ "projectIds": project_ids,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ },
+ },
+ )
+
+ def generate_group_translation_activity_report(
+ self,
+ group_id: int,
+ unit: Optional[Unit] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ ):
+ """
+ Generate Group Report (Translation Activity).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-translation-activity",
+ "schema": {
+ "unit": unit,
+ "projectIds": project_ids,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "userIds": user_ids,
+ },
+ },
+ )
+
+ def generate_group_source_content_updates_report(
+ self,
+ group_id: int,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ ):
+ """
+ Generate Group Report (Source Content Updates).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-source-content-updates",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "projectIds": project_ids,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ },
+ },
+ )
+
+ def generate_group_time_spent_report(
+ self,
+ group_id: int,
+ format: Optional[Format] = None,
+ group_by: Optional[GroupBy] = None,
+ base_rates: Optional[HourlyBaseRates] = None,
+ individual_rates: Optional[Iterable[HourlyIndividualRate]] = None,
+ language_id: Optional[str] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ type_tasks: Optional[TaskType] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Group Report (Time Spent).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-time-spent",
+ "schema": {
+ "format": format,
+ "groupBy": group_by,
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "languageId": language_id,
+ "userIds": user_ids,
+ "typeTasks": type_tasks,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "projectIds": project_ids,
+ "taskIds": task_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_group_pre_translate_accuracy_general_report(
+ self,
+ group_id: int,
+ unit: Optional[Unit] = None,
+ language_id: Optional[str] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Group Report (Pre-Translate Accuracy General).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-pre-translate-accuracy",
+ "schema": {
+ "unit": unit,
+ "languageId": language_id,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "matchScoreCategories": match_score_categories,
+ "projectIds": project_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_group_pre_translate_accuracy_by_task_report(
+ self,
+ group_id: int,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Group Report (Pre-Translate Accuracy By Task).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-pre-translate-accuracy",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "matchScoreCategories": match_score_categories,
+ "taskIds": task_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_group_translator_accuracy_report(
+ self,
+ group_id: int,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ language_id: Optional[str] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Group Report (Translator Accuracy).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-translator-accuracy",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "languageId": language_id,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "matchScoreCategories": match_score_categories,
+ "userIds": user_ids,
+ "projectIds": project_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_group_saving_activity_report(
+ self,
+ group_id: int,
+ unit: Optional[Unit] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ language_id: Optional[str] = None,
+ mode: Optional[SavingActivityMode] = None,
+ ):
+ """
+ Generate Group Report (Saving Activity).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.groups.reports.post
+ """
+
+ return self.generate_group_report(
+ group_id=group_id,
+ request_data={
+ "name": "group-saving-activity",
+ "schema": {
+ "unit": unit,
+ "projectIds": project_ids,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "languageId": language_id,
+ "mode": mode,
+ },
+ },
+ )
+
+ def generate_organization_translation_costs_post_editing_general_report(
+ self,
+ base_rates: BaseRates,
+ individual_rates: Iterable[TranslationCostsPeIndividualRate],
+ net_rate_schemes: TranslationCostsPeNetRateSchemes,
+ project_ids: Optional[Iterable[int]] = None,
+ unit: Optional[Unit] = None,
+ currency: Optional[Currency] = None,
+ format: Optional[Format] = None,
+ group_by: Optional[GroupBy] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ use_category_based_proofread_rates: Optional[bool] = None,
+ use_tm_edit_distance: Optional[bool] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Organization Report (Translation Costs Post-Editing General).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-translation-costs-pe",
+ "schema": {
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "netRateSchemes": net_rate_schemes,
+ "projectIds": project_ids,
+ "unit": unit,
+ "currency": currency,
+ "format": format,
+ "groupBy": group_by,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "userIds": user_ids,
+ "useCategoryBasedProofreadRates": use_category_based_proofread_rates,
+ "useTmEditDistance": use_tm_edit_distance,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_organization_translation_costs_post_editing_by_task_report(
+ self,
+ base_rates: BaseRates,
+ individual_rates: Iterable[TranslationCostsPeIndividualRate],
+ net_rate_schemes: TranslationCostsPeNetRateSchemes,
+ unit: Optional[Unit] = None,
+ currency: Optional[Currency] = None,
+ format: Optional[Format] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ use_category_based_proofread_rates: Optional[bool] = None,
+ use_tm_edit_distance: Optional[bool] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Organization Report (Translation Costs Post-Editing By Task).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-translation-costs-pe",
+ "schema": {
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "netRateSchemes": net_rate_schemes,
+ "unit": unit,
+ "currency": currency,
+ "format": format,
+ "taskIds": task_ids,
+ "useCategoryBasedProofreadRates": use_category_based_proofread_rates,
+ "useTmEditDistance": use_tm_edit_distance,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_organization_top_members_report(
+ self,
+ project_ids: Optional[Iterable[int]] = None,
+ unit: Optional[Unit] = None,
+ language_id: Optional[str] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ ):
+ """
+ Generate Organization Report (Top Members).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-top-members",
+ "schema": {
+ "projectIds": project_ids,
+ "unit": unit,
+ "languageId": language_id,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "userIds": user_ids,
+ },
+ },
+ )
+
+ def generate_organization_task_usage_report(
+ self,
+ format: Optional[Format] = None,
+ type: Optional[TaskUsageReportType] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ group_by: Optional[GroupBy] = None,
+ type_tasks: Optional[TaskType] = None,
+ language_id: Optional[str] = None,
+ creator_id: Optional[int] = None,
+ assignee_id: Optional[int] = None,
+ words_count_from: Optional[int] = None,
+ words_count_to: Optional[int] = None,
+ statuses: Optional[Iterable[TaskUsageStatus]] = None,
+ ):
+ """
+ Generate Organization Report (Task Usage).
+
+ `words_count_from` and `words_count_to` are used only with `time` type, `statuses` only
+ with `cost` type.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-task-usage",
+ "schema": {
+ "format": format,
+ "type": type,
+ "projectIds": project_ids,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "groupBy": group_by,
+ "typeTasks": type_tasks,
+ "languageId": language_id,
+ "creatorId": creator_id,
+ "assigneeId": assignee_id,
+ "wordsCountFrom": words_count_from,
+ "wordsCountTo": words_count_to,
+ "statuses": statuses,
+ },
+ },
+ )
+
+ def generate_organization_qa_check_issues_report(
+ self,
+ project_ids: Optional[Iterable[int]] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ ):
+ """
+ Generate Organization Report (Qa Check Issues).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-qa-check-issues",
+ "schema": {
+ "projectIds": project_ids,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ },
+ },
+ )
+
+ def generate_organization_translation_activity_report(
+ self,
+ unit: Optional[Unit] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ ):
+ """
+ Generate Organization Report (Translation Activity).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-translation-activity",
+ "schema": {
+ "unit": unit,
+ "projectIds": project_ids,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "userIds": user_ids,
+ },
+ },
+ )
+
+ def generate_organization_source_content_updates_report(
+ self,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ ):
+ """
+ Generate Organization Report (Source Content Updates).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-source-content-updates",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "projectIds": project_ids,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ },
+ },
+ )
+
+ def generate_organization_time_spent_report(
+ self,
+ format: Optional[Format] = None,
+ group_by: Optional[GroupBy] = None,
+ base_rates: Optional[HourlyBaseRates] = None,
+ individual_rates: Optional[Iterable[HourlyIndividualRate]] = None,
+ language_id: Optional[str] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ type_tasks: Optional[TaskType] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Organization Report (Time Spent).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-time-spent",
+ "schema": {
+ "format": format,
+ "groupBy": group_by,
+ "baseRates": base_rates,
+ "individualRates": individual_rates,
+ "languageId": language_id,
+ "userIds": user_ids,
+ "typeTasks": type_tasks,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "projectIds": project_ids,
+ "taskIds": task_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_organization_pre_translate_accuracy_general_report(
+ self,
+ unit: Optional[Unit] = None,
+ language_id: Optional[str] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Organization Report (Pre-Translate Accuracy General).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-pre-translate-accuracy",
+ "schema": {
+ "unit": unit,
+ "languageId": language_id,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "matchScoreCategories": match_score_categories,
+ "projectIds": project_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_organization_pre_translate_accuracy_by_task_report(
+ self,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ task_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Organization Report (Pre-Translate Accuracy By Task).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-pre-translate-accuracy",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "matchScoreCategories": match_score_categories,
+ "taskIds": task_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_organization_translator_accuracy_report(
+ self,
+ unit: Optional[Unit] = None,
+ format: Optional[Format] = None,
+ language_id: Optional[str] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ match_score_categories: Optional[Iterable[str]] = None,
+ user_ids: Optional[Iterable[int]] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ skip_archiving: Optional[bool] = None,
+ ):
+ """
+ Generate Organization Report (Translator Accuracy).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-translator-accuracy",
+ "schema": {
+ "unit": unit,
+ "format": format,
+ "languageId": language_id,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "matchScoreCategories": match_score_categories,
+ "userIds": user_ids,
+ "projectIds": project_ids,
+ "skipArchiving": skip_archiving,
+ },
+ },
+ )
+
+ def generate_organization_saving_activity_report(
+ self,
+ unit: Optional[Unit] = None,
+ project_ids: Optional[Iterable[int]] = None,
+ format: Optional[Format] = None,
+ date_from: Optional[datetime] = None,
+ date_to: Optional[datetime] = None,
+ language_id: Optional[str] = None,
+ mode: Optional[SavingActivityMode] = None,
+ ):
+ """
+ Generate Organization Report (Saving Activity).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.reports.post
+ """
+
+ return self.generate_organization_report(
+ request_data={
+ "name": "group-saving-activity",
+ "schema": {
+ "unit": unit,
+ "projectIds": project_ids,
+ "format": format,
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "languageId": language_id,
+ "mode": mode,
+ },
+ },
+ )
diff --git a/crowdin_api/api_resources/reports/tests/test_reports_resources.py b/crowdin_api/api_resources/reports/tests/test_reports_resources.py
index f36461b1..a2e11991 100644
--- a/crowdin_api/api_resources/reports/tests/test_reports_resources.py
+++ b/crowdin_api/api_resources/reports/tests/test_reports_resources.py
@@ -16,7 +16,10 @@
Unit,
ReportSettingsTemplatesPatchPath,
MatchType,
- ReportLabelIncludeType
+ ReportLabelIncludeType,
+ TaskType,
+ TaskUsageReportType,
+ TaskUsageStatus,
)
from crowdin_api.api_resources.reports.requests.cost_estimation_post_editing import (
IndividualRate,
@@ -85,6 +88,10 @@ def test_get_report_archive_export_path(self, incoming_data, path, base_absolut_
"scopeId": None,
"limit": 25,
"offset": 0,
+ "taskId": None,
+ "name": None,
+ "dateFrom": None,
+ "dateTo": None,
},
),
(
@@ -99,6 +106,10 @@ def test_get_report_archive_export_path(self, incoming_data, path, base_absolut_
"scopeId": 1,
"limit": 10,
"offset": 2,
+ "taskId": None,
+ "name": None,
+ "dateFrom": None,
+ "dateTo": None,
},
),
),
@@ -554,6 +565,7 @@ def test_generate_fuzzy_translation_cost_report(
"format": Format.XLSX,
"dateFrom": None,
"dateTo": None,
+ "userIds": None,
},
),
(
@@ -570,6 +582,7 @@ def test_generate_fuzzy_translation_cost_report(
"format": Format.JSON,
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
+ "userIds": None,
},
),
),
@@ -598,6 +611,13 @@ def test_generate_top_members_report(
"userId": None,
"dateFrom": None,
"dateTo": None,
+ "columns": None,
+ "tmIds": None,
+ "mtIds": None,
+ "aiPromptIds": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
},
),
(
@@ -616,6 +636,13 @@ def test_generate_top_members_report(
"userId": 1,
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
+ "columns": None,
+ "tmIds": None,
+ "mtIds": None,
+ "aiPromptIds": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
},
),
),
@@ -773,7 +800,8 @@ def test_generate_editor_issues_report(
{
"format": Format.XLSX,
"dateFrom": None,
- "dateTo": None
+ "dateTo": None,
+ "languageId": None,
},
),
(
@@ -786,6 +814,7 @@ def test_generate_editor_issues_report(
"format": Format.XLSX,
"dateFrom": datetime(2023, 4, 1),
"dateTo": datetime(2023, 4, 30),
+ "languageId": None,
},
),
]
@@ -819,6 +848,11 @@ def test_generate_qa_check_issues_report(
"format": Format.XLSX,
"dateFrom": None,
"dateTo": None,
+ "mode": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
+ "userIds": None,
},
),
(
@@ -835,6 +869,11 @@ def test_generate_qa_check_issues_report(
"format": Format.XLSX,
"dateFrom": datetime(2023, 2, 1),
"dateTo": datetime(2023, 2, 28),
+ "mode": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
+ "userIds": None,
},
),
]
@@ -875,6 +914,13 @@ def test_generate_saving_activity_report(
"languageId": "uk",
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
+ "matchScoreCategories": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
+ "labelIds": None,
+ "labelIncludeType": None,
+ "skipArchiving": None,
},
),
(
@@ -892,6 +938,13 @@ def test_generate_saving_activity_report(
"languageId": "uk",
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
+ "matchScoreCategories": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
+ "labelIds": None,
+ "labelIncludeType": None,
+ "skipArchiving": None,
},
)
],
@@ -931,7 +984,9 @@ def test_generate_pre_translate_accuracy_general_report(
"unit": Unit.WORDS,
"format": Format.XLSX,
"postEditingCategories": ["0-20", "20-50"],
- "taskId": 1
+ "taskId": 1,
+ "matchScoreCategories": None,
+ "skipArchiving": None,
}
),
(
@@ -944,7 +999,9 @@ def test_generate_pre_translate_accuracy_general_report(
"unit": Unit.WORDS,
"format": Format.XLSX,
"postEditingCategories": None,
- "taskId": 1
+ "taskId": 1,
+ "matchScoreCategories": None,
+ "skipArchiving": None,
}
)
]
@@ -1031,7 +1088,9 @@ def test_generate_pre_translate_accuracy_by_task_report(
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
"labelIds": [1],
- "labelIncludeType": ReportLabelIncludeType.STRINGS_WITH_LABEL
+ "labelIncludeType": ReportLabelIncludeType.STRINGS_WITH_LABEL,
+ "skipArchiving": None,
+ "workflowStepId": None,
}
)
]
@@ -1102,7 +1161,9 @@ def test_generate_costs_estimation_post_editing_general_report(
},
"calculateInternalMatches": True,
"includePreTranslatedStrings": True,
- "taskId": 1
+ "taskId": 1,
+ "taskIds": None,
+ "skipArchiving": None,
}
)
]
@@ -1170,7 +1231,13 @@ def test_generate_costs_estimation_post_editing_by_task_report(
"userIds": [1],
"fileIds": [1],
"directoryIds": [1],
- "branchIds": [1]
+ "branchIds": [1],
+ "useCategoryBasedProofreadRates": None,
+ "useTmEditDistance": None,
+ "labelIds": None,
+ "labelIncludeType": None,
+ "skipArchiving": None,
+ "workflowStepId": None,
}
)
]
@@ -1224,7 +1291,11 @@ def test_generate_translation_costs_post_editing_general_report(
"netRateSchemes": NetRateSchemes(tmMatch=[
Match(matchType=MatchType.OPTION_99_82, price=70)
]),
- "taskId": 1
+ "taskId": 1,
+ "taskIds": None,
+ "useCategoryBasedProofreadRates": None,
+ "useTmEditDistance": None,
+ "skipArchiving": None,
}
)
]
@@ -1341,6 +1412,11 @@ def test_get_report_archive_export_path(
"scopeId": None,
"limit": 25,
"offset": 0,
+ "userId": None,
+ "taskId": None,
+ "name": None,
+ "dateFrom": None,
+ "dateTo": None,
},
),
(
@@ -1355,6 +1431,11 @@ def test_get_report_archive_export_path(
"scopeId": 1,
"limit": 10,
"offset": 2,
+ "userId": None,
+ "taskId": None,
+ "name": None,
+ "dateFrom": None,
+ "dateTo": None,
},
),
),
@@ -1834,6 +1915,7 @@ def test_generate_fuzzy_translation_cost_report(
"format": Format.XLSX,
"dateFrom": None,
"dateTo": None,
+ "userIds": None,
},
),
(
@@ -1850,6 +1932,7 @@ def test_generate_fuzzy_translation_cost_report(
"format": Format.JSON,
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
+ "userIds": None,
},
),
),
@@ -1878,6 +1961,13 @@ def test_generate_top_members_report(
"userId": None,
"dateFrom": None,
"dateTo": None,
+ "columns": None,
+ "tmIds": None,
+ "mtIds": None,
+ "aiPromptIds": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
},
),
(
@@ -1896,6 +1986,13 @@ def test_generate_top_members_report(
"userId": 1,
"dateFrom": datetime(year=1988, month=1, day=4),
"dateTo": datetime(year=2015, month=10, day=13),
+ "columns": None,
+ "tmIds": None,
+ "mtIds": None,
+ "aiPromptIds": None,
+ "fileIds": None,
+ "directoryIds": None,
+ "branchIds": None,
},
),
),
@@ -1984,7 +2081,10 @@ def test_generate_contribution_raw_data_report(
"groupBy": GroupBy.LANGUAGE,
"dateFrom": None,
"dateTo": None,
- "userIds": [10, 11]
+ "userIds": [10, 11],
+ "useCategoryBasedProofreadRates": None,
+ "useTmEditDistance": None,
+ "skipArchiving": None,
}
)
]
@@ -2073,7 +2173,10 @@ def test_generate_group_translation_costs_post_editing_general_report(
"groupBy": GroupBy.LANGUAGE,
"dateFrom": None,
"dateTo": None,
- "userIds": [10, 11]
+ "userIds": [10, 11],
+ "useCategoryBasedProofreadRates": None,
+ "useTmEditDistance": None,
+ "skipArchiving": None,
}
)
]
@@ -2109,10 +2212,13 @@ def test_generate_organization_translation_costs_post_editing_general_report(
"dateFrom": None,
"dateTo": None,
"groupBy": None,
- "typeTask": None,
+ "typeTasks": None,
"languageId": None,
"creatorId": None,
"assigneeId": None,
+ "wordsCountFrom": None,
+ "wordsCountTo": None,
+ "statuses": None,
},
),
(
@@ -2135,10 +2241,13 @@ def test_generate_organization_translation_costs_post_editing_general_report(
"dateFrom": datetime(2023, 1, 1),
"dateTo": datetime(2023, 12, 31),
"groupBy": GroupBy.USER,
- "typeTask": 2,
+ "typeTasks": 2,
"languageId": "uk",
"creatorId": 10,
"assigneeId": 20,
+ "wordsCountFrom": None,
+ "wordsCountTo": None,
+ "statuses": None,
},
),
]
@@ -2214,6 +2323,7 @@ def test_generate_group_qa_check_issues_report(
"format": None,
"dateFrom": None,
"dateTo": None,
+ "userIds": None,
},
),
(
@@ -2230,6 +2340,7 @@ def test_generate_group_qa_check_issues_report(
"format": Format.XLSX,
"dateFrom": datetime(2024, 1, 1),
"dateTo": datetime(2024, 1, 31),
+ "userIds": None,
},
),
]
@@ -2371,7 +2482,6 @@ def test_list_report_settings_template(
"name": "test_name",
"currency": Currency.UAH,
"unit": Unit.WORDS,
- "mode": "simple",
"config": {
"regularRates": [
{
@@ -2393,6 +2503,7 @@ def test_list_report_settings_template(
]
},
"isPublic": None,
+ "isGlobal": None,
},
),
(
@@ -2426,7 +2537,6 @@ def test_list_report_settings_template(
"name": "test_name",
"currency": Currency.UAH,
"unit": Unit.WORDS,
- "mode": "simple",
"config": {
"regularRates": [
{
@@ -2448,6 +2558,7 @@ def test_list_report_settings_template(
]
},
"isPublic": False,
+ "isGlobal": None,
},
),
),
@@ -2637,7 +2748,6 @@ def test_list_user_report_settings_template(
"name": "test_name",
"currency": Currency.UAH,
"unit": Unit.WORDS,
- "mode": "simple",
"config": {
"regularRates": [
{
@@ -2740,3 +2850,1166 @@ def test_delete_user_report_settings_template(self, m_request, base_absolut_url)
reportSettingsTemplateId=1
),
)
+
+
+class TestReportsResourceNewReports:
+ resource_class = ReportsResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @pytest.mark.parametrize(
+ "name_method",
+ [
+ "list_user_report_settings_template",
+ "add_user_report_settings_template",
+ "get_user_report_settings_template",
+ "edit_user_report_settings_template",
+ "delete_user_report_settings_template",
+ ],
+ )
+ def test_user_report_settings_templates_available(self, name_method):
+ assert hasattr(ReportsResource, name_method)
+ assert hasattr(EnterpriseReportsResource, name_method)
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_report_archives_filters(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ date_from = datetime(2024, 1, 1)
+ date_to = datetime(2024, 2, 1)
+ assert resource.list_report_archives(
+ userId=1,
+ taskId=2,
+ name="archive",
+ dateFrom=date_from,
+ dateTo=date_to,
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/1/reports/archives",
+ params={
+ "scopeType": None,
+ "scopeId": None,
+ "taskId": 2,
+ "name": "archive",
+ "dateFrom": date_from,
+ "dateTo": date_to,
+ "limit": 25,
+ "offset": 0,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_contribution_raw_data_by_task_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.generate_contribution_raw_data_by_task_report(
+ mode=ContributionMode.TRANSLATIONS,
+ task_id=5,
+ project_id=1,
+ unit=Unit.WORDS,
+ columns=["userId", "languageId"],
+ tm_ids=[1],
+ mt_ids=[2],
+ ai_prompt_ids=[3],
+ date_from=datetime(2024, 1, 1),
+ date_to=datetime(2024, 2, 1),
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/reports",
+ request_data={
+ "name": "contribution-raw-data",
+ "schema": {
+ "mode": ContributionMode.TRANSLATIONS,
+ "unit": Unit.WORDS,
+ "taskId": 5,
+ "columns": ["userId", "languageId"],
+ "tmIds": [1],
+ "mtIds": [2],
+ "aiPromptIds": [3],
+ "dateFrom": datetime(2024, 1, 1),
+ "dateTo": datetime(2024, 2, 1),
+ },
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_translator_accuracy_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.generate_translator_accuracy_report(
+ project_id=1,
+ unit=Unit.WORDS,
+ format=Format.JSON,
+ match_score_categories=["100-90"],
+ language_id="uk",
+ user_ids=[1],
+ date_from=datetime(2024, 1, 1),
+ date_to=datetime(2024, 2, 1),
+ file_ids=[2],
+ directory_ids=[3],
+ branch_ids=[4],
+ label_ids=[5],
+ label_include_type=ReportLabelIncludeType.STRINGS_WITH_LABEL,
+ skip_archiving=True,
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/reports",
+ request_data={
+ "name": "translator-accuracy",
+ "schema": {
+ "unit": Unit.WORDS,
+ "format": Format.JSON,
+ "matchScoreCategories": ["100-90"],
+ "languageId": "uk",
+ "userIds": [1],
+ "dateFrom": datetime(2024, 1, 1),
+ "dateTo": datetime(2024, 2, 1),
+ "fileIds": [2],
+ "directoryIds": [3],
+ "branchIds": [4],
+ "labelIds": [5],
+ "labelIncludeType": ReportLabelIncludeType.STRINGS_WITH_LABEL,
+ "skipArchiving": True,
+ },
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_time_spent_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.generate_time_spent_report(
+ project_id=1,
+ format=Format.XLSX,
+ group_by=GroupBy.TASK,
+ base_rates={"hourly": 10.0},
+ individual_rates=[{"languageIds": ["uk"], "userIds": [1], "hourly": 20.0}],
+ language_id="uk",
+ user_ids=[1],
+ type_tasks=TaskType.PROOFREAD,
+ date_from=datetime(2024, 1, 1),
+ date_to=datetime(2024, 2, 1),
+ task_ids=[7],
+ skip_archiving=False,
+ workflow_step_id=9,
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/reports",
+ request_data={
+ "name": "time-spent",
+ "schema": {
+ "format": Format.XLSX,
+ "groupBy": GroupBy.TASK,
+ "baseRates": {"hourly": 10.0},
+ "individualRates": [{"languageIds": ["uk"], "userIds": [1], "hourly": 20.0}],
+ "languageId": "uk",
+ "userIds": [1],
+ "typeTasks": TaskType.PROOFREAD,
+ "dateFrom": datetime(2024, 1, 1),
+ "dateTo": datetime(2024, 2, 1),
+ "taskIds": [7],
+ "workflowStepId": 9,
+ "skipArchiving": False,
+ },
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_translation_activity_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.generate_translation_activity_report(
+ project_id=1,
+ unit=Unit.WORDS,
+ language_id="uk",
+ format=Format.JSON,
+ date_from=datetime(2024, 1, 1),
+ date_to=datetime(2024, 2, 1),
+ user_ids=[1, 2],
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/reports",
+ request_data={
+ "name": "translation-activity",
+ "schema": {
+ "unit": Unit.WORDS,
+ "languageId": "uk",
+ "format": Format.JSON,
+ "dateFrom": datetime(2024, 1, 1),
+ "dateTo": datetime(2024, 2, 1),
+ "userIds": [1, 2],
+ },
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_report_settings_template_hourly(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ config = {
+ "baseRates": {"hourly": 10.0},
+ "individualRates": [{"languageIds": ["uk"], "userIds": [1], "hourly": 20.0}],
+ }
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_report_settings_template(
+ name="hourly",
+ currency=Currency.PLN,
+ unit=Unit.HOURS,
+ config=config,
+ projectId=1,
+ isGlobal=True,
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/reports/settings-templates",
+ request_data={
+ "name": "hourly",
+ "currency": Currency.PLN,
+ "unit": Unit.HOURS,
+ "config": config,
+ "isPublic": None,
+ "isGlobal": True,
+ },
+ )
+
+
+class TestEnterpriseReportsResourceNewReports:
+ resource_class = EnterpriseReportsResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_report_archives_filters(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_report_archives(
+ scopeType=ScopeType.GROUP,
+ scopeId=1,
+ userId=2,
+ taskId=3,
+ name="archive",
+ dateFrom="2024-01-01",
+ dateTo="2024-02-01",
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="reports/archives",
+ params={
+ "scopeType": ScopeType.GROUP,
+ "scopeId": 1,
+ "userId": 2,
+ "taskId": 3,
+ "name": "archive",
+ "dateFrom": "2024-01-01",
+ "dateTo": "2024-02-01",
+ "limit": 25,
+ "offset": 0,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_task_usage_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.generate_task_usage_report(
+ project_id=1,
+ format=Format.CSV,
+ type=TaskUsageReportType.COST,
+ date_from=datetime(2024, 1, 1),
+ date_to=datetime(2024, 2, 1),
+ group_by=GroupBy.TYPE,
+ type_tasks=TaskType.TRANSLATE,
+ language_id="uk",
+ creator_id=1,
+ assignee_id=2,
+ words_count_from=10,
+ words_count_to=20,
+ statuses=[TaskUsageStatus.DONE],
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/reports",
+ request_data={
+ "name": "task-usage",
+ "schema": {
+ "format": Format.CSV,
+ "type": TaskUsageReportType.COST,
+ "dateFrom": datetime(2024, 1, 1),
+ "dateTo": datetime(2024, 2, 1),
+ "groupBy": GroupBy.TYPE,
+ "typeTasks": TaskType.TRANSLATE,
+ "languageId": "uk",
+ "creatorId": 1,
+ "assigneeId": 2,
+ "wordsCountFrom": 10,
+ "wordsCountTo": 20,
+ "statuses": [TaskUsageStatus.DONE],
+ },
+ },
+ )
+
+ @pytest.mark.parametrize(
+ "incoming_data, path",
+ (
+ ({"group_id": 1}, "groups/1/reports"),
+ ({"group_id": 1, "report_id": "hash"}, "groups/1/reports/hash"),
+ ),
+ )
+ def test_get_group_reports_path(self, incoming_data, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_group_reports_path(**incoming_data) == path
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_group_report_generation_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.check_group_report_generation_status(group_id=1, report_id="hash") == "response"
+ m_request.assert_called_once_with(method="get", path="groups/1/reports/hash")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_download_group_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.download_group_report(group_id=1, report_id="hash") == "response"
+ m_request.assert_called_once_with(method="get", path="groups/1/reports/hash/download")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_organization_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ request_data = {"name": "group-top-members", "schema": {}}
+ assert resource.generate_organization_report(request_data=request_data) == "response"
+ m_request.assert_called_once_with(method="post", path="reports", request_data=request_data)
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_organization_report_generation_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.check_organization_report_generation_status(report_id="hash") == "response"
+ m_request.assert_called_once_with(method="get", path="reports/hash")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_download_organization_report(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.download_organization_report(report_id="hash") == "response"
+ m_request.assert_called_once_with(method="get", path="reports/hash/download")
+
+ @pytest.mark.parametrize(
+ "method_name, in_params, name, schema",
+ (
+ (
+ "generate_group_translation_costs_post_editing_general_report",
+ {
+ "base_rates": "v_baseRates",
+ "individual_rates": "v_individualRates",
+ "net_rate_schemes": "v_netRateSchemes",
+ "project_ids": "v_projectIds",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "group_by": "v_groupBy",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "user_ids": "v_userIds",
+ "use_category_based_proofread_rates": "v_useCategoryBasedProofreadRates",
+ "use_tm_edit_distance": "v_useTmEditDistance",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-translation-costs-pe",
+ {
+ "baseRates": "v_baseRates",
+ "individualRates": "v_individualRates",
+ "netRateSchemes": "v_netRateSchemes",
+ "projectIds": "v_projectIds",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "groupBy": "v_groupBy",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "userIds": "v_userIds",
+ "useCategoryBasedProofreadRates": "v_useCategoryBasedProofreadRates",
+ "useTmEditDistance": "v_useTmEditDistance",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_group_translation_costs_post_editing_by_task_report",
+ {
+ "base_rates": "v_baseRates",
+ "individual_rates": "v_individualRates",
+ "net_rate_schemes": "v_netRateSchemes",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "task_ids": "v_taskIds",
+ "use_category_based_proofread_rates": "v_useCategoryBasedProofreadRates",
+ "use_tm_edit_distance": "v_useTmEditDistance",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-translation-costs-pe",
+ {
+ "baseRates": "v_baseRates",
+ "individualRates": "v_individualRates",
+ "netRateSchemes": "v_netRateSchemes",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "taskIds": "v_taskIds",
+ "useCategoryBasedProofreadRates": "v_useCategoryBasedProofreadRates",
+ "useTmEditDistance": "v_useTmEditDistance",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_group_top_members_report",
+ {
+ "project_ids": "v_projectIds",
+ "unit": "v_unit",
+ "language_id": "v_languageId",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "user_ids": "v_userIds",
+ },
+ "group-top-members",
+ {
+ "projectIds": "v_projectIds",
+ "unit": "v_unit",
+ "languageId": "v_languageId",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "userIds": "v_userIds",
+ },
+ ),
+ (
+ "generate_group_task_usage_report",
+ {
+ "format": "v_format",
+ "type": "v_type",
+ "project_ids": "v_projectIds",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "group_by": "v_groupBy",
+ "type_task": "v_typeTasks",
+ "language_id": "v_languageId",
+ "creator_id": "v_creatorId",
+ "assignee_id": "v_assigneeId",
+ "words_count_from": "v_wordsCountFrom",
+ "words_count_to": "v_wordsCountTo",
+ "statuses": "v_statuses",
+ },
+ "group-task-usage",
+ {
+ "format": "v_format",
+ "type": "v_type",
+ "projectIds": "v_projectIds",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "groupBy": "v_groupBy",
+ "typeTasks": "v_typeTasks",
+ "languageId": "v_languageId",
+ "creatorId": "v_creatorId",
+ "assigneeId": "v_assigneeId",
+ "wordsCountFrom": "v_wordsCountFrom",
+ "wordsCountTo": "v_wordsCountTo",
+ "statuses": "v_statuses",
+ },
+ ),
+ (
+ "generate_group_qa_check_issues_report",
+ {
+ "project_ids": "v_projectIds",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ },
+ "group-qa-check-issues",
+ {
+ "projectIds": "v_projectIds",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ },
+ ),
+ (
+ "generate_group_translation_activity_report",
+ {
+ "unit": "v_unit",
+ "project_ids": "v_projectIds",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "user_ids": "v_userIds",
+ },
+ "group-translation-activity",
+ {
+ "unit": "v_unit",
+ "projectIds": "v_projectIds",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "userIds": "v_userIds",
+ },
+ ),
+ (
+ "generate_group_source_content_updates_report",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "project_ids": "v_projectIds",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ },
+ "group-source-content-updates",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "projectIds": "v_projectIds",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ },
+ ),
+ (
+ "generate_group_time_spent_report",
+ {
+ "format": "v_format",
+ "group_by": "v_groupBy",
+ "base_rates": "v_baseRates",
+ "individual_rates": "v_individualRates",
+ "language_id": "v_languageId",
+ "user_ids": "v_userIds",
+ "type_tasks": "v_typeTasks",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "project_ids": "v_projectIds",
+ "task_ids": "v_taskIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-time-spent",
+ {
+ "format": "v_format",
+ "groupBy": "v_groupBy",
+ "baseRates": "v_baseRates",
+ "individualRates": "v_individualRates",
+ "languageId": "v_languageId",
+ "userIds": "v_userIds",
+ "typeTasks": "v_typeTasks",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "projectIds": "v_projectIds",
+ "taskIds": "v_taskIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_group_pre_translate_accuracy_general_report",
+ {
+ "unit": "v_unit",
+ "language_id": "v_languageId",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "match_score_categories": "v_matchScoreCategories",
+ "project_ids": "v_projectIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-pre-translate-accuracy",
+ {
+ "unit": "v_unit",
+ "languageId": "v_languageId",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "matchScoreCategories": "v_matchScoreCategories",
+ "projectIds": "v_projectIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_group_pre_translate_accuracy_by_task_report",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "match_score_categories": "v_matchScoreCategories",
+ "task_ids": "v_taskIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-pre-translate-accuracy",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "matchScoreCategories": "v_matchScoreCategories",
+ "taskIds": "v_taskIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_group_translator_accuracy_report",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "language_id": "v_languageId",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "match_score_categories": "v_matchScoreCategories",
+ "user_ids": "v_userIds",
+ "project_ids": "v_projectIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-translator-accuracy",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "languageId": "v_languageId",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "matchScoreCategories": "v_matchScoreCategories",
+ "userIds": "v_userIds",
+ "projectIds": "v_projectIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_group_saving_activity_report",
+ {
+ "unit": "v_unit",
+ "project_ids": "v_projectIds",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "language_id": "v_languageId",
+ "mode": "v_mode",
+ },
+ "group-saving-activity",
+ {
+ "unit": "v_unit",
+ "projectIds": "v_projectIds",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "languageId": "v_languageId",
+ "mode": "v_mode",
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_group_reports(
+ self, m_request, method_name, in_params, name, schema, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert getattr(resource, method_name)(group_id=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="groups/1/reports",
+ request_data={"name": name, "schema": schema},
+ )
+
+ @pytest.mark.parametrize(
+ "method_name, in_params, name, schema",
+ (
+ (
+ "generate_organization_translation_costs_post_editing_general_report",
+ {
+ "base_rates": "v_baseRates",
+ "individual_rates": "v_individualRates",
+ "net_rate_schemes": "v_netRateSchemes",
+ "project_ids": "v_projectIds",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "group_by": "v_groupBy",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "user_ids": "v_userIds",
+ "use_category_based_proofread_rates": "v_useCategoryBasedProofreadRates",
+ "use_tm_edit_distance": "v_useTmEditDistance",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-translation-costs-pe",
+ {
+ "baseRates": "v_baseRates",
+ "individualRates": "v_individualRates",
+ "netRateSchemes": "v_netRateSchemes",
+ "projectIds": "v_projectIds",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "groupBy": "v_groupBy",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "userIds": "v_userIds",
+ "useCategoryBasedProofreadRates": "v_useCategoryBasedProofreadRates",
+ "useTmEditDistance": "v_useTmEditDistance",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_organization_translation_costs_post_editing_by_task_report",
+ {
+ "base_rates": "v_baseRates",
+ "individual_rates": "v_individualRates",
+ "net_rate_schemes": "v_netRateSchemes",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "task_ids": "v_taskIds",
+ "use_category_based_proofread_rates": "v_useCategoryBasedProofreadRates",
+ "use_tm_edit_distance": "v_useTmEditDistance",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-translation-costs-pe",
+ {
+ "baseRates": "v_baseRates",
+ "individualRates": "v_individualRates",
+ "netRateSchemes": "v_netRateSchemes",
+ "unit": "v_unit",
+ "currency": "v_currency",
+ "format": "v_format",
+ "taskIds": "v_taskIds",
+ "useCategoryBasedProofreadRates": "v_useCategoryBasedProofreadRates",
+ "useTmEditDistance": "v_useTmEditDistance",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_organization_top_members_report",
+ {
+ "project_ids": "v_projectIds",
+ "unit": "v_unit",
+ "language_id": "v_languageId",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "user_ids": "v_userIds",
+ },
+ "group-top-members",
+ {
+ "projectIds": "v_projectIds",
+ "unit": "v_unit",
+ "languageId": "v_languageId",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "userIds": "v_userIds",
+ },
+ ),
+ (
+ "generate_organization_task_usage_report",
+ {
+ "format": "v_format",
+ "type": "v_type",
+ "project_ids": "v_projectIds",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "group_by": "v_groupBy",
+ "type_tasks": "v_typeTasks",
+ "language_id": "v_languageId",
+ "creator_id": "v_creatorId",
+ "assignee_id": "v_assigneeId",
+ "words_count_from": "v_wordsCountFrom",
+ "words_count_to": "v_wordsCountTo",
+ "statuses": "v_statuses",
+ },
+ "group-task-usage",
+ {
+ "format": "v_format",
+ "type": "v_type",
+ "projectIds": "v_projectIds",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "groupBy": "v_groupBy",
+ "typeTasks": "v_typeTasks",
+ "languageId": "v_languageId",
+ "creatorId": "v_creatorId",
+ "assigneeId": "v_assigneeId",
+ "wordsCountFrom": "v_wordsCountFrom",
+ "wordsCountTo": "v_wordsCountTo",
+ "statuses": "v_statuses",
+ },
+ ),
+ (
+ "generate_organization_qa_check_issues_report",
+ {
+ "project_ids": "v_projectIds",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ },
+ "group-qa-check-issues",
+ {
+ "projectIds": "v_projectIds",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ },
+ ),
+ (
+ "generate_organization_translation_activity_report",
+ {
+ "unit": "v_unit",
+ "project_ids": "v_projectIds",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "user_ids": "v_userIds",
+ },
+ "group-translation-activity",
+ {
+ "unit": "v_unit",
+ "projectIds": "v_projectIds",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "userIds": "v_userIds",
+ },
+ ),
+ (
+ "generate_organization_source_content_updates_report",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "project_ids": "v_projectIds",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ },
+ "group-source-content-updates",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "projectIds": "v_projectIds",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ },
+ ),
+ (
+ "generate_organization_time_spent_report",
+ {
+ "format": "v_format",
+ "group_by": "v_groupBy",
+ "base_rates": "v_baseRates",
+ "individual_rates": "v_individualRates",
+ "language_id": "v_languageId",
+ "user_ids": "v_userIds",
+ "type_tasks": "v_typeTasks",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "project_ids": "v_projectIds",
+ "task_ids": "v_taskIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-time-spent",
+ {
+ "format": "v_format",
+ "groupBy": "v_groupBy",
+ "baseRates": "v_baseRates",
+ "individualRates": "v_individualRates",
+ "languageId": "v_languageId",
+ "userIds": "v_userIds",
+ "typeTasks": "v_typeTasks",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "projectIds": "v_projectIds",
+ "taskIds": "v_taskIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_organization_pre_translate_accuracy_general_report",
+ {
+ "unit": "v_unit",
+ "language_id": "v_languageId",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "match_score_categories": "v_matchScoreCategories",
+ "project_ids": "v_projectIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-pre-translate-accuracy",
+ {
+ "unit": "v_unit",
+ "languageId": "v_languageId",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "matchScoreCategories": "v_matchScoreCategories",
+ "projectIds": "v_projectIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_organization_pre_translate_accuracy_by_task_report",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "match_score_categories": "v_matchScoreCategories",
+ "task_ids": "v_taskIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-pre-translate-accuracy",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "matchScoreCategories": "v_matchScoreCategories",
+ "taskIds": "v_taskIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_organization_translator_accuracy_report",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "language_id": "v_languageId",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "match_score_categories": "v_matchScoreCategories",
+ "user_ids": "v_userIds",
+ "project_ids": "v_projectIds",
+ "skip_archiving": "v_skipArchiving",
+ },
+ "group-translator-accuracy",
+ {
+ "unit": "v_unit",
+ "format": "v_format",
+ "languageId": "v_languageId",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "matchScoreCategories": "v_matchScoreCategories",
+ "userIds": "v_userIds",
+ "projectIds": "v_projectIds",
+ "skipArchiving": "v_skipArchiving",
+ },
+ ),
+ (
+ "generate_organization_saving_activity_report",
+ {
+ "unit": "v_unit",
+ "project_ids": "v_projectIds",
+ "format": "v_format",
+ "date_from": "v_dateFrom",
+ "date_to": "v_dateTo",
+ "language_id": "v_languageId",
+ "mode": "v_mode",
+ },
+ "group-saving-activity",
+ {
+ "unit": "v_unit",
+ "projectIds": "v_projectIds",
+ "format": "v_format",
+ "dateFrom": "v_dateFrom",
+ "dateTo": "v_dateTo",
+ "languageId": "v_languageId",
+ "mode": "v_mode",
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_generate_organization_reports(
+ self, m_request, method_name, in_params, name, schema, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert getattr(resource, method_name)(**in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="reports",
+ request_data={"name": name, "schema": schema},
+ )
+
+ @pytest.mark.parametrize(
+ "incoming_data, path",
+ (
+ ({}, "reports/settings-templates"),
+ ({"reportSettingsTemplateId": 1}, "reports/settings-templates/1"),
+ ),
+ )
+ def test_get_organization_report_settings_templates_path(
+ self, incoming_data, path, base_absolut_url
+ ):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_organization_report_settings_templates_path(**incoming_data) == path
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_params",
+ (
+ ({}, {"projectId": None, "groupId": None, "offset": 0, "limit": 25}),
+ (
+ {"project_id": 1, "group_id": 2, "offset": 5, "limit": 10},
+ {"projectId": 1, "groupId": 2, "offset": 5, "limit": 10},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_organization_report_settings_templates(
+ 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_organization_report_settings_templates(**incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="reports/settings-templates",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_organization_report_settings_template(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ config = {
+ "baseRates": {"fullTranslation": 0.1, "proofread": 0.05},
+ "individualRates": [],
+ "netRateSchemes": {"tmMatch": [], "mtMatch": [], "suggestionMatch": []},
+ }
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_organization_report_settings_template(
+ name="template",
+ currency=Currency.USD,
+ unit=Unit.WORDS,
+ config=config,
+ group_id=2,
+ is_public=True,
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="reports/settings-templates",
+ request_data={
+ "projectId": None,
+ "groupId": 2,
+ "name": "template",
+ "currency": Currency.USD,
+ "unit": Unit.WORDS,
+ "config": config,
+ "isPublic": True,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_organization_report_settings_template(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_organization_report_settings_template(1) == "response"
+ m_request.assert_called_once_with(method="get", path="reports/settings-templates/1")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_organization_report_settings_template(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": ReportSettingsTemplatesPatchPath.IS_PUBLIC,
+ "value": True,
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_organization_report_settings_template(1, data) == "response"
+ m_request.assert_called_once_with(
+ method="patch", path="reports/settings-templates/1", request_data=data
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_organization_report_settings_template(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_organization_report_settings_template(1) == "response"
+ m_request.assert_called_once_with(method="delete", path="reports/settings-templates/1")
+
+ @pytest.mark.parametrize(
+ "method_name, kwargs, expected",
+ (
+ (
+ "list_report_settings_template",
+ {"projectId": 1},
+ {
+ "method": "get",
+ "path": "projects/1/reports/settings-templates",
+ "params": {"offset": 0, "limit": 25},
+ },
+ ),
+ (
+ "add_report_settings_template",
+ {
+ "name": "n",
+ "currency": Currency.USD,
+ "unit": Unit.WORDS,
+ "config": {},
+ "projectId": 1,
+ },
+ {
+ "method": "post",
+ "path": "projects/1/reports/settings-templates",
+ "request_data": {
+ "name": "n",
+ "currency": Currency.USD,
+ "unit": Unit.WORDS,
+ "config": {},
+ "isPublic": None,
+ "isGlobal": None,
+ },
+ },
+ ),
+ (
+ "get_report_settings_template",
+ {"reportSettingsTemplateId": 2, "projectId": 1},
+ {"method": "get", "path": "projects/1/reports/settings-templates/2"},
+ ),
+ (
+ "edit_report_settings_template",
+ {"reportSettingsTemplateId": 2, "data": [], "projectId": 1},
+ {
+ "method": "patch",
+ "path": "projects/1/reports/settings-templates/2",
+ "request_data": [],
+ },
+ ),
+ (
+ "delete_report_settings_template",
+ {"reportSettingsTemplateId": 2, "projectId": 1},
+ {"method": "delete", "path": "projects/1/reports/settings-templates/2"},
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_deprecated_project_report_settings_templates(
+ self, m_request, method_name, kwargs, expected, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ assert getattr(resource, method_name)(**kwargs) == "response"
+ assert any(issubclass(w.category, DeprecationWarning) for w in caught)
+ m_request.assert_called_once_with(**expected)
diff --git a/crowdin_api/api_resources/reports/types.py b/crowdin_api/api_resources/reports/types.py
index 9b9e1707..e0c100c4 100644
--- a/crowdin_api/api_resources/reports/types.py
+++ b/crowdin_api/api_resources/reports/types.py
@@ -62,3 +62,50 @@ class Match(TypedDict):
class BaseRates(TypedDict):
fullTranslation: float
proofread: float
+
+
+class HourlyBaseRates(TypedDict):
+ hourly: float
+
+
+class HourlyIndividualRate(TypedDict):
+ languageIds: Iterable[str]
+ userIds: Iterable[int]
+ hourly: float
+
+
+class PostEditingIndividualRate(TypedDict):
+ languageIds: Iterable[str]
+ userIds: Iterable[int]
+ fullTranslation: float
+ proofread: float
+
+
+class PostEditingNetRateSchemes(TypedDict, total=False):
+ tmMatch: Iterable[Match]
+ mtMatch: Iterable[Match]
+ aiMatch: Iterable[Match]
+ suggestionMatch: Iterable[Match]
+
+
+class PostEditingConfig(TypedDict, total=False):
+ """
+ Report settings template config for post-editing templates.
+ """
+
+ baseRates: BaseRates
+ individualRates: Iterable[PostEditingIndividualRate]
+ netRateSchemes: PostEditingNetRateSchemes
+ calculateInternalMatches: bool
+ includePreTranslatedStrings: bool
+ useCategoryBasedProofreadRates: bool
+ useTmEditDistance: bool
+
+
+class HourlyConfig(TypedDict):
+ """
+ Report settings template config for hourly templates (unit `hours`).
+ """
+
+ baseRates: HourlyBaseRates
+ individualRates: Iterable[HourlyIndividualRate]
diff --git a/crowdin_api/api_resources/screenshots/enums.py b/crowdin_api/api_resources/screenshots/enums.py
index 27a3a227..7b03c317 100644
--- a/crowdin_api/api_resources/screenshots/enums.py
+++ b/crowdin_api/api_resources/screenshots/enums.py
@@ -3,6 +3,7 @@
class ScreenshotPatchPath(Enum):
NAME = "/name"
+ LABEL_IDS = "/labelIds"
class TagPatchPath(Enum):
diff --git a/crowdin_api/api_resources/screenshots/resource.py b/crowdin_api/api_resources/screenshots/resource.py
index 06853784..d89564e5 100644
--- a/crowdin_api/api_resources/screenshots/resource.py
+++ b/crowdin_api/api_resources/screenshots/resource.py
@@ -8,6 +8,7 @@
TagPatchRequest,
)
from crowdin_api.sorting import Sorting
+from crowdin_api.utils import convert_to_query_list
class ScreenshotsResource(BaseResource):
@@ -20,7 +21,7 @@ class ScreenshotsResource(BaseResource):
Use API to manage screenshots and their tags.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Screenshots
+ https://support.crowdin.com/developer/api/v2/#tag/Screenshots
"""
# Screenshots
@@ -41,12 +42,16 @@ def list_screenshots(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ search: Optional[str] = None,
):
"""
List Screenshots.
+ :param search: Search screenshots by name, tagged strings or file names that include
+ screenshots.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.getMany
"""
projectId = projectId or self.get_project_id()
@@ -55,7 +60,13 @@ def list_screenshots(
warnings.warn("`stringId` is deprecated, use `stringIds` instead", category=DeprecationWarning)
stringIds = [stringId]
- params = {"orderBy": orderBy, "stringIds": stringIds, "labelIds": labelIds, "excludeLabelIds": excludeLabelIds}
+ params = {
+ "search": search,
+ "orderBy": orderBy,
+ "stringIds": convert_to_query_list(stringIds),
+ "labelIds": convert_to_query_list(labelIds),
+ "excludeLabelIds": convert_to_query_list(excludeLabelIds),
+ }
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
return self._get_entire_data(
@@ -78,8 +89,11 @@ def add_screenshot(
"""
Add Screenshot.
+ :param fileId: File Identifier (file-based projects only).
+ :param directoryId: Directory Identifier (file-based projects only).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.post
"""
projectId = projectId or self.get_project_id()
@@ -103,7 +117,7 @@ def get_screenshot(self, screenshotId: int, projectId: Optional[int] = None):
Get Screenshot.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.get
"""
projectId = projectId or self.get_project_id()
@@ -119,12 +133,16 @@ def update_screenshot(
storageId: int,
name: str,
projectId: Optional[int] = None,
+ usePreviousTags: Optional[bool] = None,
):
"""
Update Screenshot.
+ :param usePreviousTags: Defines whether to keep the tags of the previous screenshot
+ version (the API default is `True`).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.put
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.put
"""
projectId = projectId or self.get_project_id()
@@ -134,6 +152,7 @@ def update_screenshot(
request_data={
"storageId": storageId,
"name": name,
+ "usePreviousTags": usePreviousTags,
},
path=self.get_screenshots_path(projectId=projectId, screenshotId=screenshotId),
)
@@ -143,7 +162,7 @@ def delete_screenshot(self, screenshotId: int, projectId: Optional[int] = None):
Delete Screenshot.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.delete
"""
projectId = projectId or self.get_project_id()
@@ -163,7 +182,7 @@ def edit_screenshot(
Edit Screenshot.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.patch
"""
projectId = projectId or self.get_project_id()
@@ -193,7 +212,7 @@ def list_tags(
List Tags.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.getMany
"""
projectId = projectId or self.get_project_id()
@@ -214,7 +233,7 @@ def replace_tags(
Replace Tags.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.putMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.putMany
"""
projectId = projectId or self.get_project_id()
@@ -226,13 +245,24 @@ def replace_tags(
)
def auto_tag(
- self, screenshotId: int, autoTag: bool, projectId: Optional[int] = None
+ self,
+ screenshotId: int,
+ autoTag: bool,
+ projectId: Optional[int] = None,
+ fileId: Optional[int] = None,
+ branchId: Optional[int] = None,
+ directoryId: Optional[int] = None,
):
"""
Auto Tag.
+ :param fileId: Limit auto tagging to strings of this file (file-based projects only).
+ :param branchId: Limit auto tagging to strings of this branch.
+ :param directoryId: Limit auto tagging to strings of this directory
+ (file-based projects only).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.putMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.putMany
"""
projectId = projectId or self.get_project_id()
@@ -240,7 +270,12 @@ def auto_tag(
return self.requester.request(
method="put",
path=self.get_tags_path(projectId=projectId, screenshotId=screenshotId),
- request_data={"autoTag": autoTag},
+ request_data={
+ "autoTag": autoTag,
+ "fileId": fileId,
+ "branchId": branchId,
+ "directoryId": directoryId,
+ },
)
def add_tag(
@@ -253,7 +288,7 @@ def add_tag(
Add Tag.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.post
"""
projectId = projectId or self.get_project_id()
@@ -269,7 +304,7 @@ def clear_tags(self, screenshotId: int, projectId: Optional[int] = None):
Clear Tags.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.deleteMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.deleteMany
"""
projectId = projectId or self.get_project_id()
@@ -284,7 +319,7 @@ def get_tag(self, screenshotId: int, tagId: int, projectId: Optional[int] = None
Get Tag.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.get
"""
projectId = projectId or self.get_project_id()
@@ -301,7 +336,7 @@ def delete_tag(
Delete Tag.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.delete
"""
projectId = projectId or self.get_project_id()
@@ -322,7 +357,7 @@ def edit_tag(
Edit Tag.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.screenshots.tags.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.screenshots.tags.patch
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/screenshots/tests/test_screenshots_resources.py b/crowdin_api/api_resources/screenshots/tests/test_screenshots_resources.py
index 962aae61..0bfe898a 100644
--- a/crowdin_api/api_resources/screenshots/tests/test_screenshots_resources.py
+++ b/crowdin_api/api_resources/screenshots/tests/test_screenshots_resources.py
@@ -39,6 +39,7 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
(
{},
{
+ "search": None,
"orderBy": None,
"stringIds": None,
"labelIds": None,
@@ -55,16 +56,18 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
"stringIds": [1, 2, 3],
"labelIds": [4, 5, 6],
"excludeLabelIds": [7, 8, 9],
+ "search": "home",
"limit": 10,
"offset": 0,
},
{
+ "search": "home",
"orderBy": Sorting(
[SortingRule(ListScreenshotsOrderBy.ID, SortingOrder.DESC)]
),
- "stringIds": [1, 2, 3],
- "labelIds": [4, 5, 6],
- "excludeLabelIds": [7, 8, 9],
+ "stringIds": "1,2,3",
+ "labelIds": "4,5,6",
+ "excludeLabelIds": "7,8,9",
"limit": 10,
"offset": 0,
},
@@ -87,6 +90,26 @@ def test_list_screenshots(
params=request_params,
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_screenshots_string_id(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+ resource = self.get_resource(base_absolut_url)
+ with pytest.warns(DeprecationWarning):
+ assert resource.list_screenshots(projectId=1, stringId=5) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path=resource.get_screenshots_path(projectId=1),
+ params={
+ "search": None,
+ "orderBy": None,
+ "stringIds": "5",
+ "labelIds": None,
+ "excludeLabelIds": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ )
+
@pytest.mark.parametrize(
"in_params, expected_params",
[
@@ -150,7 +173,24 @@ def test_update_screenshot(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="put",
path=resource.get_screenshots_path(projectId=1, screenshotId=2),
- request_data={"storageId": 3, "name": "test"},
+ request_data={"storageId": 3, "name": "test", "usePreviousTags": None},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_update_screenshot_use_previous_tags(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.update_screenshot(
+ projectId=1, screenshotId=2, storageId=3, name="test", usePreviousTags=False
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="put",
+ path=resource.get_screenshots_path(projectId=1, screenshotId=2),
+ request_data={"storageId": 3, "name": "test", "usePreviousTags": False},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -174,7 +214,12 @@ def test_edit_screenshot(self, m_request, base_absolut_url):
"value": "test",
"op": PatchOperation.REPLACE,
"path": ScreenshotPatchPath.NAME,
- }
+ },
+ {
+ "value": [1, 2],
+ "op": PatchOperation.REPLACE,
+ "path": ScreenshotPatchPath.LABEL_IDS,
+ },
]
resource = self.get_resource(base_absolut_url)
@@ -243,7 +288,29 @@ def test_auto_tag(self, m_request, base_absolut_url):
projectId=1,
screenshotId=2,
),
- request_data={"autoTag": False},
+ request_data={
+ "autoTag": False,
+ "fileId": None,
+ "branchId": None,
+ "directoryId": None,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_auto_tag_with_scope(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.auto_tag(
+ projectId=1, screenshotId=2, autoTag=True, fileId=3, branchId=4, directoryId=5
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="put",
+ path=resource.get_tags_path(projectId=1, screenshotId=2),
+ request_data={"autoTag": True, "fileId": 3, "branchId": 4, "directoryId": 5},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
diff --git a/crowdin_api/api_resources/screenshots/types.py b/crowdin_api/api_resources/screenshots/types.py
index dd08a1f0..3bdf2320 100644
--- a/crowdin_api/api_resources/screenshots/types.py
+++ b/crowdin_api/api_resources/screenshots/types.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, Union
+from typing import Any, Iterable, Optional, Union
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.api_resources.screenshots.enums import ScreenshotPatchPath, TagPatchPath
@@ -6,7 +6,7 @@
class ScreenshotPatchRequest(TypedDict):
- value: str
+ value: Union[str, Iterable[int]]
op: Union[PatchOperation, str]
path: ScreenshotPatchPath
diff --git a/crowdin_api/api_resources/security_logs/resource.py b/crowdin_api/api_resources/security_logs/resource.py
index ffb65bc1..438d43f0 100644
--- a/crowdin_api/api_resources/security_logs/resource.py
+++ b/crowdin_api/api_resources/security_logs/resource.py
@@ -9,19 +9,19 @@ class SecurityLogsResource(BaseResource):
"""
Resource for Security Logs
- Link to documentaion:
- https://developer.crowdin.com/api/v2/#tag/Security-Logs
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Security-Logs
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Security-Logs
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Security-Logs
"""
def get_user_security_logs_path(
self, userId: int, securityLogId: Optional[int] = None
):
if securityLogId is not None:
- return f"/users/{userId}/security-logs/{securityLogId}"
- return f"/users/{userId}/security-logs"
+ return f"users/{userId}/security-logs/{securityLogId}"
+ return f"users/{userId}/security-logs"
def list_user_security_logs(
self,
@@ -37,8 +37,8 @@ def list_user_security_logs(
"""
List User Security Logs
- Link to documentaion:
- https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.getMany
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.security-logs.getMany
"""
params = {
@@ -60,7 +60,7 @@ def get_user_security_log(self, userId: int, securityLogId: int):
Get User Security Log
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.security-logs.get
"""
return self.requester.request(
@@ -72,8 +72,8 @@ def get_user_security_log(self, userId: int, securityLogId: int):
def get_organization_security_logs_path(self, securityLogId: Optional[int] = None):
if securityLogId is not None:
- return f"/security-logs/{securityLogId}"
- return "/security-logs"
+ return f"security-logs/{securityLogId}"
+ return "security-logs"
def list_organization_security_logs(
self,
@@ -90,7 +90,7 @@ def list_organization_security_logs(
List Organization Security Logs
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.security-logs.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.security-logs.getMany
"""
params = {
@@ -112,8 +112,8 @@ def get_organization_security_log(self, securityLogId: int):
"""
Get Organization Security Log
- Link to documentaion:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.security-logs.get
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.security-logs.get
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/security_logs/tests/test_security_logs_resource.py b/crowdin_api/api_resources/security_logs/tests/test_security_logs_resource.py
index 23a2d4cc..698a07a7 100644
--- a/crowdin_api/api_resources/security_logs/tests/test_security_logs_resource.py
+++ b/crowdin_api/api_resources/security_logs/tests/test_security_logs_resource.py
@@ -16,8 +16,8 @@ def get_resource(self, base_absolut_url):
@pytest.mark.parametrize(
"incoming_data, path",
(
- ({"userId": 1}, "/users/1/security-logs"),
- ({"userId": 1, "securityLogId": 2}, "/users/1/security-logs/2"),
+ ({"userId": 1}, "users/1/security-logs"),
+ ({"userId": 1, "securityLogId": 2}, "users/1/security-logs/2"),
),
)
def test_get_user_security_logs_path(self, incoming_data, path, base_absolut_url):
@@ -89,8 +89,8 @@ def test_get_user_security_log(self, m_request, base_absolut_url):
@pytest.mark.parametrize(
"incoming_data, path",
(
- ({}, "/security-logs"),
- ({"securityLogId": 1}, "/security-logs/1"),
+ ({}, "security-logs"),
+ ({"securityLogId": 1}, "security-logs/1"),
),
)
def test_get_organization_security_logs_path(
diff --git a/crowdin_api/api_resources/source_files/enums.py b/crowdin_api/api_resources/source_files/enums.py
index 7dcfa2d9..f0e45a67 100644
--- a/crowdin_api/api_resources/source_files/enums.py
+++ b/crowdin_api/api_resources/source_files/enums.py
@@ -10,8 +10,9 @@ class Priority(Enum):
class BranchPatchPath(Enum):
NAME = "/name"
TITLE = "/title"
- EXPORT_PATTERN = "/exportPattern"
+ EXPORT_PATTERN = "/exportPattern" # file-based projects only
PRIORITY = "/priority"
+ IS_PROTECTED = "/isProtected" # string-based projects only
class DirectoryPatchPath(Enum):
@@ -60,6 +61,36 @@ class FileType(Enum):
SBV = "sbv"
VTT = "vtt"
SRT = "srt"
+ XLIFF_TWO = "xliff_two"
+ MDX_V1 = "mdx_v1"
+ MDX_V2 = "mdx_v2"
+ VSDX = "vsdx"
+ XLSX = "xlsx"
+ PROPERTIES_PLAY = "properties_play"
+ PROPERTIES_XML = "properties_xml"
+ MAXTHON = "maxthon"
+ GO_JSON = "go_json"
+ DITA = "dita"
+ IDML = "idml"
+ MIF = "mif"
+ STRINGSDICT = "stringsdict"
+ PLIST = "plist"
+ VDF = "vdf"
+ STF = "stf"
+ TOML = "toml"
+ CONTENTFUL_RT = "contentful_rt"
+ SVG = "svg"
+ JS = "js"
+ COFFEE = "coffee"
+ TS = "ts"
+ I18NEXT_JSON = "i18next_json"
+ XAML = "xaml"
+ ARB = "arb"
+ ADOC = "adoc"
+ FBT = "fbt"
+ WEBXML = "webxml"
+ NESTJS_I18N = "nestjs_i18n"
+ LOC = "loc"
class EscapeQuotes(Enum):
@@ -116,6 +147,59 @@ class FilePatchPath(Enum):
EXCLUDED_TARGET_LANGUAGES = "/excludedTargetLanguages"
ATTACH_LABEL_IDS = "/attachLabelIds"
DETACH_LABEL_IDS = "/detachLabelIds"
+ IMPORT_OPTIONS_IMPORT_KEY_AS_SOURCE = "/importOptions/importKeyAsSource"
+ IMPORT_OPTIONS_IMPORT_HIDDEN_SHEETS = "/importOptions/importHiddenSheets"
+ IMPORT_OPTIONS_HIDE_ATTRIBUTE_VALUES = "/importOptions/hideAttributeValues"
+ IMPORT_OPTIONS_EXCLUDED_ELEMENTS = "/importOptions/excludedElements"
+ IMPORT_OPTIONS_EXCLUDE_INCLUDE_DIRECTIVES = "/importOptions/excludeIncludeDirectives"
+ IMPORT_OPTIONS_EXCLUDED_FRONT_MATTER_ELEMENTS = "/importOptions/excludedFrontMatterElements"
+ IMPORT_OPTIONS_EXCLUDE_CODE_BLOCKS = "/importOptions/excludeCodeBlocks"
+ IMPORT_OPTIONS_INLINE_TAGS = "/importOptions/inlineTags"
+ IMPORT_OPTIONS_INLINE_HYPERLINK_TEXT = "/importOptions/inlineHyperlinkText"
+ IMPORT_OPTIONS_TRANSLATE_DOC_PROPERTIES = "/importOptions/translateDocProperties"
+ IMPORT_OPTIONS_TRANSLATE_COMMENTS = "/importOptions/translateComments"
+ IMPORT_OPTIONS_IGNORE_WHITESPACE_STYLES = "/importOptions/ignoreWhitespaceStyles"
+ IMPORT_OPTIONS_ADD_TAB_AS_CHARACTER = "/importOptions/addTabAsCharacter"
+ IMPORT_OPTIONS_ADD_LINE_SEPARATOR_AS_CHARACTER = "/importOptions/addLineSeparatorAsCharacter"
+ IMPORT_OPTIONS_LINE_SEPARATOR_REPLACEMENT = "/importOptions/lineSeparatorReplacement"
+ IMPORT_OPTIONS_REPLACE_NO_BREAK_HYPHEN_TAG = "/importOptions/replaceNoBreakHyphenTag"
+ IMPORT_OPTIONS_IGNORE_SOFT_HYPHEN_TAG = "/importOptions/ignoreSoftHyphenTag"
+ IMPORT_OPTIONS_COMPLEX_FIELD_DEFINITIONS_TO_EXTRACT = (
+ "/importOptions/complexFieldDefinitionsToExtract"
+ )
+ IMPORT_OPTIONS_TRANSLATE_WORD_HEADERS_FOOTERS = "/importOptions/translateWordHeadersFooters"
+ IMPORT_OPTIONS_TRANSLATE_WORD_GRAPHIC_NAME = "/importOptions/translateWordGraphicName"
+ IMPORT_OPTIONS_TRANSLATE_WORD_GRAPHIC_DESCRIPTION = (
+ "/importOptions/translateWordGraphicDescription"
+ )
+ IMPORT_OPTIONS_IGNORE_WORD_FONT_COLORS = "/importOptions/ignoreWordFontColors"
+ IMPORT_OPTIONS_WORD_FONT_COLORS_MIN_IGNORANCE_THRESHOLD = (
+ "/importOptions/wordFontColorsMinIgnoranceThreshold"
+ )
+ IMPORT_OPTIONS_WORD_FONT_COLORS_MAX_IGNORANCE_THRESHOLD = (
+ "/importOptions/wordFontColorsMaxIgnoranceThreshold"
+ )
+ IMPORT_OPTIONS_EXCLUDE_WORD_STYLES = "/importOptions/excludeWordStyles"
+ IMPORT_OPTIONS_TRANSLATE_WORD_IN_EXCLUDE_STYLE_MODE = (
+ "/importOptions/translateWordInExcludeStyleMode"
+ )
+ IMPORT_OPTIONS_WORD_HIGHLIGHT_COLORS = "/importOptions/wordHighlightColors"
+ IMPORT_OPTIONS_TRANSLATE_WORD_IN_EXCLUDE_HIGHLIGHT_MODE = (
+ "/importOptions/translateWordInExcludeHighlightMode"
+ )
+ IMPORT_OPTIONS_TRANSLATE_WORD_EXCLUDE_COLORS = "/importOptions/translateWordExcludeColors"
+ IMPORT_OPTIONS_WORD_EXCLUDED_COLORS = "/importOptions/wordExcludedColors"
+ IMPORT_OPTIONS_TRANSLATE_EXCEL_CELLS_COPIED = "/importOptions/translateExcelCellsCopied"
+ IMPORT_OPTIONS_TRANSLATE_EXCEL_SHEET_NAMES = "/importOptions/translateExcelSheetNames"
+ IMPORT_OPTIONS_EXCEL_EXCLUDED_COLORS = "/importOptions/excelExcludedColors"
+ IMPORT_OPTIONS_TRANSLATE_EXCEL_DIAGRAM_DATA = "/importOptions/translateExcelDiagramData"
+ IMPORT_OPTIONS_TRANSLATE_EXCEL_DRAWINGS = "/importOptions/translateExcelDrawings"
+ EXPORT_OPTIONS_EXPORT_QUOTES = "/exportOptions/exportQuotes"
+ EXPORT_OPTIONS_ESCAPE_SPECIAL_CHARACTERS = "/exportOptions/escapeSpecialCharacters"
+ EXPORT_OPTIONS_ALLOW_WORD_STYLE_OPTIMIZATION = "/exportOptions/allowWordStyleOptimization"
+ EXPORT_OPTIONS_TRANSLATE_EXCEL_EXCLUDE_COLORS = "/exportOptions/translateExcelExcludeColors"
+ FIELDS = "/fields" # Enterprise only
+ FIELD = "/fields/{fieldSlug}" # Enterprise only
class ListProjectBranchesOrderBy(Enum):
@@ -147,3 +231,35 @@ class ListFilesOrderBy(Enum):
UPDATED_AT = "updatedAt"
EXPORT_PATTERN = "exportPattern"
PRIORITY = "priority"
+
+
+class EscapeSpecialCharacters(Enum):
+ """
+ Values available:
+ 0 - Do not escape special characters
+ 1 - Escape special characters by a backslash
+ """
+ ZERO = 0
+ ONE = 1
+
+
+class MarkdownMarker(Enum):
+ ASTERISK = "asterisk"
+ UNDERSCORE = "underscore"
+
+
+class UnorderedListBullet(Enum):
+ ASTERISKS = "asterisks"
+ PLUS = "plus"
+ DASH = "dash"
+
+
+class TableColumnWidth(Enum):
+ CONSOLIDATE = "consolidate"
+ EVENLY_DISTRIBUTE_CELLS = "evenly_distribute_cells"
+
+
+class FrontMatterQuotes(Enum):
+ AUTO = "auto"
+ SINGLE = "single"
+ DOUBLE = "double"
diff --git a/crowdin_api/api_resources/source_files/resource.py b/crowdin_api/api_resources/source_files/resource.py
index d566a514..18dcebf3 100644
--- a/crowdin_api/api_resources/source_files/resource.py
+++ b/crowdin_api/api_resources/source_files/resource.py
@@ -1,4 +1,4 @@
-from typing import Any, Iterable, Optional, Union
+from typing import Any, Iterable, Optional
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.source_files.enums import (
@@ -9,18 +9,10 @@
from crowdin_api.api_resources.source_files.types import (
BranchPatchRequest,
DirectoryPatchRequest,
+ FileExportOptions,
+ FileFields,
+ FileImportOptions,
FilePatchRequest,
- GeneralExportOptions,
- JavascriptExportOptions,
- HtmlFileImportOptions,
- HtmlWithFrontMatterFileImportOptions,
- MdxV1FileImportOptions,
- MdxV2FileImportOptions,
- OtherImportOptions,
- PropertyExportOptions,
- SpreadsheetImportOptions,
- XmlImportOptions,
- DocxFileImportOptions,
)
from crowdin_api.sorting import Sorting
@@ -39,7 +31,7 @@ class SourceFilesResource(BaseResource):
Crowdin.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Source-Files
+ https://support.crowdin.com/developer/api/v2/#tag/Source-Files
"""
# Organization Search
@@ -56,8 +48,8 @@ def search_branches(
Search Branches.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.branches.getMany
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.branches.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.branches.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.branches.getMany
"""
params = {
@@ -84,8 +76,8 @@ def search_directories(
Search Directories.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.directories.getMany
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.directories.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.directories.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.directories.getMany
"""
params = {
@@ -112,8 +104,8 @@ def search_files(
Search Files.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.files.getMany
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.files.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.files.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.files.getMany
"""
params = {
@@ -147,7 +139,7 @@ def list_project_branches(
List Branches.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.getMany
"""
projectId = projectId or self.get_project_id()
@@ -165,12 +157,16 @@ def add_branch(
title: Optional[str] = None,
exportPattern: Optional[str] = None,
priority: Optional[Priority] = None,
+ isProtected: Optional[bool] = None,
):
"""
Add Branch.
+ `exportPattern` and `priority` are for file-based projects only,
+ `isProtected` is for string-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.post
"""
projectId = projectId or self.get_project_id()
@@ -183,6 +179,7 @@ def add_branch(
"title": title,
"exportPattern": exportPattern,
"priority": priority,
+ "isProtected": isProtected,
},
)
@@ -191,7 +188,7 @@ def get_branch(self, branchId: int, projectId: Optional[int] = None):
Get Branch.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.get
"""
projectId = projectId or self.get_project_id()
@@ -210,8 +207,11 @@ def delete_branch(
"""
Delete Branch.
+ Pass `prefer="respond-async"` to delete the branch asynchronously (recommended); the API
+ then returns a job to poll via `check_branch_deletion_status`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.delete
"""
projectId = projectId or self.get_project_id()
@@ -233,7 +233,7 @@ def check_branch_deletion_status(
Check Branch Deletion Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.jobs.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.jobs.get
"""
projectId = projectId or self.get_project_id()
@@ -252,8 +252,11 @@ def edit_branch(
"""
Edit Branch.
+ `/exportPattern` is for file-based projects only, `/isProtected` is for string-based
+ projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.patch
"""
projectId = projectId or self.get_project_id()
@@ -287,7 +290,7 @@ def list_directories(
List Directories.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.getMany
"""
projectId = projectId or self.get_project_id()
@@ -312,7 +315,7 @@ def add_directory(
projectId: Optional[int] = None,
branchId: Optional[int] = None,
directoryId: Optional[int] = None,
- title: Optional[int] = None,
+ title: Optional[str] = None,
exportPattern: Optional[str] = None,
priority: Optional[Priority] = None,
):
@@ -320,7 +323,7 @@ def add_directory(
Add Directory.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.post
"""
projectId = projectId or self.get_project_id()
@@ -343,7 +346,7 @@ def get_directory(self, directoryId: int, projectId: Optional[int] = None):
Get Directory.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.get
"""
projectId = projectId or self.get_project_id()
@@ -362,8 +365,11 @@ def delete_directory(
"""
Delete Directory.
+ Pass `prefer="respond-async"` to delete the directory asynchronously (recommended); the API
+ then returns a job to poll via `check_directory_deletion_status`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.delete
"""
projectId = projectId or self.get_project_id()
@@ -385,7 +391,7 @@ def check_directory_deletion_status(
Check Directory Deletion Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.jobs.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.jobs.get
"""
projectId = projectId or self.get_project_id()
@@ -405,7 +411,7 @@ def edit_directory(
Edit Directory.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.patch
"""
projectId = projectId or self.get_project_id()
@@ -439,7 +445,7 @@ def list_files(
List Files.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.getMany
"""
projectId = projectId or self.get_project_id()
@@ -463,32 +469,23 @@ def add_file(
projectId: Optional[int] = None,
branchId: Optional[int] = None,
directoryId: Optional[int] = None,
- title: Optional[int] = None,
+ title: Optional[str] = None,
context: Optional[str] = None,
type: Optional[FileType] = FileType.AUTO,
- importOptions: Optional[
- Union[
- SpreadsheetImportOptions,
- XmlImportOptions,
- DocxFileImportOptions,
- OtherImportOptions,
- HtmlFileImportOptions,
- HtmlWithFrontMatterFileImportOptions,
- MdxV1FileImportOptions,
- MdxV2FileImportOptions,
- ]
- ] = None,
- exportOptions: Optional[
- Union[PropertyExportOptions, GeneralExportOptions, JavascriptExportOptions]
- ] = None,
+ importOptions: Optional[FileImportOptions] = None,
+ exportOptions: Optional[FileExportOptions] = None,
excludedTargetLanguages: Optional[Iterable[str]] = None,
attachLabelIds: Optional[Iterable[int]] = None,
+ parserVersion: Optional[int] = None,
+ fields: Optional[FileFields] = None,
):
"""
Add File.
+ `parserVersion` must be used together with `type`. `fields` is Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.post
"""
projectId = projectId or self.get_project_id()
@@ -508,6 +505,8 @@ def add_file(
"exportOptions": exportOptions,
"excludedTargetLanguages": excludedTargetLanguages,
"attachLabelIds": attachLabelIds,
+ "parserVersion": parserVersion,
+ "fields": fields,
},
)
@@ -516,7 +515,7 @@ def get_file(self, fileId: int, projectId: Optional[int] = None):
Get File.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.get
"""
projectId = projectId or self.get_project_id()
@@ -532,8 +531,10 @@ def restore_file(
"""
Restore File.
+ Restore the file to one of the previous revisions.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.put
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.put
"""
projectId = projectId or self.get_project_id()
@@ -550,29 +551,23 @@ def update_file(
storageId: int,
projectId: Optional[int] = None,
updateOption: Optional[FileUpdateOption] = None,
- importOptions: Optional[
- Union[
- SpreadsheetImportOptions,
- XmlImportOptions,
- DocxFileImportOptions,
- OtherImportOptions,
- HtmlFileImportOptions,
- HtmlWithFrontMatterFileImportOptions,
- MdxV1FileImportOptions,
- MdxV2FileImportOptions,
- ]
- ] = None,
- exportOptions: Optional[
- Union[GeneralExportOptions, PropertyExportOptions, JavascriptExportOptions]
- ] = None,
+ importOptions: Optional[FileImportOptions] = None,
+ exportOptions: Optional[FileExportOptions] = None,
attachLabelIds: Optional[Iterable[int]] = None,
detachLabelIds: Optional[Iterable[int]] = None,
+ name: Optional[str] = None,
+ replaceModifiedContext: Optional[bool] = None,
):
"""
Update File.
+ Replace the file with a new one from the Storage. `importOptions` is sent to the API
+ as-is (e.g. `{"importTranslations": True}` for spreadsheet files). Note that
+ `importOptions.importTranslations` in the API response for spreadsheet files is
+ marked as deprecated by the API and may not reflect the value sent in the request.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.put
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.put
"""
projectId = projectId or self.get_project_id()
@@ -587,6 +582,8 @@ def update_file(
"exportOptions": exportOptions,
"attachLabelIds": attachLabelIds,
"detachLabelIds": detachLabelIds,
+ "name": name,
+ "replaceModifiedContext": replaceModifiedContext,
},
)
@@ -599,8 +596,11 @@ def delete_file(
"""
Delete File.
+ Pass `prefer="respond-async"` to delete the file asynchronously (recommended); the API
+ then returns a job to poll via `check_file_deletion_status`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.delete
"""
projectId = projectId or self.get_project_id()
@@ -622,7 +622,7 @@ def check_file_deletion_status(
Check File Deletion Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.jobs.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.jobs.get
"""
projectId = projectId or self.get_project_id()
@@ -641,8 +641,11 @@ def edit_file(
"""
Edit File.
+ `/fields` and `/fields/{fieldSlug}` paths are Enterprise only. For `/fields/{fieldSlug}`
+ pass the path as a string, e.g. `"/fields/some-field"`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.patch
"""
projectId = projectId or self.get_project_id()
@@ -658,7 +661,7 @@ def download_file_preview(self, fileId: int, projectId: Optional[int] = None):
Download File Preview.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.preview.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.preview.get
"""
projectId = projectId or self.get_project_id()
@@ -672,7 +675,7 @@ def download_file(self, fileId: int, projectId: Optional[int] = None):
Download File.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.download.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.download.get
"""
projectId = projectId or self.get_project_id()
@@ -705,7 +708,7 @@ def list_file_revisions(
List File Revisions.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.revisions.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.revisions.getMany
"""
projectId = projectId or self.get_project_id()
@@ -723,7 +726,7 @@ def get_file_revision(
Get File Revision.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.revisions.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.revisions.get
"""
projectId = projectId or self.get_project_id()
@@ -765,10 +768,10 @@ def list_asset_references(
"offset": offset,
}
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_asset_references_path(project_id, file_id),
- params=params
+ params=params,
)
def add_asset_reference(
diff --git a/crowdin_api/api_resources/source_files/tests/test_source_files_resources.py b/crowdin_api/api_resources/source_files/tests/test_source_files_resources.py
index 9452f37f..79434790 100644
--- a/crowdin_api/api_resources/source_files/tests/test_source_files_resources.py
+++ b/crowdin_api/api_resources/source_files/tests/test_source_files_resources.py
@@ -1,3 +1,4 @@
+import json
from unittest import mock
import pytest
@@ -8,6 +9,7 @@
DirectoryPatchPath,
FilePatchPath,
FileType,
+ FileUpdateOption,
ListDirectoriesOrderBy,
ListFilesOrderBy,
ListProjectBranchesOrderBy,
@@ -174,7 +176,25 @@ def test_add_branch(self, m_request, in_params, request_data, base_absolut_url):
m_request.assert_called_once_with(
method="post",
path=resource.get_branch_path(projectId=1),
- request_data=request_data,
+ request_data={"isProtected": None, **request_data},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_branch_string_based(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_branch(projectId=1, name="name", isProtected=True) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path=resource.get_branch_path(projectId=1),
+ request_data={
+ "name": "name",
+ "title": None,
+ "exportPattern": None,
+ "priority": None,
+ "isProtected": True,
+ },
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -649,7 +669,45 @@ def test_add_file(self, m_request, in_params, request_data, base_absolut_url):
m_request.assert_called_once_with(
method="post",
path=resource.get_file_path(projectId=1),
- request_data=request_data,
+ request_data={"parserVersion": None, "fields": None, **request_data},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_file_with_parser_version_and_fields(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_file(
+ projectId=1,
+ storageId=1,
+ name="name",
+ type=FileType.DOCX,
+ importOptions={"translateDocProperties": True, "translateComments": False},
+ exportOptions={"allowWordStyleOptimization": False},
+ parserVersion=2,
+ fields={"some-field": "value"},
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path=resource.get_file_path(projectId=1),
+ request_data={
+ "name": "name",
+ "storageId": 1,
+ "branchId": None,
+ "directoryId": None,
+ "title": None,
+ "type": FileType.DOCX,
+ "context": None,
+ "importOptions": {"translateDocProperties": True, "translateComments": False},
+ "exportOptions": {"allowWordStyleOptimization": False},
+ "excludedTargetLanguages": None,
+ "attachLabelIds": None,
+ "parserVersion": 2,
+ "fields": {"some-field": "value"},
+ },
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -719,6 +777,28 @@ def test_edit_file(self, m_request, base_absolut_url):
path=resource.get_file_path(projectId=1, fileId=2),
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_file_fields(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {"value": {"a": 1}, "op": PatchOperation.REPLACE, "path": FilePatchPath.FIELDS},
+ {
+ "value": True,
+ "op": PatchOperation.REPLACE,
+ "path": FilePatchPath.IMPORT_OPTIONS_TRANSLATE_DOC_PROPERTIES,
+ },
+ {"value": "x", "op": PatchOperation.REPLACE, "path": "/fields/some-field"},
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_file(projectId=1, fileId=2, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ request_data=data,
+ path=resource.get_file_path(projectId=1, fileId=2),
+ )
+
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_update_file(self, m_request, base_absolut_url):
m_request.return_value = "response"
@@ -734,10 +814,76 @@ def test_update_file(self, m_request, base_absolut_url):
"exportOptions": None,
"attachLabelIds": None,
"detachLabelIds": None,
+ "name": None,
+ "replaceModifiedContext": None,
+ },
+ path=resource.get_file_path(projectId=1, fileId=2),
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_update_file_with_all_params(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ import_options = {
+ "firstLineContainsHeader": True,
+ "importTranslations": True,
+ "scheme": {"identifier": 0, "sourcePhrase": 1, "translation": 2},
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.update_file(
+ projectId=1,
+ fileId=2,
+ storageId=1,
+ updateOption=FileUpdateOption.KEEP_TRANSLATIONS,
+ importOptions=import_options,
+ exportOptions={"exportPattern": "pattern"},
+ attachLabelIds=[1],
+ detachLabelIds=[2],
+ name="name.csv",
+ replaceModifiedContext=True,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="put",
+ request_data={
+ "storageId": 1,
+ "updateOption": FileUpdateOption.KEEP_TRANSLATIONS,
+ "importOptions": {
+ "firstLineContainsHeader": True,
+ "importTranslations": True,
+ "scheme": {"identifier": 0, "sourcePhrase": 1, "translation": 2},
+ },
+ "exportOptions": {"exportPattern": "pattern"},
+ "attachLabelIds": [1],
+ "detachLabelIds": [2],
+ "name": "name.csv",
+ "replaceModifiedContext": True,
},
path=resource.get_file_path(projectId=1, fileId=2),
)
+ @mock.patch("requests.Session.request")
+ def test_update_file_sends_import_translations(self, m_session_request, base_absolut_url):
+ """Regression test for #246: importTranslations must reach the HTTP body unchanged."""
+ m_session_request.return_value = mock.Mock(status_code=200, content=b"{}")
+
+ resource = self.get_resource(base_absolut_url)
+ resource.update_file(
+ projectId=1,
+ fileId=2,
+ storageId=1,
+ updateOption=FileUpdateOption.KEEP_TRANSLATIONS,
+ importOptions={"importTranslations": True, "firstLineContainsHeader": False},
+ )
+ assert json.loads(m_session_request.call_args.kwargs["data"]) == {
+ "storageId": 1,
+ "updateOption": "keep_translations",
+ "importOptions": {"importTranslations": True, "firstLineContainsHeader": False},
+ }
+
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_restore_file(self, m_request, base_absolut_url):
m_request.return_value = "response"
diff --git a/crowdin_api/api_resources/source_files/types.py b/crowdin_api/api_resources/source_files/types.py
index 6c0cd942..cb95f5a8 100644
--- a/crowdin_api/api_resources/source_files/types.py
+++ b/crowdin_api/api_resources/source_files/types.py
@@ -1,12 +1,17 @@
-from typing import Any, Iterable
+from typing import Any, Dict, Iterable, Union
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.api_resources.source_files.enums import (
BranchPatchPath,
DirectoryPatchPath,
EscapeQuotes,
+ EscapeSpecialCharacters,
ExportQuotes,
FilePatchPath,
+ FrontMatterQuotes,
+ MarkdownMarker,
+ TableColumnWidth,
+ UnorderedListBullet,
)
from crowdin_api.typing import TypedDict
@@ -24,25 +29,48 @@ class DirectoryPatchRequest(TypedDict):
class Scheme(TypedDict, total=False):
+ none: int
identifier: int
sourcePhrase: int
+ sourceOrTranslation: int
+ translation: int
+ context: int
+ maxLength: int
+ translatable: int
+ labels: int
-class SpreadsheetImportOptions(TypedDict):
+class SpreadsheetImportOptions(TypedDict, total=False):
firstLineContainsHeader: bool
+ importHiddenSheets: bool
+ importHiddenRows: bool
+ importEqSuggestions: bool
+ autoApproveImported: bool
+ translateHidden: bool
+ addToTm: bool
+ contentSegmentation: bool
+ srxStorageId: int
importTranslations: bool
scheme: Scheme
- srxStorageId: int
-class XmlImportOptions(TypedDict):
+class XmlImportOptions(TypedDict, total=False):
translateContent: bool
translateAttributes: bool
+ inlineTags: Iterable[str]
contentSegmentation: bool
translatableElements: Iterable[str]
+ srxStorageId: int
-class DocxFileImportOptions(TypedDict):
+class WebXmlFileImportOptions(TypedDict, total=False):
+ inlineTags: Iterable[str]
+ hideAttributeValues: bool
+ contentSegmentation: bool
+ srxStorageId: int
+
+
+class DocxFileImportOptions(TypedDict, total=False):
cleanTagsAggressively: bool
translateHiddenText: bool
translateHyperlinkUrls: bool
@@ -51,52 +79,161 @@ class DocxFileImportOptions(TypedDict):
importHiddenSlides: bool
contentSegmentation: bool
srxStorageId: int
-
-
-class OtherImportOptions(TypedDict):
+ translateDocProperties: bool
+ translateComments: bool
+ ignoreWhitespaceStyles: bool
+ addTabAsCharacter: bool
+ addLineSeparatorAsCharacter: bool
+ lineSeparatorReplacement: str
+ replaceNoBreakHyphenTag: bool
+ ignoreSoftHyphenTag: bool
+ complexFieldDefinitionsToExtract: Iterable[str]
+ translateWordHeadersFooters: bool
+ translateWordGraphicName: bool
+ translateWordGraphicDescription: bool
+ ignoreWordFontColors: bool
+ wordFontColorsMinIgnoranceThreshold: str
+ wordFontColorsMaxIgnoranceThreshold: str
+ excludeWordStyles: Iterable[str]
+ translateWordInExcludeStyleMode: bool
+ wordHighlightColors: Iterable[str]
+ translateWordInExcludeHighlightMode: bool
+ translateWordExcludeColors: bool
+ wordExcludedColors: Iterable[str]
+ translateExcelCellsCopied: bool
+ translateExcelSheetNames: bool
+ excelExcludedColors: Iterable[str]
+ translateExcelDiagramData: bool
+ translateExcelDrawings: bool
+
+
+class VsdxFileImportOptions(TypedDict, total=False):
+ cleanTagsAggressively: bool
+ translateHyperlinkUrls: bool
contentSegmentation: bool
srxStorageId: int
-class GeneralExportOptions(TypedDict):
- exportPattern: str
-
-
-class PropertyExportOptions:
- escapeQuotes: EscapeQuotes
- exportPattern: str
-
-
-class JavascriptExportOptions:
- exportQuotes: ExportQuotes
- exportPattern: str
+class IdmlFileImportOptions(TypedDict, total=False):
+ inlineHyperlinkText: bool
+ contentSegmentation: bool
+ srxStorageId: int
-class FilePatchRequest(TypedDict):
- value: Any
- op: PatchOperation
- path: FilePatchPath
+class OtherImportOptions(TypedDict, total=False):
+ contentSegmentation: bool
+ srxStorageId: int
-class HtmlFileImportOptions(TypedDict):
+class HtmlFileImportOptions(TypedDict, total=False):
excludedElements: Iterable[str]
contentSegmentation: bool
+ inlineTags: Iterable[str]
srxStorageId: int
-class HtmlWithFrontMatterFileImportOptions(HtmlFileImportOptions):
+class HtmlWithFrontMatterFileImportOptions(HtmlFileImportOptions, total=False):
+ excludedFrontMatterElements: Iterable[str]
+
+
+class MdFileImportOptions(TypedDict, total=False):
excludedFrontMatterElements: Iterable[str]
+ excludeCodeBlocks: bool
+ inlineTags: Iterable[str]
+ contentSegmentation: bool
+ srxStorageId: int
-class MdxV1FileImportOptions(TypedDict):
+class MdxV1FileImportOptions(TypedDict, total=False):
excludedFrontMatterElements: Iterable[str]
excludeCodeBlocks: bool
contentSegmentation: bool
srxStorageId: int
-class MdxV2FileImportOptions(TypedDict):
+class MdxV2FileImportOptions(TypedDict, total=False):
excludedFrontMatterElements: Iterable[str]
excludeCodeBlocks: bool
contentSegmentation: bool
srxStorageId: int
+
+
+class StringCatalogFileImportOptions(TypedDict, total=False):
+ importKeyAsSource: bool
+ importTranslations: bool
+
+
+class AdocFileImportOptions(TypedDict, total=False):
+ excludeIncludeDirectives: bool
+
+
+class VdfFileImportOptions(TypedDict, total=False):
+ convertIcu: bool
+ addGenderArgument: bool
+
+
+FileImportOptions = Union[
+ SpreadsheetImportOptions,
+ XmlImportOptions,
+ WebXmlFileImportOptions,
+ DocxFileImportOptions,
+ VsdxFileImportOptions,
+ IdmlFileImportOptions,
+ HtmlFileImportOptions,
+ HtmlWithFrontMatterFileImportOptions,
+ MdFileImportOptions,
+ MdxV1FileImportOptions,
+ MdxV2FileImportOptions,
+ StringCatalogFileImportOptions,
+ AdocFileImportOptions,
+ VdfFileImportOptions,
+ OtherImportOptions,
+]
+
+
+class GeneralExportOptions(TypedDict, total=False):
+ exportPattern: str
+
+
+class PropertyExportOptions(GeneralExportOptions, total=False):
+ escapeQuotes: EscapeQuotes
+ escapeSpecialCharacters: EscapeSpecialCharacters
+
+
+class JavascriptExportOptions(GeneralExportOptions, total=False):
+ exportQuotes: ExportQuotes
+
+
+class MdxFileExportOptions(GeneralExportOptions, total=False):
+ strongMarker: MarkdownMarker
+ emphasisMarker: MarkdownMarker
+ unorderedListBullet: UnorderedListBullet
+ tableColumnWidth: TableColumnWidth
+
+
+class MdFileExportOptions(MdxFileExportOptions, total=False):
+ frontMatterQuotes: FrontMatterQuotes
+
+
+class DocxFileExportOptions(GeneralExportOptions, total=False):
+ allowWordStyleOptimization: bool
+ translateExcelExcludeColors: bool
+
+
+FileExportOptions = Union[
+ GeneralExportOptions,
+ PropertyExportOptions,
+ JavascriptExportOptions,
+ MdFileExportOptions,
+ MdxFileExportOptions,
+ DocxFileExportOptions,
+]
+
+# Enterprise only. Keys get via List Fields.
+FileFields = Dict[str, Union[str, int, bool, Iterable[str]]]
+
+
+class FilePatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: Union[FilePatchPath, str]
diff --git a/crowdin_api/api_resources/source_strings/enums.py b/crowdin_api/api_resources/source_strings/enums.py
index 56ad66d0..d42d395f 100644
--- a/crowdin_api/api_resources/source_strings/enums.py
+++ b/crowdin_api/api_resources/source_strings/enums.py
@@ -10,11 +10,15 @@ class ScopeFilter(Enum):
class SourceStringsPatchPath(Enum):
+ IDENTIFIER = "/identifier"
TEXT = "/text"
CONTEXT = "/context"
IS_HIDDEN = "/isHidden"
MAXLENGTH = "/maxLength"
LABEL_IDS = "/labelIds"
+ # Crowdin Enterprise only
+ FIELDS = "/fields"
+ FIELD = "/fields/{fieldSlug}"
class StringBatchOperations(Enum):
@@ -30,6 +34,10 @@ class StringBatchOperationsPath(Enum):
IS_HIDDEN = "/{stringId}/isHidden"
MAX_LENGTH = "/{stringId}/maxLength"
LABEL_IDS = "/{stringId}/labelIds"
+ # Use with the "remove" operation
+ STRING = "/{stringId}"
+ # Use with the "add" operation
+ NEW_STRING = "/-"
class ListStringsOrderBy(Enum):
@@ -40,3 +48,35 @@ class ListStringsOrderBy(Enum):
CREATED_AT = "createdAt"
UPDATED_AT = "updatedAt"
TYPE = "type"
+
+
+class StringUpdateOption(Enum):
+ """
+ Defines whether to keep existing translations and approvals for updated strings.
+ """
+
+ KEEP_TRANSLATIONS_AND_APPROVALS = "keep_translations_and_approvals"
+ KEEP_TRANSLATIONS = "keep_translations"
+ CLEAR_TRANSLATIONS_AND_APPROVALS = "clear_translations_and_approvals"
+
+
+class UploadStringsType(Enum):
+ AUTO = "auto"
+ ANDROID = "android"
+ MACOSX = "macosx"
+ ARB = "arb"
+ CSV = "csv"
+ JSON = "json"
+ XLSX = "xlsx"
+ XLIFF = "xliff"
+ XLIFF_TWO = "xliff_two"
+ RESX = "resx"
+ GETTEXT = "gettext"
+ I18NEXT_JSON = "i18next_json"
+ PROPERTIES = "properties"
+ PROPERTIES_XML = "properties_xml"
+ PROPERTIES_PLAY = "properties_play"
+ YAML = "yaml"
+ STRING_CATALOG = "string_catalog"
+ NESTJS_I18N = "nestjs_i18n"
+ STRINGSDICT = "stringsdict"
diff --git a/crowdin_api/api_resources/source_strings/resource.py b/crowdin_api/api_resources/source_strings/resource.py
index 6e6bfb88..7de7885e 100644
--- a/crowdin_api/api_resources/source_strings/resource.py
+++ b/crowdin_api/api_resources/source_strings/resource.py
@@ -1,11 +1,16 @@
-from typing import Iterable, Optional
+from typing import Any, Dict, Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.enums import DenormalizePlaceholders
-from crowdin_api.api_resources.source_strings.enums import ScopeFilter
+from crowdin_api.api_resources.source_strings.enums import (
+ ScopeFilter,
+ StringUpdateOption,
+ UploadStringsType,
+)
from crowdin_api.api_resources.source_strings.types import (
SourceStringsPatchRequest,
StringBatchOperationPatchRequest,
+ UploadStringsImportOptions,
)
from crowdin_api.sorting import Sorting
@@ -22,7 +27,7 @@ class SourceStringsResource(BaseResource):
iOS strings, PROPERTIES, XLIFF).
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Source-Strings
+ https://support.crowdin.com/developer/api/v2/#tag/Source-Strings
"""
def search_strings(
@@ -40,8 +45,8 @@ def search_strings(
Search Strings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.strings.getMany
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.strings.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.strings.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.strings.getMany
"""
params = {
@@ -78,12 +83,16 @@ def list_strings(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ directoryId: Optional[int] = None,
):
"""
List Strings.
+ :param directoryId: Directory Identifier (file-based projects only).
+ Can't be used with `taskId`, `fileId` or `branchId` in the same request.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings.getMany
"""
projectId = projectId or self.get_project_id()
@@ -91,6 +100,7 @@ def list_strings(
"orderBy": orderBy,
"branchId": branchId,
"fileId": fileId,
+ "directoryId": directoryId,
"denormalizePlaceholders": denormalizePlaceholders,
"labelIds": None if labelIds is None else ",".join(str(item) for item in labelIds),
"taskId": taskId,
@@ -108,7 +118,7 @@ def list_strings(
def add_string(
self,
- text: str,
+ text: Union[str, Dict[str, str]],
projectId: Optional[int] = None,
identifier: Optional[str] = None,
fileId: Optional[int] = None,
@@ -116,13 +126,20 @@ def add_string(
isHidden: Optional[bool] = None,
maxLength: Optional[int] = None,
labelIds: Optional[Iterable[int]] = None,
- branchId: Optional[int] = None
+ branchId: Optional[int] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
Add String.
+ :param text: Text for translation. Use a dict (e.g. {"one": "...", "other": "..."})
+ for plural strings.
+ :param fileId: File Identifier (file-based projects only).
+ :param branchId: Branch Identifier (string-based projects only).
+ :param fields: Fields values, keys get via List Fields (Crowdin Enterprise only).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings.post
"""
projectId = projectId or self.get_project_id()
@@ -138,16 +155,22 @@ def add_string(
"isHidden": isHidden,
"maxLength": maxLength,
"labelIds": labelIds,
- "branchId": branchId
+ "branchId": branchId,
+ "fields": fields,
},
)
- def get_string(self, stringId: int, projectId: Optional[int] = None):
+ def get_string(
+ self,
+ stringId: int,
+ projectId: Optional[int] = None,
+ denormalizePlaceholders: Optional[DenormalizePlaceholders] = None,
+ ):
"""
Get String.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings.get
"""
projectId = projectId or self.get_project_id()
@@ -155,6 +178,7 @@ def get_string(self, stringId: int, projectId: Optional[int] = None):
return self.requester.request(
method="get",
path=self.get_source_strings_path(projectId=projectId, stringId=stringId),
+ params={"denormalizePlaceholders": denormalizePlaceholders},
)
def delete_string(self, stringId: int, projectId: Optional[int] = None):
@@ -162,7 +186,7 @@ def delete_string(self, stringId: int, projectId: Optional[int] = None):
Delete String.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings.delete
"""
projectId = projectId or self.get_project_id()
@@ -177,12 +201,16 @@ def edit_string(
stringId: int,
data: Iterable[SourceStringsPatchRequest],
projectId: Optional[int] = None,
+ updateOption: Optional[StringUpdateOption] = None,
):
"""
Edit String.
+ :param updateOption: Defines whether to keep existing translations and approvals for
+ the updated string. Applied only when `text` or `identifier` is changed.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings.patch
"""
projectId = projectId or self.get_project_id()
@@ -190,6 +218,7 @@ def edit_string(
return self.requester.request(
method="patch",
path=self.get_source_strings_path(projectId=projectId, stringId=stringId),
+ params={"updateOption": updateOption},
request_data=data,
)
@@ -197,12 +226,16 @@ def string_batch_operation(
self,
data: Iterable[StringBatchOperationPatchRequest],
projectId: Optional[int] = None,
+ updateOption: Optional[StringUpdateOption] = None,
):
"""
String Batch Operations.
+ :param updateOption: Defines whether to keep existing translations and approvals for
+ updated strings. Applied only when `text` or `identifier` is changed.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.strings.batchPatch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.strings.batchPatch
"""
projectId = projectId or self.get_project_id()
@@ -210,5 +243,180 @@ def string_batch_operation(
return self.requester.request(
method="patch",
path=self.get_source_strings_path(projectId=projectId),
+ params={"updateOption": updateOption},
request_data=data,
)
+
+ # Upload Strings
+ def get_strings_uploads_path(self, projectId: int, uploadId: Optional[str] = None):
+ if uploadId is not None:
+ return f"projects/{projectId}/strings/uploads/{uploadId}"
+
+ return f"projects/{projectId}/strings/uploads"
+
+ def upload_strings(
+ self,
+ storageId: int,
+ branchId: int,
+ projectId: Optional[int] = None,
+ type: Optional[UploadStringsType] = None,
+ parserVersion: Optional[int] = None,
+ labelIds: Optional[Iterable[int]] = None,
+ updateStrings: Optional[bool] = None,
+ cleanupMode: Optional[bool] = None,
+ importOptions: Optional[UploadStringsImportOptions] = None,
+ updateOption: Optional[StringUpdateOption] = None,
+ ):
+ """
+ Upload Strings.
+
+ String-based projects only.
+
+ :param updateOption: Must be used together with `updateStrings = True`.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/string-based/#operation/api.projects.strings.uploads.post
+ https://support.crowdin.com/developer/enterprise/api/v2/string-based/#operation/api.projects.strings.uploads.post
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_strings_uploads_path(projectId=projectId),
+ request_data={
+ "storageId": storageId,
+ "branchId": branchId,
+ "type": type,
+ "parserVersion": parserVersion,
+ "labelIds": labelIds,
+ "updateStrings": updateStrings,
+ "cleanupMode": cleanupMode,
+ "importOptions": importOptions,
+ "updateOption": updateOption,
+ },
+ )
+
+ def upload_strings_status(self, uploadId: str, projectId: Optional[int] = None):
+ """
+ Upload Strings Status.
+
+ String-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/string-based/#operation/api.projects.strings.uploads.get
+ https://support.crowdin.com/developer/enterprise/api/v2/string-based/#operation/api.projects.strings.uploads.get
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_strings_uploads_path(projectId=projectId, uploadId=uploadId),
+ )
+
+
+class EnterpriseSourceStringsResource(SourceStringsResource):
+ """
+ Resource for Source Strings (Crowdin Enterprise).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Source-Strings
+ """
+
+ def get_reviewed_builds_path(self, projectId: int, buildId: Optional[int] = None):
+ if buildId is not None:
+ return f"projects/{projectId}/strings/reviewed-builds/{buildId}"
+
+ return f"projects/{projectId}/strings/reviewed-builds"
+
+ def list_reviewed_source_files_builds(
+ self,
+ projectId: Optional[int] = None,
+ branchId: Optional[int] = None,
+ page: Optional[int] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List Reviewed Source Files Builds.
+
+ File-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.strings.reviewed-builds.getMany
+ """
+
+ projectId = projectId or self.get_project_id()
+ params = {"branchId": branchId}
+ params.update(self.get_page_params(page=page, offset=offset, limit=limit))
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_reviewed_builds_path(projectId=projectId),
+ params=params,
+ )
+
+ def build_reviewed_source_files(
+ self,
+ projectId: Optional[int] = None,
+ branchId: Optional[int] = None,
+ ):
+ """
+ Build Reviewed Source Files.
+
+ File-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.strings.reviewed-builds.post
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_reviewed_builds_path(projectId=projectId),
+ request_data={"branchId": branchId},
+ )
+
+ def check_reviewed_source_files_build_status(
+ self,
+ buildId: int,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Check Reviewed Source Files Build Status.
+
+ File-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.strings.reviewed-builds.get
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_reviewed_builds_path(projectId=projectId, buildId=buildId),
+ )
+
+ def download_reviewed_source_files(
+ self,
+ buildId: int,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Download Reviewed Source Files.
+
+ File-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.strings.reviewed-builds.download.download
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_reviewed_builds_path(projectId=projectId, buildId=buildId) + "/download",
+ )
diff --git a/crowdin_api/api_resources/source_strings/tests/test_source_strings_resources.py b/crowdin_api/api_resources/source_strings/tests/test_source_strings_resources.py
index 8deafaf7..f8f43ca3 100644
--- a/crowdin_api/api_resources/source_strings/tests/test_source_strings_resources.py
+++ b/crowdin_api/api_resources/source_strings/tests/test_source_strings_resources.py
@@ -8,8 +8,13 @@
SourceStringsPatchPath,
StringBatchOperationsPath,
StringBatchOperations,
+ StringUpdateOption,
+ UploadStringsType,
+)
+from crowdin_api.api_resources.source_strings.resource import (
+ EnterpriseSourceStringsResource,
+ SourceStringsResource,
)
-from crowdin_api.api_resources.source_strings.resource import SourceStringsResource
from crowdin_api.requester import APIRequester
from crowdin_api.sorting import Sorting, SortingOrder, SortingRule
@@ -96,6 +101,7 @@ def test_get_source_strings_path(self, in_params, path, base_absolut_url):
"offset": 0,
"limit": 10,
"fileId": None,
+ "directoryId": None,
"croql": None,
"denormalizePlaceholders": None,
"labelIds": None,
@@ -120,6 +126,7 @@ def test_get_source_strings_path(self, in_params, path, base_absolut_url):
"scope": ScopeFilter.CONTEXT,
"branchId": 2,
"taskId": 5,
+ "directoryId": 7,
},
{
"orderBy": Sorting(
@@ -135,6 +142,7 @@ def test_get_source_strings_path(self, in_params, path, base_absolut_url):
"scope": ScopeFilter.CONTEXT,
"branchId": 2,
"taskId": 5,
+ "directoryId": 7,
},
),
),
@@ -166,7 +174,8 @@ def test_list_strings(self, m_request, in_params, request_params, base_absolut_u
"isHidden": None,
"maxLength": None,
"labelIds": None,
- "branchId": None
+ "branchId": None,
+ "fields": None,
},
),
(
@@ -178,7 +187,8 @@ def test_list_strings(self, m_request, in_params, request_params, base_absolut_u
"isHidden": True,
"maxLength": 2,
"labelIds": [1, 2, 3],
- "branchId": None
+ "branchId": None,
+ "fields": {"some-field": "value"},
},
{
"text": "text",
@@ -188,7 +198,26 @@ def test_list_strings(self, m_request, in_params, request_params, base_absolut_u
"isHidden": True,
"maxLength": 2,
"labelIds": [1, 2, 3],
- "branchId": None
+ "branchId": None,
+ "fields": {"some-field": "value"},
+ },
+ ),
+ (
+ {
+ "text": {"one": "string", "other": "strings"},
+ "identifier": "identifier",
+ "branchId": 3,
+ },
+ {
+ "text": {"one": "string", "other": "strings"},
+ "identifier": "identifier",
+ "fileId": None,
+ "context": None,
+ "isHidden": None,
+ "maxLength": None,
+ "labelIds": None,
+ "branchId": 3,
+ "fields": None,
},
),
),
@@ -212,7 +241,28 @@ def test_get_string(self, m_request, base_absolut_url):
resource = self.get_resource(base_absolut_url)
assert resource.get_string(projectId=1, stringId=2) == "response"
m_request.assert_called_once_with(
- method="get", path=resource.get_source_strings_path(projectId=1, stringId=2)
+ method="get",
+ path=resource.get_source_strings_path(projectId=1, stringId=2),
+ params={"denormalizePlaceholders": None},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_string_denormalize_placeholders(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.get_string(
+ projectId=1,
+ stringId=2,
+ denormalizePlaceholders=DenormalizePlaceholders.ENABLE,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path=resource.get_source_strings_path(projectId=1, stringId=2),
+ params={"denormalizePlaceholders": DenormalizePlaceholders.ENABLE},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -243,6 +293,41 @@ def test_edit_string(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="patch",
request_data=data,
+ params={"updateOption": None},
+ path=resource.get_source_strings_path(projectId=1, stringId=2),
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_string_with_update_option(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "value": "new.identifier",
+ "op": PatchOperation.REPLACE,
+ "path": SourceStringsPatchPath.IDENTIFIER,
+ },
+ {
+ "value": {"some-field": "value"},
+ "op": PatchOperation.REPLACE,
+ "path": SourceStringsPatchPath.FIELDS,
+ },
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.edit_string(
+ projectId=1,
+ stringId=2,
+ data=data,
+ updateOption=StringUpdateOption.KEEP_TRANSLATIONS,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="patch",
+ request_data=data,
+ params={"updateOption": StringUpdateOption.KEEP_TRANSLATIONS},
path=resource.get_source_strings_path(projectId=1, stringId=2),
)
@@ -267,5 +352,202 @@ def test_string_batch_operation(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="patch",
path=resource.get_source_strings_path(1),
+ params={"updateOption": None},
request_data=data,
)
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_string_batch_operation_with_update_option(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": StringBatchOperations.ADD,
+ "path": StringBatchOperationsPath.NEW_STRING,
+ "value": {"text": "new", "identifier": "a.b.c", "branchId": 5},
+ },
+ {
+ "op": StringBatchOperations.REMOVE,
+ "path": StringBatchOperationsPath.STRING,
+ },
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.string_batch_operation(
+ projectId=1,
+ data=data,
+ updateOption=StringUpdateOption.CLEAR_TRANSLATIONS_AND_APPROVALS,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="patch",
+ path=resource.get_source_strings_path(1),
+ params={"updateOption": StringUpdateOption.CLEAR_TRANSLATIONS_AND_APPROVALS},
+ request_data=data,
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"projectId": 1}, "projects/1/strings/uploads"),
+ ({"projectId": 1, "uploadId": "abc"}, "projects/1/strings/uploads/abc"),
+ ),
+ )
+ def test_get_strings_uploads_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_strings_uploads_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {"storageId": 1, "branchId": 2},
+ {
+ "storageId": 1,
+ "branchId": 2,
+ "type": None,
+ "parserVersion": None,
+ "labelIds": None,
+ "updateStrings": None,
+ "cleanupMode": None,
+ "importOptions": None,
+ "updateOption": None,
+ },
+ ),
+ (
+ {
+ "storageId": 1,
+ "branchId": 2,
+ "type": UploadStringsType.XLSX,
+ "parserVersion": 3,
+ "labelIds": [4, 5],
+ "updateStrings": True,
+ "cleanupMode": False,
+ "importOptions": {
+ "firstLineContainsHeader": True,
+ "scheme": {"identifier": 0, "sourcePhrase": 1},
+ },
+ "updateOption": StringUpdateOption.KEEP_TRANSLATIONS,
+ },
+ {
+ "storageId": 1,
+ "branchId": 2,
+ "type": UploadStringsType.XLSX,
+ "parserVersion": 3,
+ "labelIds": [4, 5],
+ "updateStrings": True,
+ "cleanupMode": False,
+ "importOptions": {
+ "firstLineContainsHeader": True,
+ "scheme": {"identifier": 0, "sourcePhrase": 1},
+ },
+ "updateOption": StringUpdateOption.KEEP_TRANSLATIONS,
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_upload_strings(self, m_request, in_params, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.upload_strings(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/strings/uploads",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_upload_strings_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.upload_strings_status(projectId=1, uploadId="abc") == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/strings/uploads/abc",
+ )
+
+
+class TestEnterpriseSourceStringsResource(TestSourceFilesResource):
+ resource_class = EnterpriseSourceStringsResource
+
+ @pytest.mark.parametrize(
+ "in_params, path",
+ (
+ ({"projectId": 1}, "projects/1/strings/reviewed-builds"),
+ ({"projectId": 1, "buildId": 2}, "projects/1/strings/reviewed-builds/2"),
+ ),
+ )
+ def test_get_reviewed_builds_path(self, in_params, path, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_reviewed_builds_path(**in_params) == path
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"branchId": None, "offset": 0, "limit": 25}),
+ ({"branchId": 2, "offset": 5, "limit": 10}, {"branchId": 2, "offset": 5, "limit": 10}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_reviewed_source_files_builds(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_reviewed_source_files_builds(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/strings/reviewed-builds",
+ params=request_params,
+ )
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ ({}, {"branchId": None}),
+ ({"branchId": 2}, {"branchId": 2}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_build_reviewed_source_files(
+ self, m_request, in_params, request_data, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.build_reviewed_source_files(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/strings/reviewed-builds",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_check_reviewed_source_files_build_status(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.check_reviewed_source_files_build_status(projectId=1, buildId=2)
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/strings/reviewed-builds/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_download_reviewed_source_files(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.download_reviewed_source_files(projectId=1, buildId=2) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/strings/reviewed-builds/2/download",
+ )
diff --git a/crowdin_api/api_resources/source_strings/types.py b/crowdin_api/api_resources/source_strings/types.py
index 64a9d0f2..31805cee 100644
--- a/crowdin_api/api_resources/source_strings/types.py
+++ b/crowdin_api/api_resources/source_strings/types.py
@@ -19,3 +19,43 @@ class StringBatchOperationPatchRequest(TypedDict):
op: StringBatchOperations
path: StringBatchOperationsPath
value: Union[str, dict, int, bool]
+
+
+class UploadStringsSpreadsheetScheme(TypedDict, total=False):
+ """
+ Column mapping (numbering starts at 0). Language identifiers (e.g. "en", "de") may also be
+ used as keys with a column number as a value.
+ """
+
+ none: int
+ identifier: int
+ sourcePhrase: int
+ sourceOrTranslation: int
+ translation: int
+ context: int
+ translatable: int
+
+
+class UploadStringsSpreadsheetImportOptions(TypedDict, total=False):
+ firstLineContainsHeader: bool
+ importHiddenSheets: bool
+ contentSegmentation: bool
+ srxStorageId: int
+ scheme: UploadStringsSpreadsheetScheme
+
+
+class UploadStringsStringCatalogImportOptions(TypedDict, total=False):
+ importKeyAsSource: bool
+ importTranslations: bool
+
+
+class UploadStringsOtherImportOptions(TypedDict, total=False):
+ contentSegmentation: bool
+ srxStorageId: int
+
+
+UploadStringsImportOptions = Union[
+ UploadStringsSpreadsheetImportOptions,
+ UploadStringsStringCatalogImportOptions,
+ UploadStringsOtherImportOptions,
+]
diff --git a/crowdin_api/api_resources/storages/resource.py b/crowdin_api/api_resources/storages/resource.py
index 363eec7e..2fc0a763 100644
--- a/crowdin_api/api_resources/storages/resource.py
+++ b/crowdin_api/api_resources/storages/resource.py
@@ -17,7 +17,7 @@ class StoragesResource(BaseResource):
be removed from storage and will remain in your account.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Storage
+ https://support.crowdin.com/developer/api/v2/#tag/Storage
"""
def get_storages_path(self, storageId: Optional[int] = None):
@@ -35,7 +35,7 @@ def list_storages(
"""List Storages.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.storages.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.storages.getMany
"""
return self._get_entire_data(
@@ -48,7 +48,7 @@ def add_storage(self, file: IO):
"""Add Storage.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.storages.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.storages.post
"""
return self.requester.request(method="post", path=self.get_storages_path(), file=file)
@@ -57,7 +57,7 @@ def get_storage(self, storageId: int):
"""Get Storage.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.storages.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.storages.get
"""
return self.requester.request(
@@ -68,7 +68,7 @@ def delete_storage(self, storageId: int):
"""Delete Storage.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.storages.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.storages.delete
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/string_comments/resource.py b/crowdin_api/api_resources/string_comments/resource.py
index f5a702e1..7cdf4812 100644
--- a/crowdin_api/api_resources/string_comments/resource.py
+++ b/crowdin_api/api_resources/string_comments/resource.py
@@ -1,4 +1,4 @@
-from typing import Iterable, Optional
+from typing import Dict, Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.string_comments.enums import (
@@ -17,7 +17,7 @@ class StringCommentsResource(BaseResource):
Use API to add or remove strings translations, approvals, and votes.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/String-Comments
+ https://support.crowdin.com/developer/api/v2/#tag/String-Comments
"""
def get_string_comments_path(self, projectId: int, stringCommentId: Optional[int] = None):
@@ -37,18 +37,24 @@ def list_string_comments(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ fileId: Optional[int] = None,
):
"""
List String Comments.
+ :param stringId: Filter comments by string. Can't be used together with `fileId`.
+ :param fileId: Filter comments by asset file (file-based projects only).
+ Can't be used together with `stringId`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.comments.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.comments.getMany
"""
projectId = projectId or self.get_project_id()
params = {
"orderBy": orderBy,
"stringId": stringId,
+ "fileId": fileId,
"type": type,
"issueType": None if issueType is None else ",".join(item.value for item in issueType),
"issueStatus": issueStatus,
@@ -64,18 +70,27 @@ def list_string_comments(
def add_string_comment(
self,
text: str,
- stringId: int,
- targetLanguageId: str,
- type: StringCommentType,
+ stringId: Optional[int] = None,
+ targetLanguageId: Optional[str] = None,
+ type: Optional[StringCommentType] = None,
projectId: Optional[int] = None,
issueType: Optional[StringCommentIssueType] = None,
- attachments: Optional[Iterable[int]] = None,
+ attachments: Optional[Iterable[Union[int, Dict[str, int]]]] = None,
+ fileId: Optional[int] = None,
+ isShared: Optional[bool] = None,
):
"""
Add String Comment.
+ :param stringId: String Identifier. Required unless `fileId` is used.
+ :param type: Comment type (comment or issue). Required by the API.
+ :param attachments: Storage Identifiers (or {"id": storageId} objects) of attachments.
+ :param fileId: Asset File Identifier, used instead of `stringId` to comment
+ an asset (file-based projects only).
+ :param isShared: Defines shared comment or issue (Crowdin Enterprise only).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.comments.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.comments.post
"""
projectId = projectId or self.get_project_id()
@@ -86,10 +101,16 @@ def add_string_comment(
request_data={
"text": text,
"stringId": stringId,
+ "fileId": fileId,
"targetLanguageId": targetLanguageId,
"type": type,
+ "isShared": isShared,
"issueType": issueType,
- "attachments": attachments,
+ "attachments": None
+ if attachments is None
+ else [
+ item if isinstance(item, dict) else {"id": item} for item in attachments
+ ],
},
)
@@ -98,7 +119,7 @@ def get_string_comment(self, stringCommentId: int, projectId: Optional[int] = No
Get String Comment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.comments.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.comments.get
"""
projectId = projectId or self.get_project_id()
@@ -117,7 +138,7 @@ def delete_string_comment(
Delete String Comment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.comments.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.comments.delete
"""
projectId = projectId or self.get_project_id()
@@ -136,7 +157,7 @@ def delete_string_comment_attachment(
Delete String Comment Attachment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.comments.attachments.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.comments.attachments.delete
"""
projectId = projectId or self.get_project_id()
@@ -159,7 +180,7 @@ def edit_string_comment(
Edit String Comment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.comments.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.comments.patch
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/string_comments/tests/test_string_comments_resources.py b/crowdin_api/api_resources/string_comments/tests/test_string_comments_resources.py
index 20f3e7a1..e751f180 100644
--- a/crowdin_api/api_resources/string_comments/tests/test_string_comments_resources.py
+++ b/crowdin_api/api_resources/string_comments/tests/test_string_comments_resources.py
@@ -48,6 +48,20 @@ def test_get_string_comments_path(self, in_params, path, base_absolut_url):
"offset": 0,
"limit": 10,
"stringId": None,
+ "fileId": None,
+ "type": None,
+ "issueType": None,
+ "issueStatus": None,
+ },
+ ),
+ (
+ {"fileId": 5},
+ {
+ "orderBy": None,
+ "offset": 0,
+ "limit": 25,
+ "stringId": None,
+ "fileId": 5,
"type": None,
"issueType": None,
"issueStatus": None,
@@ -75,6 +89,7 @@ def test_get_string_comments_path(self, in_params, path, base_absolut_url):
"offset": 0,
"limit": 10,
"stringId": 1,
+ "fileId": None,
"type": StringCommentType.COMMENT,
"issueType": "context_request,source_mistake",
"issueStatus": StringCommentIssueStatus.UNRESOLVED,
@@ -107,8 +122,10 @@ def test_list_string_comments(self, m_request, in_params, request_params, base_a
{
"text": "text",
"stringId": 1,
+ "fileId": None,
"targetLanguageId": "ua",
"type": StringCommentType.COMMENT,
+ "isShared": None,
"issueType": None,
"attachments": None,
},
@@ -125,10 +142,31 @@ def test_list_string_comments(self, m_request, in_params, request_params, base_a
{
"text": "text",
"stringId": 1,
+ "fileId": None,
"targetLanguageId": "ua",
"type": StringCommentType.COMMENT,
+ "isShared": None,
"issueType": StringCommentIssueType.CONTEXT_REQUEST,
- "attachments": [1, 2, 3],
+ "attachments": [{"id": 1}, {"id": 2}, {"id": 3}],
+ },
+ ),
+ (
+ {
+ "text": "text",
+ "fileId": 7,
+ "type": StringCommentType.ISSUE,
+ "isShared": True,
+ "attachments": [{"id": 1}],
+ },
+ {
+ "text": "text",
+ "stringId": None,
+ "fileId": 7,
+ "targetLanguageId": None,
+ "type": StringCommentType.ISSUE,
+ "isShared": True,
+ "issueType": None,
+ "attachments": [{"id": 1}],
},
),
),
diff --git a/crowdin_api/api_resources/string_corrections/resource.py b/crowdin_api/api_resources/string_corrections/resource.py
index 1e095b4f..63a32d9e 100644
--- a/crowdin_api/api_resources/string_corrections/resource.py
+++ b/crowdin_api/api_resources/string_corrections/resource.py
@@ -8,9 +8,10 @@
class StringCorrectionsResource(BaseResource):
"""
- Resource for String Corrections.
+ Resource for String Corrections (Crowdin Enterprise only).
- Use API to add or remove strings translations, approvals, and votes.
+ Use API to list, add, get, restore, and delete source string corrections
+ proposed during proofreading.
Link to documentation:
https://support.crowdin.com/developer/enterprise/api/v2/#tag/String-Corrections
@@ -46,10 +47,10 @@ def list_corrections(
"denormalizePlaceholders": convert_enum_to_string_if_exists(denormalize_placeholders)
}
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_string_corrections_path(project_id),
- params=params
+ params=params,
)
def add_correction(
diff --git a/crowdin_api/api_resources/string_corrections/tests/test_string_corrections_resource.py b/crowdin_api/api_resources/string_corrections/tests/test_string_corrections_resource.py
index 1b90c651..e0e4423f 100644
--- a/crowdin_api/api_resources/string_corrections/tests/test_string_corrections_resource.py
+++ b/crowdin_api/api_resources/string_corrections/tests/test_string_corrections_resource.py
@@ -74,6 +74,26 @@ def test_list_corrections(self, m_request, in_params, request_params, base_absol
params=request_params
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_corrections_fetch_all(self, m_request, base_absolut_url):
+ m_request.return_value = {"data": [{"data": {"id": 1}}]}
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.with_fetch_all().list_corrections(1, string_id=2) == {
+ "data": [{"data": {"id": 1}}]
+ }
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/corrections",
+ params={
+ "stringId": 2,
+ "limit": 500,
+ "offset": 0,
+ "orderBy": None,
+ "denormalizePlaceholders": None,
+ },
+ )
+
@pytest.mark.parametrize(
"in_params, request_params",
(
diff --git a/crowdin_api/api_resources/string_translations/enums.py b/crowdin_api/api_resources/string_translations/enums.py
index 2e63e0cd..d52f45f5 100644
--- a/crowdin_api/api_resources/string_translations/enums.py
+++ b/crowdin_api/api_resources/string_translations/enums.py
@@ -28,3 +28,17 @@ class ListStringTranslationsOrderBy(Enum):
TEXT = "text"
RATING = "rating"
CREATED_AT = "createdAt"
+
+
+class TranslationProvider(Enum):
+ TM = "tm"
+ GLOBAL_TM = "global_tm"
+ GOOGLE = "google"
+ MICROSOFT = "microsoft"
+ CROWDIN = "crowdin"
+ DEEPL = "deepl"
+ AMAZON = "amazon"
+ GOOGLE_AUTOML = "google_automl"
+ MODERNMT = "modernmt"
+ CUSTOM_MT = "custom_mt"
+ AI = "ai"
diff --git a/crowdin_api/api_resources/string_translations/resource.py b/crowdin_api/api_resources/string_translations/resource.py
index fbeef0c1..a97cc164 100644
--- a/crowdin_api/api_resources/string_translations/resource.py
+++ b/crowdin_api/api_resources/string_translations/resource.py
@@ -1,8 +1,8 @@
-from typing import Iterable, Optional
+from typing import Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.enums import DenormalizePlaceholders, PluralCategoryName
-from crowdin_api.api_resources.string_translations.enums import VoteMark
+from crowdin_api.api_resources.string_translations.enums import TranslationProvider, VoteMark
from crowdin_api.api_resources.string_translations.types import (
ApprovalBatchOpPatchRequest,
TranslationBatchOpPatchRequest
@@ -17,7 +17,7 @@ class StringTranslationsResource(BaseResource):
Use API to add or remove strings translations, approvals, and votes.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/String-Translations
+ https://support.crowdin.com/developer/api/v2/#tag/String-Translations
"""
def search_translations(
@@ -35,8 +35,8 @@ def search_translations(
Search Translations.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.translations.getMany
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.translations.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.translations.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.translations.getMany
"""
params = {
@@ -74,12 +74,18 @@ def list_translation_approvals(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ correctionId: Optional[int] = None,
):
"""
List Translation Approvals
+ `fileId` is available in file-based projects only, `correctionId` in Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.approvals.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.approvals.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.approvals.getMany
"""
projectId = projectId or self.get_project_id()
@@ -91,6 +97,7 @@ def list_translation_approvals(
"stringId": stringId,
"languageId": languageId,
"translationId": translationId,
+ "correctionId": correctionId,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
@@ -102,14 +109,20 @@ def list_translation_approvals(
def add_approval(
self,
- translationId: int,
+ translationId: Optional[int] = None,
projectId: Optional[int] = None,
+ correctionId: Optional[int] = None,
):
"""
Add Approval.
+ Pass either `translationId` or `correctionId` (Crowdin Enterprise only).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.approvals.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.approvals.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.approvals.post
"""
projectId = projectId or self.get_project_id()
@@ -117,20 +130,28 @@ def add_approval(
return self.requester.request(
method="post",
path=self.get_approvals_path(projectId=projectId),
- request_data={"translationId": translationId},
+ request_data={"translationId": translationId, "correctionId": correctionId},
)
- def remove_string_approvals(self, stringId: int, projectId: Optional[int] = None):
+ def remove_string_approvals(
+ self,
+ stringId: Optional[int] = None,
+ projectId: Optional[int] = None,
+ fileId: Optional[int] = None,
+ ):
"""
Remove String Approvals
- Link to documentaion:
+ Remove approvals of a string (`stringId`) or of all strings in a file
+ (`fileId`, file-based projects only). `stringId` is required in string-based projects.
+
+ Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/String-Translations/operation/api.projects.approvals.deleteMany
"""
projectId = projectId or self.get_project_id()
- params = {"stringId": stringId}
+ params = {"stringId": stringId, "fileId": fileId}
return self.requester.request(
method="delete", path=self.get_approvals_path(projectId=projectId), params=params
@@ -141,7 +162,7 @@ def get_approval(self, approvalId: int, projectId: Optional[int] = None):
Get Approval.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.approvals.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.approvals.get
"""
projectId = projectId or self.get_project_id()
@@ -153,10 +174,10 @@ def get_approval(self, approvalId: int, projectId: Optional[int] = None):
def remove_approval(self, approvalId: int, projectId: Optional[int] = None):
"""
- Remove Approvall.
+ Remove Approval.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.approvals.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.approvals.delete
"""
projectId = projectId or self.get_project_id()
@@ -182,12 +203,22 @@ def list_language_translations(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ approvedOnly: Optional[Union[bool, int]] = None,
+ passedWorkflow: Optional[Union[bool, int]] = None,
+ minApprovalCount: Optional[int] = None,
):
"""
List Language Translations
+ `fileId` and `directoryId` are available in file-based projects only.
+ `orderBy` and `approvedOnly` are available in Crowdin only;
+ `passedWorkflow` and `minApprovalCount` in Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.languages.translations.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.languages.translations.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.languages.translations.getMany
"""
projectId = projectId or self.get_project_id()
@@ -204,6 +235,9 @@ def list_language_translations(
"directoryId": directoryId,
"croql": croql,
"denormalizePlaceholders": denormalizePlaceholders,
+ "approvedOnly": None if approvedOnly is None else int(approvedOnly),
+ "passedWorkflow": None if passedWorkflow is None else int(passedWorkflow),
+ "minApprovalCount": minApprovalCount,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
@@ -224,7 +258,7 @@ def translation_alignment(
Translation Alignment
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.alignment.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.alignment.post
"""
projectId = projectId or self.get_project_id()
@@ -257,12 +291,15 @@ def list_string_translations(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ fileId: Optional[int] = None,
):
"""
List String Translations
+ `languageId` is required by the API. `fileId` is available in file-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.getMany
"""
projectId = projectId or self.get_project_id()
@@ -271,6 +308,7 @@ def list_string_translations(
"stringId": stringId,
"languageId": languageId,
"denormalizePlaceholders": denormalizePlaceholders,
+ "fileId": fileId,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
@@ -288,12 +326,17 @@ def add_translation(
projectId: Optional[int] = None,
pluralCategoryName: Optional[PluralCategoryName] = None,
addToTm: Optional[bool] = None,
+ provider: Optional[TranslationProvider] = None,
+ providerId: Optional[int] = None,
+ isPreTranslated: Optional[bool] = None,
):
"""
Add Translation.
+ `provider` is required when `providerId` or `isPreTranslated` is specified.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.post
"""
projectId = projectId or self.get_project_id()
@@ -306,36 +349,78 @@ def add_translation(
"text": text,
"pluralCategoryName": pluralCategoryName,
"addToTm": addToTm,
+ "provider": provider,
+ "providerId": providerId,
+ "isPreTranslated": isPreTranslated,
+ },
+ )
+
+ def add_file_translations(
+ self,
+ fileId: int,
+ languageId: str,
+ storageId: int,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Add Translation (upload translations of a file from storage).
+
+ File-based projects only.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.translations.post
+ """
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=self.get_translations_path(projectId=projectId),
+ request_data={
+ "fileId": fileId,
+ "languageId": languageId,
+ "storageId": storageId,
},
)
def delete_string_translations(
self,
- stringId: int,
+ stringId: Optional[int] = None,
languageId: Optional[str] = None,
projectId: Optional[int] = None,
+ fileId: Optional[int] = None,
):
"""
Delete String Translations.
+ Delete translations of a string (`stringId`) or of all strings in a file
+ (`fileId`, file-based projects only). `stringId` is required in string-based projects.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.deleteMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.deleteMany
"""
projectId = projectId or self.get_project_id()
return self.requester.request(
method="delete",
- params={"stringId": stringId, "languageId": languageId},
+ params={"stringId": stringId, "languageId": languageId, "fileId": fileId},
path=self.get_translations_path(projectId=projectId),
)
- def get_translation(self, translationId: int, projectId: Optional[int] = None):
+ def get_translation(
+ self,
+ translationId: int,
+ projectId: Optional[int] = None,
+ denormalizePlaceholders: Optional[DenormalizePlaceholders] = None,
+ ):
"""
Get Translation.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.get
"""
projectId = projectId or self.get_project_id()
@@ -343,6 +428,7 @@ def get_translation(self, translationId: int, projectId: Optional[int] = None):
return self.requester.request(
method="get",
path=self.get_translations_path(projectId=projectId, translationId=translationId),
+ params={"denormalizePlaceholders": denormalizePlaceholders},
)
def restore_translation(self, translationId: int, projectId: Optional[int] = None):
@@ -350,7 +436,7 @@ def restore_translation(self, translationId: int, projectId: Optional[int] = Non
Restore Translation.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.put
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.put
"""
projectId = projectId or self.get_project_id()
@@ -365,7 +451,7 @@ def delete_translation(self, translationId: int, projectId: Optional[int] = None
Delete Translation.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.delete
"""
projectId = projectId or self.get_project_id()
@@ -398,8 +484,10 @@ def list_translation_votes(
"""
List Translation Votes
+ `fileId` is available in file-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.votes.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.votes.getMany
"""
projectId = projectId or self.get_project_id()
@@ -427,7 +515,7 @@ def add_vote(
Add Vote.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.votes.pos
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.votes.post
"""
projectId = projectId or self.get_project_id()
@@ -443,7 +531,7 @@ def get_vote(self, voteId: int, projectId: Optional[int] = None):
Get Vote.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.votes.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.votes.get
"""
projectId = projectId or self.get_project_id()
@@ -458,7 +546,7 @@ def cancel_vote(self, voteId: int, projectId: Optional[int] = None):
Cancel Vote.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.votes.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.votes.delete
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/string_translations/tests/test_string_translations_resources.py b/crowdin_api/api_resources/string_translations/tests/test_string_translations_resources.py
index d4cd36ed..76405b2f 100644
--- a/crowdin_api/api_resources/string_translations/tests/test_string_translations_resources.py
+++ b/crowdin_api/api_resources/string_translations/tests/test_string_translations_resources.py
@@ -6,6 +6,7 @@
ListLanguageTranslationsOrderBy,
ListStringTranslationsOrderBy,
ListTranslationApprovalsOrderBy,
+ TranslationProvider,
VoteMark,
)
from crowdin_api.api_resources.string_translations.resource import StringTranslationsResource
@@ -103,6 +104,7 @@ def test_get_approvals_path(self, in_params, path, base_absolut_url):
"stringId": None,
"languageId": None,
"translationId": None,
+ "correctionId": None,
},
),
(
@@ -139,6 +141,22 @@ def test_get_approvals_path(self, in_params, path, base_absolut_url):
"stringId": 2,
"languageId": "ua",
"translationId": 3,
+ "correctionId": None,
+ },
+ ),
+ (
+ {"offset": 0, "limit": 10, "correctionId": 4},
+ {
+ "orderBy": None,
+ "offset": 0,
+ "limit": 10,
+ "fileId": None,
+ "labelIds": None,
+ "excludeLabelIds": None,
+ "stringId": None,
+ "languageId": None,
+ "translationId": None,
+ "correctionId": 4,
},
),
),
@@ -166,7 +184,19 @@ def test_add_approval(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="post",
path=resource.get_approvals_path(projectId=1),
- request_data={"translationId": 2},
+ request_data={"translationId": 2, "correctionId": None},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_approval_by_correction(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_approval(projectId=1, correctionId=3) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path=resource.get_approvals_path(projectId=1),
+ request_data={"translationId": None, "correctionId": 3},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -183,7 +213,19 @@ def test_remove_string_approvals(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="delete",
path=resource.get_approvals_path(projectId=projectId),
- params={"stringId": stringId},
+ params={"stringId": stringId, "fileId": None},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_remove_string_approvals_by_file(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.remove_string_approvals(projectId=2, fileId=5) == "response"
+ m_request.assert_called_once_with(
+ method="delete",
+ path=resource.get_approvals_path(projectId=2),
+ params={"stringId": None, "fileId": 5},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -223,6 +265,9 @@ def test_remove_approval(self, m_request, base_absolut_url):
"directoryId": None,
"croql": None,
"denormalizePlaceholders": None,
+ "approvedOnly": None,
+ "passedWorkflow": None,
+ "minApprovalCount": None,
},
),
(
@@ -263,6 +308,45 @@ def test_remove_approval(self, m_request, base_absolut_url):
"directoryId": 7,
"croql": "croql",
"denormalizePlaceholders": DenormalizePlaceholders.ENABLE,
+ "approvedOnly": None,
+ "passedWorkflow": None,
+ "minApprovalCount": None,
+ },
+ ),
+ (
+ {"offset": 0, "limit": 10, "approvedOnly": True},
+ {
+ "orderBy": None,
+ "offset": 0,
+ "limit": 10,
+ "stringIds": None,
+ "labelIds": None,
+ "fileId": None,
+ "branchId": None,
+ "directoryId": None,
+ "croql": None,
+ "denormalizePlaceholders": None,
+ "approvedOnly": 1,
+ "passedWorkflow": None,
+ "minApprovalCount": None,
+ },
+ ),
+ (
+ {"offset": 0, "limit": 10, "passedWorkflow": False, "minApprovalCount": 2},
+ {
+ "orderBy": None,
+ "offset": 0,
+ "limit": 10,
+ "stringIds": None,
+ "labelIds": None,
+ "fileId": None,
+ "branchId": None,
+ "directoryId": None,
+ "croql": None,
+ "denormalizePlaceholders": None,
+ "approvedOnly": None,
+ "passedWorkflow": 0,
+ "minApprovalCount": 2,
},
),
),
@@ -327,6 +411,7 @@ def test_get_translations_path(self, in_params, path, base_absolut_url):
"stringId": None,
"languageId": None,
"denormalizePlaceholders": None,
+ "fileId": None,
},
),
(
@@ -359,6 +444,19 @@ def test_get_translations_path(self, in_params, path, base_absolut_url):
"stringId": 1,
"languageId": 2,
"denormalizePlaceholders": DenormalizePlaceholders.ENABLE,
+ "fileId": None,
+ },
+ ),
+ (
+ {"offset": 0, "limit": 10, "languageId": "uk", "fileId": 3},
+ {
+ "orderBy": None,
+ "offset": 0,
+ "limit": 10,
+ "stringId": None,
+ "languageId": "uk",
+ "denormalizePlaceholders": None,
+ "fileId": 3,
},
),
),
@@ -390,6 +488,9 @@ def test_list_string_translations(self, m_request, in_params, request_params, ba
"text": "text",
"pluralCategoryName": None,
"addToTm": None,
+ "provider": None,
+ "providerId": None,
+ "isPreTranslated": None,
},
),
(
@@ -405,6 +506,30 @@ def test_list_string_translations(self, m_request, in_params, request_params, ba
"text": "text",
"pluralCategoryName": "some name",
"addToTm": None,
+ "provider": None,
+ "providerId": None,
+ "isPreTranslated": None,
+ },
+ ),
+ (
+ {
+ "stringId": 1,
+ "languageId": "ua",
+ "text": "text",
+ "addToTm": False,
+ "provider": TranslationProvider.AI,
+ "providerId": 5,
+ "isPreTranslated": True,
+ },
+ {
+ "stringId": 1,
+ "languageId": "ua",
+ "text": "text",
+ "pluralCategoryName": None,
+ "addToTm": False,
+ "provider": TranslationProvider.AI,
+ "providerId": 5,
+ "isPreTranslated": True,
},
),
),
@@ -432,10 +557,37 @@ def test_delete_string_translations(self, m_request, base_absolut_url):
)
m_request.assert_called_once_with(
method="delete",
- params={"stringId": 2, "languageId": "ua"},
+ params={"stringId": 2, "languageId": "ua", "fileId": None},
path=resource.get_translations_path(projectId=1),
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_string_translations_by_file(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_string_translations(projectId=1, fileId=3) == "response"
+ m_request.assert_called_once_with(
+ method="delete",
+ params={"stringId": None, "languageId": None, "fileId": 3},
+ path=resource.get_translations_path(projectId=1),
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_file_translations(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_file_translations(projectId=1, fileId=2, languageId="uk", storageId=3)
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path=resource.get_translations_path(projectId=1),
+ request_data={"fileId": 2, "languageId": "uk", "storageId": 3},
+ )
+
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_get_translation(self, m_request, base_absolut_url):
m_request.return_value = "response"
@@ -445,6 +597,26 @@ def test_get_translation(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="get",
path=resource.get_translations_path(projectId=1, translationId=2),
+ params={"denormalizePlaceholders": None},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_translation_denormalize_placeholders(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.get_translation(
+ projectId=1,
+ translationId=2,
+ denormalizePlaceholders=DenormalizePlaceholders.ENABLE,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path=resource.get_translations_path(projectId=1, translationId=2),
+ params={"denormalizePlaceholders": DenormalizePlaceholders.ENABLE},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
diff --git a/crowdin_api/api_resources/string_translations/types.py b/crowdin_api/api_resources/string_translations/types.py
index 52bc384f..e55033f9 100644
--- a/crowdin_api/api_resources/string_translations/types.py
+++ b/crowdin_api/api_resources/string_translations/types.py
@@ -1,4 +1,6 @@
-from typing import TypedDict, Any
+from typing import Any
+
+from crowdin_api.typing import TypedDict
from crowdin_api.api_resources.enums import PatchOperation
diff --git a/crowdin_api/api_resources/style_guides/enums.py b/crowdin_api/api_resources/style_guides/enums.py
index a789c7bf..609304c6 100644
--- a/crowdin_api/api_resources/style_guides/enums.py
+++ b/crowdin_api/api_resources/style_guides/enums.py
@@ -15,3 +15,4 @@ class StyleGuidePatchPath(Enum):
PROJECT_IDS = "/projectIds"
IS_SHARED = "/isShared"
STORAGE_ID = "/storageId"
+ GROUP_ID = "/groupId"
diff --git a/crowdin_api/api_resources/style_guides/resource.py b/crowdin_api/api_resources/style_guides/resource.py
index 5ff7ba01..c7da719f 100644
--- a/crowdin_api/api_resources/style_guides/resource.py
+++ b/crowdin_api/api_resources/style_guides/resource.py
@@ -50,6 +50,8 @@ def add_style_guide(self, request_data: AddStyleGuideRequest):
"""
Create Style Guide
+ Note: `groupId` in request data is supported by Crowdin Enterprise only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.post
"""
@@ -91,6 +93,8 @@ def edit_style_guide(
"""
Edit Style Guide
+ Note: the `/groupId` patch path is supported by Crowdin Enterprise only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#operation/api.style-guides.patch
"""
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
index d88ac603..26d6db0b 100644
--- 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
@@ -95,6 +95,7 @@ def test_list_style_guides(self, m_request, incoming_data, request_params, base_
languageIds=["uk", "fr", "de"],
projectIds=[1, 2, 3],
isShared=False,
+ groupId=2,
),
{
"name": "Be My Eyes iOS's Style Guide",
@@ -103,6 +104,7 @@ def test_list_style_guides(self, m_request, incoming_data, request_params, base_
"languageIds": ["uk", "fr", "de"],
"projectIds": [1, 2, 3],
"isShared": False,
+ "groupId": 2,
},
),
),
@@ -153,7 +155,12 @@ def test_edit_style_guide(self, m_request, base_absolut_url):
"op": PatchOperation.REPLACE,
"path": StyleGuidePatchPath.NAME,
"value": "Be My Eyes iOS's Style Guide",
- }
+ },
+ {
+ "op": PatchOperation.REPLACE,
+ "path": StyleGuidePatchPath.GROUP_ID,
+ "value": 2,
+ },
]
resource = self.get_resource(base_absolut_url)
diff --git a/crowdin_api/api_resources/style_guides/types.py b/crowdin_api/api_resources/style_guides/types.py
index a3078ef6..036038d0 100644
--- a/crowdin_api/api_resources/style_guides/types.py
+++ b/crowdin_api/api_resources/style_guides/types.py
@@ -12,6 +12,7 @@ class AddStyleGuideRequest(TypedDict):
languageIds: Optional[Iterable[str]]
projectIds: Optional[Iterable[int]]
isShared: Optional[bool]
+ groupId: Optional[int] # Enterprise only
class StyleGuidePatchRequest(TypedDict):
diff --git a/crowdin_api/api_resources/system_placeholders/__init__.py b/crowdin_api/api_resources/system_placeholders/__init__.py
new file mode 100644
index 00000000..f5406199
--- /dev/null
+++ b/crowdin_api/api_resources/system_placeholders/__init__.py
@@ -0,0 +1 @@
+__pdoc__ = {"tests": False}
diff --git a/crowdin_api/api_resources/system_placeholders/enums.py b/crowdin_api/api_resources/system_placeholders/enums.py
new file mode 100644
index 00000000..3d06efa7
--- /dev/null
+++ b/crowdin_api/api_resources/system_placeholders/enums.py
@@ -0,0 +1,37 @@
+from enum import Enum
+
+
+class SystemPlaceholderPatchPath(Enum):
+ """
+ The first segment is the key of the system placeholder, and `/isEnabled` is the only property
+ it accepts.
+ """
+
+ WRAPPED_AMPERSAND = "/wrappedAmpersand/isEnabled"
+ APPLE_STRINGSDICT_PLURAL = "/appleStringsdictPlural/isEnabled"
+ DOLLAR_INSIDE_BRACES = "/dollarInsideBraces/isEnabled"
+ DOLLAR_OUTSIDE_BRACES = "/dollarOutsideBraces/isEnabled"
+ WRAPPED_COLON = "/wrappedColon/isEnabled"
+ WRAPPED_DOLLAR = "/wrappedDollar/isEnabled"
+ DOLLAR_PARENTHESES = "/dollarParentheses/isEnabled"
+ I18NEXT_NESTING = "/i18nextNesting/isEnabled"
+ I18NEXT_LEGACY = "/i18nextLegacy/isEnabled"
+ RUBY_INTERPOLATION = "/rubyInterpolation/isEnabled"
+ MAILCHIMP_MERGE_TAG = "/mailchimpMergeTag/isEnabled"
+ SWIFT_INTERPOLATION = "/swiftInterpolation/isEnabled"
+ BRACES_TRIPLE = "/bracesTriple/isEnabled"
+ BRACES_DOUBLE = "/bracesDouble/isEnabled"
+ BRACES_SINGLE = "/bracesSingle/isEnabled"
+ BRACES_DOUBLE_FORMATTED = "/bracesDoubleFormatted/isEnabled"
+ DATE_TIME_PATTERN = "/dateTimePattern/isEnabled"
+ APPLE_STRING_CATALOG_NAMED = "/appleStringCatalogNamed/isEnabled"
+ PRINTF_SPECIFIER = "/printfSpecifier/isEnabled"
+ PYTHON_PERCENT_FORMAT = "/pythonPercentFormat/isEnabled"
+ RAILS_I18N = "/railsI18n/isEnabled"
+ JAVA_MESSAGE_FORMAT = "/javaMessageFormat/isEnabled"
+ DOT_NET_COMPOSITE_FORMAT = "/dotNetCompositeFormat/isEnabled"
+ TWIG = "/twig/isEnabled"
+ PHP_INTERPOLATION = "/phpInterpolation/isEnabled"
+ FREEMARKER_DIRECTIVE = "/freemarkerDirective/isEnabled"
+ WRAPPED_PERCENT = "/wrappedPercent/isEnabled"
+ DATE_TIME_SPECIFIER = "/dateTimeSpecifier/isEnabled"
diff --git a/crowdin_api/api_resources/system_placeholders/resource.py b/crowdin_api/api_resources/system_placeholders/resource.py
new file mode 100644
index 00000000..7827fb2a
--- /dev/null
+++ b/crowdin_api/api_resources/system_placeholders/resource.py
@@ -0,0 +1,59 @@
+from typing import Iterable, Optional
+
+from crowdin_api.api_resources.abstract.resources import BaseResource
+from crowdin_api.api_resources.system_placeholders.types import SystemPlaceholderPatchRequest
+
+
+class SystemPlaceholdersResource(BaseResource):
+ """
+ Resource for Project System Placeholders.
+
+ System placeholders are the placeholders Crowdin ships. Each project can turn them on and off.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#tag/Placeholders
+ """
+
+ def get_system_placeholders_path(self, projectId: int):
+ return f"projects/{projectId}/system-placeholders"
+
+ def list_system_placeholders(
+ self,
+ projectId: Optional[int] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
+ """
+ List Project System Placeholders.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.system-placeholders.getMany
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self._get_entire_data(
+ method="get",
+ path=self.get_system_placeholders_path(projectId=projectId),
+ params=self.get_page_params(limit=limit, offset=offset),
+ )
+
+ def system_placeholders_batch_operations(
+ self,
+ data: Iterable[SystemPlaceholderPatchRequest],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Batch Edit Project System Placeholders.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.system-placeholders.batchPatch
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="patch",
+ path=self.get_system_placeholders_path(projectId=projectId),
+ request_data=data,
+ )
diff --git a/crowdin_api/api_resources/system_placeholders/tests/test_system_placeholders_resources.py b/crowdin_api/api_resources/system_placeholders/tests/test_system_placeholders_resources.py
new file mode 100644
index 00000000..3da0ea44
--- /dev/null
+++ b/crowdin_api/api_resources/system_placeholders/tests/test_system_placeholders_resources.py
@@ -0,0 +1,78 @@
+from unittest import mock
+
+import pytest
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.system_placeholders.enums import SystemPlaceholderPatchPath
+from crowdin_api.api_resources.system_placeholders.resource import SystemPlaceholdersResource
+from crowdin_api.requester import APIRequester
+
+
+class TestSystemPlaceholdersResource:
+ resource_class = SystemPlaceholdersResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ def test_get_system_placeholders_path(self, base_absolut_url):
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_system_placeholders_path(projectId=1) == "projects/1/system-placeholders"
+
+ @pytest.mark.parametrize(
+ "in_params, request_params",
+ (
+ ({}, {"limit": 25, "offset": 0}),
+ ({"limit": 10, "offset": 2}, {"limit": 10, "offset": 2}),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_system_placeholders(
+ self, m_request, in_params, request_params, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_system_placeholders(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/system-placeholders",
+ params=request_params,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_system_placeholders_with_project_id(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.resource_class(
+ requester=APIRequester(base_url=base_absolut_url), project_id=3
+ )
+ assert resource.list_system_placeholders() == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/3/system-placeholders",
+ params={"limit": 25, "offset": 0},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_system_placeholders_batch_operations(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": SystemPlaceholderPatchPath.WRAPPED_AMPERSAND,
+ "value": False,
+ },
+ {
+ "op": PatchOperation.TEST,
+ "path": SystemPlaceholderPatchPath.PRINTF_SPECIFIER,
+ "value": True,
+ },
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.system_placeholders_batch_operations(projectId=1, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ path="projects/1/system-placeholders",
+ request_data=data,
+ )
diff --git a/crowdin_api/api_resources/system_placeholders/types.py b/crowdin_api/api_resources/system_placeholders/types.py
new file mode 100644
index 00000000..c86488ec
--- /dev/null
+++ b/crowdin_api/api_resources/system_placeholders/types.py
@@ -0,0 +1,13 @@
+from crowdin_api.api_resources.enums import PatchOperation
+from crowdin_api.api_resources.system_placeholders.enums import SystemPlaceholderPatchPath
+from crowdin_api.typing import TypedDict
+
+
+class SystemPlaceholderPatchRequest(TypedDict):
+ """
+ `op` accepts `replace` or `test` only.
+ """
+
+ op: PatchOperation
+ path: SystemPlaceholderPatchPath
+ value: bool
diff --git a/crowdin_api/api_resources/tasks/enums.py b/crowdin_api/api_resources/tasks/enums.py
index dbf8f6e2..71cacb31 100644
--- a/crowdin_api/api_resources/tasks/enums.py
+++ b/crowdin_api/api_resources/tasks/enums.py
@@ -2,18 +2,40 @@
class TaskOperationPatchPath(Enum):
+ """
+ Patch paths for Crowdin tasks.
+
+ `FILE_IDS`, `DIRECTORY_IDS` and `SPLIT_FILES` are file-based projects only.
+ `SPLIT_FILES` is deprecated, use `SPLIT_CONTENT` instead.
+ """
+
STATUS = "/status"
TITLE = "/title"
DESCRIPTION = "/description"
DEADLINE = "/deadline"
+ STARTED_AT = "/startedAt"
+ RESOLVED_AT = "/resolvedAt"
SPLIT_FILES = "/splitFiles"
+ SPLIT_CONTENT = "/splitContent"
FILE_IDS = "/fileIds"
+ DIRECTORY_IDS = "/directoryIds"
+ BRANCH_IDS = "/branchIds"
+ STRING_IDS = "/stringIds"
ASSIGNEES = "/assignees"
DATE_FROM = "/dateFrom"
DATE_TO = "/dateTo"
+ TRANSLATIONS_UPDATED_DATE_FROM = "/translationsUpdatedDateFrom"
+ TRANSLATIONS_UPDATED_DATE_TO = "/translationsUpdatedDateTo"
LABEL_IDS = "/labelIds"
- BATCH_ID = "/batchId"
+ LABEL_MATCH_RULE = "/labelMatchRule"
+ EXCLUDE_LABEL_IDS = "/excludeLabelIds"
+ EXCLUDE_LABEL_MATCH_RULE = "/excludeLabelMatchRule"
+ SKIP_ASSIGNED_STRINGS = "/skipAssignedStrings"
RESET_SCOPE = "/resetScope"
+ GENERATE_COST_ESTIMATE = "/generateCostEstimate"
+ GENERATE_TRANSLATION_COST = "/generateTranslationCost"
+ REPORT_SETTINGS_TEMPLATE_ID = "/reportSettingsTemplateId"
+ BATCH_ID = "/batchId"
class VendorTaskOperationPatchPath(Enum):
@@ -22,6 +44,126 @@ class VendorTaskOperationPatchPath(Enum):
status = "/status"
+class PendingTaskOperationPatchPath(Enum):
+ TITLE = "/title"
+ DESCRIPTION = "/description"
+ ASSIGNEES = "/assignees"
+ DEADLINE = "/deadline"
+
+
+class VendorPendingTaskOperationPatchPath(Enum):
+ TITLE = "/title"
+ DESCRIPTION = "/description"
+
+
+class EnterpriseTaskOperationPatchPath(Enum):
+ """
+ Patch paths for Crowdin Enterprise tasks.
+
+ `FILE_IDS`, `DIRECTORY_IDS` and `SPLIT_FILES` are file-based projects only.
+ `SPLIT_FILES` is deprecated, use `SPLIT_CONTENT` instead.
+ To edit a single field value use the `/fields/{fieldSlug}` path as a plain string.
+ """
+
+ STATUS = "/status"
+ TITLE = "/title"
+ DESCRIPTION = "/description"
+ DEADLINE = "/deadline"
+ STARTED_AT = "/startedAt"
+ RESOLVED_AT = "/resolvedAt"
+ SPLIT_FILES = "/splitFiles"
+ SPLIT_CONTENT = "/splitContent"
+ FILE_IDS = "/fileIds"
+ DIRECTORY_IDS = "/directoryIds"
+ BRANCH_IDS = "/branchIds"
+ STRING_IDS = "/stringIds"
+ ASSIGNEES = "/assignees"
+ ASSIGNED_TEAMS = "/assignedTeams"
+ DATE_FROM = "/dateFrom"
+ DATE_TO = "/dateTo"
+ TRANSLATIONS_UPDATED_DATE_FROM = "/translationsUpdatedDateFrom"
+ TRANSLATIONS_UPDATED_DATE_TO = "/translationsUpdatedDateTo"
+ LABEL_IDS = "/labelIds"
+ LABEL_MATCH_RULE = "/labelMatchRule"
+ EXCLUDE_LABEL_IDS = "/excludeLabelIds"
+ EXCLUDE_LABEL_MATCH_RULE = "/excludeLabelMatchRule"
+ SKIP_ASSIGNED_STRINGS = "/skipAssignedStrings"
+ SKIP_ASSIGNED_STRINGS_SCOPE = "/skipAssignedStringsScope"
+ FIELDS = "/fields"
+ RESET_SCOPE = "/resetScope"
+ GENERATE_COST_ESTIMATE = "/generateCostEstimate"
+ GENERATE_TRANSLATION_COST = "/generateTranslationCost"
+ REPORT_SETTINGS_TEMPLATE_ID = "/reportSettingsTemplateId"
+ BATCH_ID = "/batchId"
+
+
+class EnterpriseVendorTaskOperationPatchPath(Enum):
+ """
+ Patch paths for Crowdin Enterprise vendor tasks.
+
+ `FILE_IDS` is file-based projects only.
+ """
+
+ TITLE = "/title"
+ DESCRIPTION = "/description"
+ FILE_IDS = "/fileIds"
+ STRING_IDS = "/stringIds"
+ DATE_FROM = "/dateFrom"
+ DATE_TO = "/dateTo"
+ TRANSLATIONS_UPDATED_DATE_FROM = "/translationsUpdatedDateFrom"
+ TRANSLATIONS_UPDATED_DATE_TO = "/translationsUpdatedDateTo"
+ DEADLINE = "/deadline"
+ STARTED_AT = "/startedAt"
+ RESOLVED_AT = "/resolvedAt"
+ LABEL_IDS = "/labelIds"
+ LABEL_MATCH_RULE = "/labelMatchRule"
+ EXCLUDE_LABEL_IDS = "/excludeLabelIds"
+ EXCLUDE_LABEL_MATCH_RULE = "/excludeLabelMatchRule"
+ GENERATE_COST_ESTIMATE = "/generateCostEstimate"
+ GENERATE_TRANSLATION_COST = "/generateTranslationCost"
+ REPORT_SETTINGS_TEMPLATE_ID = "/reportSettingsTemplateId"
+
+
+class EnterpriseInterOrganizationalTaskOperationPatchPath(Enum):
+ """
+ Patch paths for Crowdin Enterprise inter-organizational tasks.
+
+ `SPLIT_FILES` is file-based projects only and deprecated, use `SPLIT_CONTENT` instead.
+ """
+
+ ASSIGNEE = "/assignee"
+ ASSIGNED_TEAMS = "/assignedTeams"
+ SPLIT_FILES = "/splitFiles"
+ SPLIT_CONTENT = "/splitContent"
+ STATUS = "/status"
+ GENERATE_COST_ESTIMATE = "/generateCostEstimate"
+ GENERATE_TRANSLATION_COST = "/generateTranslationCost"
+ REPORT_SETTINGS_TEMPLATE_ID = "/reportSettingsTemplateId"
+
+
+class EnterprisePendingTaskOperationPatchPath(Enum):
+ TITLE = "/title"
+ DESCRIPTION = "/description"
+ ASSIGNEES = "/assignees"
+ ASSIGNED_TEAMS = "/assignedTeams"
+ DEADLINE = "/deadline"
+
+
+class TaskCommentPatchPath(Enum):
+ TEXT = "/text"
+ TIME_SPENT = "/timeSpent"
+
+
+class TaskLabelMatchRule(Enum):
+ ALL = "all"
+ ANY = "any"
+
+
+class TaskSkipAssignedStringsScope(Enum):
+ ALL = "all"
+ SAME_WORKFLOW_STEP = "sameWorkflowStep"
+
+
class ConfigTaskOperationPatchPath(Enum):
NAME = "/name"
CONFIG = "/config"
diff --git a/crowdin_api/api_resources/tasks/resource.py b/crowdin_api/api_resources/tasks/resource.py
index 49c2c3ce..60cb117e 100644
--- a/crowdin_api/api_resources/tasks/resource.py
+++ b/crowdin_api/api_resources/tasks/resource.py
@@ -1,22 +1,31 @@
from datetime import datetime
-from typing import Dict, Iterable, Optional, Union
+from typing import Any, Dict, Iterable, Optional, Union
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.tasks.enums import (
CrowdinGeneralTaskType,
CrowdinTaskStatus,
CrowdinTaskType,
+ TaskLabelMatchRule,
+ TaskSkipAssignedStringsScope,
)
from crowdin_api.api_resources.tasks.types import (
CrowdinTaskAssignee,
EnterpriseTaskAssignedTeams,
TaskPatchRequest,
VendorPatchRequest,
+ PendingTaskPatchRequest,
+ VendorPendingTaskPatchRequest,
+ EnterpriseTaskPatchRequest,
+ EnterpriseVendorTaskPatchRequest,
+ EnterpriseInterOrganizationalTaskPatchRequest,
+ EnterprisePendingTaskPatchRequest,
ConfigPatchRequest,
EnterpriseTaskSettingsTemplateLanguages,
TaskSettingsTemplateLanguages,
)
from crowdin_api.sorting import Sorting
+from crowdin_api.utils import convert_to_query_list
from crowdin_api.api_resources.tasks.types import TaskCommentPatchRequest
@@ -31,7 +40,7 @@ class TasksResource(BaseResource):
Use API to create, modify, and delete specific tasks.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Tasks
+ https://support.crowdin.com/developer/api/v2/#tag/Tasks
"""
def get_task_settings_templates_path(
self, projectId: int, taskSettingsTemplateId: Optional[int] = None
@@ -52,10 +61,10 @@ def list_task_settings_templates(
List Task Settings Templates.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.settings-templates.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.settings-templates.getMany
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.getMany
"""
projectId = projectId or self.get_project_id()
@@ -77,7 +86,7 @@ def add_task_settings_template(
Add Task Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.settings-templates.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.settings-templates.post
"""
projectId = projectId or self.get_project_id()
@@ -95,10 +104,10 @@ def get_task_settings_template(
Get Task Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.settings-templates.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.settings-templates.get
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.get
"""
projectId = projectId or self.get_project_id()
@@ -117,10 +126,10 @@ def delete_task_settings_template(
Delete Task Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.settings-templates.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.settings-templates.delete
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.delete
"""
projectId = projectId or self.get_project_id()
@@ -143,10 +152,10 @@ def edit_task_settings_template(
Edit Task Settings Template.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.settings-templates.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.settings-templates.patch
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.patch
"""
projectId = projectId or self.get_project_id()
@@ -171,7 +180,7 @@ def list_tasks(
projectId: Optional[int] = None,
orderBy: Optional[Sorting] = None,
assigneeId: Optional[int] = None,
- status: Optional[CrowdinTaskStatus] = None,
+ status: Optional[Union[CrowdinTaskStatus, Iterable[CrowdinTaskStatus]]] = None,
batchId: Optional[int] = None,
page: Optional[int] = None,
offset: Optional[int] = None,
@@ -180,18 +189,21 @@ def list_tasks(
"""
List Tasks.
+ `status` can be a single status or a list of statuses.
+ `assigneeId` is Crowdin only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.getMany
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.getMany
"""
projectId = projectId or self.get_project_id()
params = {
"orderBy": orderBy,
"assigneeId": assigneeId,
- "status": status,
+ "status": convert_to_query_list(status),
"batchId": batchId,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
@@ -207,7 +219,7 @@ def add_task(self, request_data: Dict, projectId: Optional[int] = None):
Add Task.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -222,7 +234,7 @@ def add_general_task(
self,
title: str,
languageId: str,
- fileIds: Iterable[int],
+ fileIds: Optional[Iterable[int]],
type: CrowdinGeneralTaskType,
projectId: Optional[int] = None,
status: Optional[CrowdinTaskStatus] = None,
@@ -238,12 +250,25 @@ def add_general_task(
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
batchId: Optional[int] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ branchIds: Optional[Iterable[int]] = None,
+ labelMatchRule: Optional[TaskLabelMatchRule] = None,
+ excludeLabelMatchRule: Optional[TaskLabelMatchRule] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
):
"""
- Add Task(Crowdin Task Create Form).
+ Add Task(Crowdin Task Create Form, Create By Source Ids Form).
+
+ One of `fileIds`, `directoryIds` or `branchIds` is required (pass `fileIds=None` when
+ using `directoryIds` or `branchIds`). `fileIds` and `directoryIds` are file-based
+ projects only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -268,6 +293,15 @@ def add_general_task(
"dateFrom": dateFrom,
"dateTo": dateTo,
"batchId": batchId,
+ "directoryIds": directoryIds,
+ "branchIds": branchIds,
+ "labelMatchRule": labelMatchRule,
+ "excludeLabelMatchRule": excludeLabelMatchRule,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
},
)
@@ -289,12 +323,17 @@ def add_general_by_string_ids_task(
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
batchId: Optional[int] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
):
"""
- Add Task(Crowdin Task Create Form).
+ Add Task(Crowdin Task Create Form, Create By String Ids Form).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -317,6 +356,11 @@ def add_general_by_string_ids_task(
"dateFrom": dateFrom,
"dateTo": dateTo,
"batchId": batchId,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
},
)
@@ -324,7 +368,7 @@ def add_vendor_task(
self,
title: str,
languageId: str,
- fileIds: Iterable[int],
+ fileIds: Optional[Iterable[int]],
type: CrowdinTaskType,
vendor: str,
projectId: Optional[int] = None,
@@ -337,12 +381,25 @@ def add_vendor_task(
deadline: Optional[datetime] = None,
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ branchIds: Optional[Iterable[int]] = None,
+ labelMatchRule: Optional[TaskLabelMatchRule] = None,
+ excludeLabelMatchRule: Optional[TaskLabelMatchRule] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
):
"""
- Add Task(Crowdin Vendor Task Create Form).
+ Add Task(Crowdin Vendor Task Create Form, Create By Source Ids Form).
+
+ One of `fileIds`, `directoryIds` or `branchIds` is required (pass `fileIds=None` when
+ using `directoryIds` or `branchIds`). `fileIds` and `directoryIds` are file-based
+ projects only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -364,6 +421,15 @@ def add_vendor_task(
"deadline": deadline,
"dateFrom": dateFrom,
"dateTo": dateTo,
+ "directoryIds": directoryIds,
+ "branchIds": branchIds,
+ "labelMatchRule": labelMatchRule,
+ "excludeLabelMatchRule": excludeLabelMatchRule,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
},
)
@@ -382,12 +448,21 @@ def add_vendor_by_string_ids_task(
deadline: Optional[datetime] = None,
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ labelIds: Optional[Iterable[int]] = None,
+ labelMatchRule: Optional[TaskLabelMatchRule] = None,
+ excludeLabelIds: Optional[Iterable[int]] = None,
+ excludeLabelMatchRule: Optional[TaskLabelMatchRule] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
):
"""
- Add Task(Crowdin Vendor Task Create Form).
+ Add Task(Crowdin Vendor Task Create Form, Create By String Ids Form).
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -407,6 +482,15 @@ def add_vendor_by_string_ids_task(
"deadline": deadline,
"dateFrom": dateFrom,
"dateTo": dateTo,
+ "labelIds": labelIds,
+ "labelMatchRule": labelMatchRule,
+ "excludeLabelIds": excludeLabelIds,
+ "excludeLabelMatchRule": excludeLabelMatchRule,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
},
)
@@ -421,6 +505,9 @@ def add_pending_task(
):
"""
Add Task(Crowdin Pending Task Create Form).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -449,6 +536,9 @@ def add_vendor_pending_task(
):
"""
Add Task(Crowdin Vendor Pending Task Create Form).
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -471,7 +561,7 @@ def export_task_strings(self, taskId: int, projectId: Optional[int] = None):
Export Task Strings.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.exports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.exports.post
"""
projectId = projectId or self.get_project_id()
@@ -486,7 +576,7 @@ def get_task(self, taskId: int, projectId: Optional[int] = None):
Get Task.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.get
"""
projectId = projectId or self.get_project_id()
@@ -500,7 +590,7 @@ def delete_task(self, taskId: int, projectId: Optional[int] = None):
Delete Task.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.delete
"""
projectId = projectId or self.get_project_id()
@@ -513,14 +603,21 @@ def delete_task(self, taskId: int, projectId: Optional[int] = None):
def edit_task(
self,
taskId: int,
- data: Union[Iterable[VendorPatchRequest], Iterable[TaskPatchRequest]],
+ data: Union[
+ Iterable[VendorPatchRequest],
+ Iterable[TaskPatchRequest],
+ Iterable[PendingTaskPatchRequest],
+ Iterable[VendorPendingTaskPatchRequest],
+ ],
projectId: Optional[int] = None,
):
"""
Edit Task.
+ The `/splitFiles` patch path is deprecated, use `/splitContent` instead.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.patch
"""
projectId = projectId or self.get_project_id()
@@ -534,20 +631,25 @@ def edit_task(
def list_user_tasks(
self,
orderBy: Optional[Sorting] = None,
- status: Optional[CrowdinTaskStatus] = None,
+ status: Optional[Union[CrowdinTaskStatus, Iterable[CrowdinTaskStatus]]] = None,
isArchived: Optional[bool] = None,
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
):
"""
- List Tasks.
+ List User Tasks (tasks of the authorized user).
+
+ `status` can be a single status or a list of statuses.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.user.tasks.getMany
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.user.tasks.getMany
"""
- params = {"orderBy": orderBy, "status": status}
+ params = {"orderBy": orderBy, "status": convert_to_query_list(status)}
if isArchived is not None:
params["isArchived"] = 1 if isArchived else 0
@@ -556,6 +658,82 @@ def list_user_tasks(
return self._get_entire_data(method="get", path="user/tasks", params=params)
+ def _get_tasks_filter_params(
+ self,
+ orderBy: Optional[Sorting] = None,
+ status: Optional[Union[CrowdinTaskStatus, Iterable[CrowdinTaskStatus]]] = None,
+ type: Optional[Union[CrowdinTaskType, Iterable[CrowdinTaskType]]] = None,
+ projectIds: Optional[Iterable[int]] = None,
+ assigneeIds: Optional[Iterable[int]] = None,
+ creatorIds: Optional[Iterable[int]] = None,
+ targetLanguageIds: Optional[Iterable[str]] = None,
+ sourceLanguageIds: Optional[Iterable[str]] = None,
+ createdAtFrom: Optional[datetime] = None,
+ createdAtTo: Optional[datetime] = None,
+ deadlineFrom: Optional[datetime] = None,
+ deadlineTo: Optional[datetime] = None,
+ ) -> Dict:
+ return {
+ "orderBy": orderBy,
+ "status": convert_to_query_list(status),
+ "type": convert_to_query_list(type),
+ "projectIds": convert_to_query_list(projectIds),
+ "assigneeIds": convert_to_query_list(assigneeIds),
+ "creatorIds": convert_to_query_list(creatorIds),
+ "targetLanguageIds": convert_to_query_list(targetLanguageIds),
+ "sourceLanguageIds": convert_to_query_list(sourceLanguageIds),
+ "createdAtFrom": createdAtFrom,
+ "createdAtTo": createdAtTo,
+ "deadlineFrom": deadlineFrom,
+ "deadlineTo": deadlineTo,
+ }
+
+ def list_specific_user_tasks(
+ self,
+ userId: int,
+ orderBy: Optional[Sorting] = None,
+ status: Optional[Union[CrowdinTaskStatus, Iterable[CrowdinTaskStatus]]] = None,
+ type: Optional[Union[CrowdinTaskType, Iterable[CrowdinTaskType]]] = None,
+ projectIds: Optional[Iterable[int]] = None,
+ assigneeIds: Optional[Iterable[int]] = None,
+ creatorIds: Optional[Iterable[int]] = None,
+ targetLanguageIds: Optional[Iterable[str]] = None,
+ sourceLanguageIds: Optional[Iterable[str]] = None,
+ createdAtFrom: Optional[datetime] = None,
+ createdAtTo: Optional[datetime] = None,
+ deadlineFrom: Optional[datetime] = None,
+ deadlineTo: Optional[datetime] = None,
+ page: Optional[int] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List User Tasks (all of the specified user's project tasks).
+
+ Crowdin only. `status` and `type` can be a single value or a list of values.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.users.tasks.getMany
+ """
+
+ params = self._get_tasks_filter_params(
+ orderBy=orderBy,
+ status=status,
+ type=type,
+ projectIds=projectIds,
+ assigneeIds=assigneeIds,
+ creatorIds=creatorIds,
+ targetLanguageIds=targetLanguageIds,
+ sourceLanguageIds=sourceLanguageIds,
+ createdAtFrom=createdAtFrom,
+ createdAtTo=createdAtTo,
+ deadlineFrom=deadlineFrom,
+ deadlineTo=deadlineTo,
+ )
+ params.update(self.get_page_params(page=page, offset=offset, limit=limit))
+
+ return self._get_entire_data(method="get", path=f"users/{userId}/tasks", params=params)
+
# Task Comments
def get_task_comments_path(
self,
@@ -581,7 +759,7 @@ def list_task_comments(
List Task Comments.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.comments.getMany
"""
projectId = projectId or self.get_project_id()
@@ -599,12 +777,15 @@ def add_task_comment(
text: str,
taskId: int,
projectId: Optional[int] = None,
+ timeSpent: Optional[int] = None,
):
"""
Add Task Comment.
+ `timeSpent` is the time spent on the task, in seconds.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.comments.post
"""
projectId = projectId or self.get_project_id()
@@ -612,7 +793,7 @@ def add_task_comment(
return self.requester.request(
method="post",
path=self.get_task_comments_path(projectId=projectId, taskId=taskId),
- request_data={"text": text},
+ request_data={"text": text, "timeSpent": timeSpent},
)
def get_task_comment(
@@ -625,7 +806,7 @@ def get_task_comment(
Get Task Comment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.comments.get
"""
projectId = projectId or self.get_project_id()
@@ -647,7 +828,7 @@ def delete_task_comment(
Delete Task Comment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.comments.delete
"""
projectId = projectId or self.get_project_id()
@@ -669,8 +850,10 @@ def edit_task_comment(
"""
Edit Task Comment.
+ Supported patch paths: `/text` and `/timeSpent` (see `TaskCommentPatchPath`).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tasks.comments.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tasks.comments.patch
"""
projectId = projectId or self.get_project_id()
@@ -690,7 +873,7 @@ def edit_task_archived_status(
Edit Task Archived Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.user.tasks.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.user.tasks.patch
"""
projectId = projectId or self.get_project_id()
@@ -714,7 +897,7 @@ class EnterpriseTasksResource(TasksResource):
Use API to create, modify, and delete specific tasks.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Tasks
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Tasks
"""
def add_task_settings_template(
@@ -727,7 +910,7 @@ def add_task_settings_template(
Add Task Settings Template.
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.settings-templates.post
"""
projectId = projectId or self.get_project_id()
@@ -742,8 +925,8 @@ def add_general_task(
self,
title: str,
languageId: str,
- fileIds: Iterable[int],
- type: CrowdinGeneralTaskType,
+ fileIds: Optional[Iterable[int]],
+ type: Optional[CrowdinGeneralTaskType],
workflowStepId: Optional[int] = None,
projectId: Optional[int] = None,
status: Optional[CrowdinTaskStatus] = None,
@@ -760,12 +943,28 @@ def add_general_task(
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
batchId: Optional[int] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ branchIds: Optional[Iterable[int]] = None,
+ labelMatchRule: Optional[TaskLabelMatchRule] = None,
+ excludeLabelMatchRule: Optional[TaskLabelMatchRule] = None,
+ skipAssignedStringsScope: Optional[TaskSkipAssignedStringsScope] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
- Add Task(Enterprise Task Create Form).
+ Add Task(Enterprise Task Create Form, Create By Source Ids Form).
+
+ One of `fileIds`, `directoryIds` or `branchIds` is required (pass `fileIds=None` when
+ using `directoryIds` or `branchIds`). `fileIds` and `directoryIds` are file-based
+ projects only. One of `type` or `workflowStepId` is required (pass `type=None` when
+ using `workflowStepId`). `status` accepts `todo` or `in_progress`.
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -792,6 +991,17 @@ def add_general_task(
"dateFrom": dateFrom,
"dateTo": dateTo,
"batchId": batchId,
+ "directoryIds": directoryIds,
+ "branchIds": branchIds,
+ "labelMatchRule": labelMatchRule,
+ "excludeLabelMatchRule": excludeLabelMatchRule,
+ "skipAssignedStringsScope": skipAssignedStringsScope,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
+ "fields": fields,
},
)
@@ -815,12 +1025,21 @@ def add_general_by_string_ids_task(
dateFrom: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
batchId: Optional[int] = None,
+ skipAssignedStringsScope: Optional[TaskSkipAssignedStringsScope] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
- Add Task(Enterprise Task Create Form).
+ Add Task(Enterprise Task Create Form, Create By String Ids Form).
+
+ One of `type` or `workflowStepId` is required. `status` accepts `todo` or `in_progress`.
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -845,6 +1064,13 @@ def add_general_by_string_ids_task(
"dateFrom": dateFrom,
"dateTo": dateTo,
"batchId": batchId,
+ "skipAssignedStringsScope": skipAssignedStringsScope,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
+ "fields": fields,
},
)
@@ -853,7 +1079,7 @@ def add_vendor_task(
title: str,
languageId: str,
workflowStepId: int,
- fileIds: Iterable[int],
+ fileIds: Optional[Iterable[int]],
projectId: Optional[int] = None,
description: Optional[str] = None,
skipAssignedStrings: Optional[bool] = None,
@@ -863,12 +1089,27 @@ def add_vendor_task(
deadline: Optional[datetime] = None,
startedAt: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ branchIds: Optional[Iterable[int]] = None,
+ labelMatchRule: Optional[TaskLabelMatchRule] = None,
+ excludeLabelMatchRule: Optional[TaskLabelMatchRule] = None,
+ skipAssignedStringsScope: Optional[TaskSkipAssignedStringsScope] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
- Add Task(Enterprise Vendor Task Create Form).
+ Add Task(Enterprise Vendor Task Create Form, Create By Source Ids Form).
+
+ One of `fileIds`, `directoryIds` or `branchIds` is required (pass `fileIds=None` when
+ using `directoryIds` or `branchIds`). `fileIds` and `directoryIds` are file-based
+ projects only.
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -888,6 +1129,17 @@ def add_vendor_task(
"deadline": deadline,
"startedAt": startedAt,
"dateTo": dateTo,
+ "directoryIds": directoryIds,
+ "branchIds": branchIds,
+ "labelMatchRule": labelMatchRule,
+ "excludeLabelMatchRule": excludeLabelMatchRule,
+ "skipAssignedStringsScope": skipAssignedStringsScope,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
+ "fields": fields,
},
)
@@ -904,12 +1156,21 @@ def add_vendor_by_string_ids_task(
deadline: Optional[datetime] = None,
startedAt: Optional[datetime] = None,
dateTo: Optional[datetime] = None,
+ labelIds: Optional[Iterable[int]] = None,
+ labelMatchRule: Optional[TaskLabelMatchRule] = None,
+ skipAssignedStringsScope: Optional[TaskSkipAssignedStringsScope] = None,
+ translationsUpdatedDateFrom: Optional[datetime] = None,
+ translationsUpdatedDateTo: Optional[datetime] = None,
+ generateCostEstimate: Optional[bool] = None,
+ generateTranslationCost: Optional[bool] = None,
+ reportSettingsTemplateId: Optional[int] = None,
+ fields: Optional[Dict[str, Any]] = None,
):
"""
- Add Task(Enterprise Vendor Task Create Form).
+ Add Task(Enterprise Vendor Task Create Form, Create By String Ids Form).
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
@@ -927,6 +1188,15 @@ def add_vendor_by_string_ids_task(
"deadline": deadline,
"startedAt": startedAt,
"dateTo": dateTo,
+ "labelIds": labelIds,
+ "labelMatchRule": labelMatchRule,
+ "skipAssignedStringsScope": skipAssignedStringsScope,
+ "translationsUpdatedDateFrom": translationsUpdatedDateFrom,
+ "translationsUpdatedDateTo": translationsUpdatedDateTo,
+ "generateCostEstimate": generateCostEstimate,
+ "generateTranslationCost": generateTranslationCost,
+ "reportSettingsTemplateId": reportSettingsTemplateId,
+ "fields": fields,
},
)
@@ -939,21 +1209,33 @@ def add_pending_task(
assignees: Optional[Iterable[CrowdinTaskAssignee]] = None,
assignedTeams: Optional[Iterable[EnterpriseTaskAssignedTeams]] = None,
deadline: Optional[datetime] = None,
+ type: Optional[CrowdinTaskType] = None,
+ workflowStepId: Optional[int] = None,
+ vendor: Optional[str] = None,
):
"""
Add Task(Enterprise Pending Task Create Form).
+ One of `type` or `workflowStepId` can be provided, not both. When neither is given,
+ `type` defaults to `CrowdinTaskType.PROOFREAD`. `vendor` is required for
+ `CrowdinTaskType.PROOFREAD_BY_VENDOR`.
+
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.tasks.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.post
"""
projectId = projectId or self.get_project_id()
+ if type is None and workflowStepId is None:
+ type = CrowdinTaskType.PROOFREAD
+
return self.add_task(
projectId=projectId,
request_data={
"precedingTaskId": precedingTaskId,
- "type": CrowdinTaskType.PROOFREAD,
+ "type": type,
+ "workflowStepId": workflowStepId,
+ "vendor": vendor,
"title": title,
"description": description,
"assignees": assignees,
@@ -961,3 +1243,73 @@ def add_pending_task(
"deadline": deadline,
},
)
+
+ def edit_task(
+ self,
+ taskId: int,
+ data: Union[
+ Iterable[EnterpriseTaskPatchRequest],
+ Iterable[EnterpriseVendorTaskPatchRequest],
+ Iterable[EnterpriseInterOrganizationalTaskPatchRequest],
+ Iterable[EnterprisePendingTaskPatchRequest],
+ ],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Edit Task.
+
+ The `/splitFiles` patch path is deprecated, use `/splitContent` instead.
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.tasks.patch
+ """
+
+ return super().edit_task(taskId=taskId, data=data, projectId=projectId)
+
+ def list_organization_tasks(
+ self,
+ orderBy: Optional[Sorting] = None,
+ status: Optional[Union[CrowdinTaskStatus, Iterable[CrowdinTaskStatus]]] = None,
+ type: Optional[Union[CrowdinTaskType, Iterable[CrowdinTaskType]]] = None,
+ projectIds: Optional[Iterable[int]] = None,
+ groupIds: Optional[Iterable[int]] = None,
+ assigneeIds: Optional[Iterable[int]] = None,
+ creatorIds: Optional[Iterable[int]] = None,
+ targetLanguageIds: Optional[Iterable[str]] = None,
+ sourceLanguageIds: Optional[Iterable[str]] = None,
+ createdAtFrom: Optional[datetime] = None,
+ createdAtTo: Optional[datetime] = None,
+ deadlineFrom: Optional[datetime] = None,
+ deadlineTo: Optional[datetime] = None,
+ page: Optional[int] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List Tasks (organization level).
+
+ `status` and `type` can be a single value or a list of values.
+ `projectIds` cannot be used together with `groupIds`.
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.tasks.getMany
+ """
+
+ params = self._get_tasks_filter_params(
+ orderBy=orderBy,
+ status=status,
+ type=type,
+ projectIds=projectIds,
+ assigneeIds=assigneeIds,
+ creatorIds=creatorIds,
+ targetLanguageIds=targetLanguageIds,
+ sourceLanguageIds=sourceLanguageIds,
+ createdAtFrom=createdAtFrom,
+ createdAtTo=createdAtTo,
+ deadlineFrom=deadlineFrom,
+ deadlineTo=deadlineTo,
+ )
+ params["groupIds"] = convert_to_query_list(groupIds)
+ params.update(self.get_page_params(page=page, offset=offset, limit=limit))
+
+ return self._get_entire_data(method="get", path="tasks", params=params)
diff --git a/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py b/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py
index 15cef7a3..b55a36c2 100644
--- a/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py
+++ b/crowdin_api/api_resources/tasks/tests/test_tasks_resources.py
@@ -11,12 +11,57 @@
ListUserTasksOrderBy,
TaskOperationPatchPath,
ConfigTaskOperationPatchPath,
+ PendingTaskOperationPatchPath,
+ VendorPendingTaskOperationPatchPath,
+ EnterpriseTaskOperationPatchPath,
+ EnterpriseVendorTaskOperationPatchPath,
+ EnterpriseInterOrganizationalTaskOperationPatchPath,
+ EnterprisePendingTaskOperationPatchPath,
+ TaskCommentPatchPath,
+ TaskLabelMatchRule,
+ TaskSkipAssignedStringsScope,
)
from crowdin_api.api_resources.tasks.resource import TasksResource, EnterpriseTasksResource
from crowdin_api.requester import APIRequester
from crowdin_api.sorting import Sorting, SortingOrder, SortingRule
+COST_FIELDS_NEW = {
+ "translationsUpdatedDateFrom": None,
+ "translationsUpdatedDateTo": None,
+ "generateCostEstimate": None,
+ "generateTranslationCost": None,
+ "reportSettingsTemplateId": None,
+}
+C_GENERAL_NEW = {
+ "directoryIds": None,
+ "branchIds": None,
+ "labelMatchRule": None,
+ "excludeLabelMatchRule": None,
+ **COST_FIELDS_NEW,
+}
+C_GENERAL_STRING_NEW = {**COST_FIELDS_NEW}
+C_VENDOR_NEW = {**C_GENERAL_NEW}
+C_VENDOR_STRING_NEW = {
+ "labelIds": None,
+ "labelMatchRule": None,
+ "excludeLabelIds": None,
+ "excludeLabelMatchRule": None,
+ **COST_FIELDS_NEW,
+}
+E_GENERAL_NEW = {**C_GENERAL_NEW, "skipAssignedStringsScope": None, "fields": None}
+E_GENERAL_STRING_NEW = {**C_GENERAL_STRING_NEW, "skipAssignedStringsScope": None, "fields": None}
+E_VENDOR_NEW = {**E_GENERAL_NEW}
+E_VENDOR_STRING_NEW = {
+ "labelIds": None,
+ "labelMatchRule": None,
+ "skipAssignedStringsScope": None,
+ **COST_FIELDS_NEW,
+ "fields": None,
+}
+E_PENDING_NEW = {"workflowStepId": None, "vendor": None}
+
+
class TestTasksResource:
resource_class = TasksResource
@@ -315,7 +360,7 @@ def test_add_general_task(self, m_add_task, incoming_data, request_data, base_ab
resource = self.get_resource(base_absolut_url)
assert resource.add_general_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**C_GENERAL_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -389,7 +434,7 @@ def test_add_general_by_string_ids_task(self, m_add_task, incoming_data, request
resource = self.get_resource(base_absolut_url)
assert resource.add_general_by_string_ids_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**C_GENERAL_STRING_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -461,7 +506,7 @@ def test_add_vendor_task(self, m_add_task, incoming_data, request_data, base_abs
resource = self.get_resource(base_absolut_url)
assert resource.add_vendor_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**C_VENDOR_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -529,7 +574,7 @@ def test_add_vendor_by_string_ids_task(
resource = self.get_resource(base_absolut_url)
assert resource.add_vendor_by_string_ids_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**C_VENDOR_STRING_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -803,7 +848,361 @@ def test_add_task_comment(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="post",
path=resource.get_task_comments_path(projectId=1, taskId=2),
- request_data={"text": "hello"},
+ request_data={"text": "hello", "timeSpent": None},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_task_comment_with_time_spent(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_task_comment(projectId=1, taskId=2, text="hello", timeSpent=3600)
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path=resource.get_task_comments_path(projectId=1, taskId=2),
+ request_data={"text": "hello", "timeSpent": 3600},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_task_comment_time_spent(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "value": 120,
+ "op": PatchOperation.REPLACE,
+ "path": TaskCommentPatchPath.TIME_SPENT,
+ }
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.edit_task_comment(projectId=1, taskId=2, taskCommentId=3, data=data)
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="patch",
+ path=resource.get_task_comments_path(projectId=1, taskId=2, taskCommentId=3),
+ request_data=data,
+ )
+
+ COST_FIELDS_VALUES = {
+ "translationsUpdatedDateFrom": datetime(year=2024, month=1, day=1),
+ "translationsUpdatedDateTo": datetime(year=2024, month=2, day=1),
+ "generateCostEstimate": True,
+ "generateTranslationCost": False,
+ "reportSettingsTemplateId": 7,
+ }
+
+ @mock.patch("crowdin_api.api_resources.tasks.resource.TasksResource.add_task")
+ def test_add_general_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "directoryIds": [10],
+ "branchIds": [11],
+ "labelMatchRule": TaskLabelMatchRule.ALL,
+ "excludeLabelMatchRule": TaskLabelMatchRule.ANY,
+ **self.COST_FIELDS_VALUES,
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_general_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ fileIds=None,
+ type=CrowdinGeneralTaskType.PROOFREAD,
+ **new_fields,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "fileIds": None,
+ "type": CrowdinGeneralTaskType.PROOFREAD,
+ "status": None,
+ "description": None,
+ "splitContent": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "labelIds": None,
+ "excludeLabelIds": None,
+ "assignees": None,
+ "deadline": None,
+ "startedAt": None,
+ "dateFrom": None,
+ "dateTo": None,
+ "batchId": None,
+ **new_fields,
+ },
+ )
+
+ @mock.patch("crowdin_api.api_resources.tasks.resource.TasksResource.add_task")
+ def test_add_general_by_string_ids_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_general_by_string_ids_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ stringIds=[1],
+ type=CrowdinGeneralTaskType.TRANSLATE,
+ **self.COST_FIELDS_VALUES,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "stringIds": [1],
+ "type": CrowdinGeneralTaskType.TRANSLATE,
+ "status": None,
+ "description": None,
+ "splitContent": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "assignees": None,
+ "deadline": None,
+ "startedAt": None,
+ "dateFrom": None,
+ "dateTo": None,
+ "batchId": None,
+ **self.COST_FIELDS_VALUES,
+ },
+ )
+
+ @mock.patch("crowdin_api.api_resources.tasks.resource.TasksResource.add_task")
+ def test_add_vendor_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "directoryIds": None,
+ "branchIds": [11],
+ "labelMatchRule": TaskLabelMatchRule.ANY,
+ "excludeLabelMatchRule": TaskLabelMatchRule.ALL,
+ **self.COST_FIELDS_VALUES,
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_vendor_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ fileIds=None,
+ type=CrowdinTaskType.TRANSLATE_BY_VENDOR,
+ vendor="gengo",
+ **new_fields,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "fileIds": None,
+ "type": CrowdinTaskType.TRANSLATE_BY_VENDOR,
+ "vendor": "gengo",
+ "status": None,
+ "description": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "labelIds": None,
+ "excludeLabelIds": None,
+ "deadline": None,
+ "dateFrom": None,
+ "dateTo": None,
+ **new_fields,
+ },
+ )
+
+ @mock.patch("crowdin_api.api_resources.tasks.resource.TasksResource.add_task")
+ def test_add_vendor_by_string_ids_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "labelIds": [1],
+ "labelMatchRule": TaskLabelMatchRule.ALL,
+ "excludeLabelIds": [2],
+ "excludeLabelMatchRule": TaskLabelMatchRule.ANY,
+ **self.COST_FIELDS_VALUES,
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_vendor_by_string_ids_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ stringIds=[1],
+ type=CrowdinTaskType.PROOFREAD_BY_VENDOR,
+ vendor="oht",
+ **new_fields,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "stringIds": [1],
+ "type": CrowdinTaskType.PROOFREAD_BY_VENDOR,
+ "vendor": "oht",
+ "status": None,
+ "description": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "deadline": None,
+ "dateFrom": None,
+ "dateTo": None,
+ **new_fields,
+ },
+ )
+
+ @pytest.mark.parametrize(
+ "path",
+ (
+ TaskOperationPatchPath.STARTED_AT,
+ TaskOperationPatchPath.SPLIT_CONTENT,
+ TaskOperationPatchPath.GENERATE_COST_ESTIMATE,
+ TaskOperationPatchPath.GENERATE_TRANSLATION_COST,
+ TaskOperationPatchPath.REPORT_SETTINGS_TEMPLATE_ID,
+ PendingTaskOperationPatchPath.ASSIGNEES,
+ VendorPendingTaskOperationPatchPath.DESCRIPTION,
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_task_new_paths(self, m_request, path, base_absolut_url):
+ m_request.return_value = "response"
+ data = [{"value": 1, "op": PatchOperation.REPLACE, "path": path}]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_task(projectId=1, taskId=2, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ request_data=data,
+ path="projects/1/tasks/2",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_tasks_multiple_statuses(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.list_tasks(
+ projectId=1, status=[CrowdinTaskStatus.TODO, CrowdinTaskStatus.IN_PROGRESS]
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/tasks",
+ params={
+ "orderBy": None,
+ "assigneeId": None,
+ "status": "todo,in_progress",
+ "batchId": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_user_tasks_multiple_statuses(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.list_user_tasks(status=[CrowdinTaskStatus.DONE, CrowdinTaskStatus.CLOSED])
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path="user/tasks",
+ params={"orderBy": None, "status": "done,closed", "offset": 0, "limit": 25},
+ )
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_params",
+ (
+ (
+ {},
+ {
+ "orderBy": None,
+ "status": None,
+ "type": None,
+ "projectIds": None,
+ "assigneeIds": None,
+ "creatorIds": None,
+ "targetLanguageIds": None,
+ "sourceLanguageIds": None,
+ "createdAtFrom": None,
+ "createdAtTo": None,
+ "deadlineFrom": None,
+ "deadlineTo": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ ),
+ (
+ {
+ "orderBy": Sorting([SortingRule(ListTasksOrderBy.ID, SortingOrder.DESC)]),
+ "status": CrowdinTaskStatus.TODO,
+ "type": [CrowdinTaskType.TRANSLATE, CrowdinTaskType.PROOFREAD],
+ "projectIds": [1, 2],
+ "assigneeIds": [3],
+ "creatorIds": [4, 5],
+ "targetLanguageIds": ["uk", "de"],
+ "sourceLanguageIds": ["en"],
+ "createdAtFrom": datetime(year=2024, month=1, day=1),
+ "createdAtTo": datetime(year=2024, month=2, day=1),
+ "deadlineFrom": datetime(year=2024, month=3, day=1),
+ "deadlineTo": datetime(year=2024, month=4, day=1),
+ "limit": 10,
+ },
+ {
+ "orderBy": Sorting([SortingRule(ListTasksOrderBy.ID, SortingOrder.DESC)]),
+ "status": CrowdinTaskStatus.TODO,
+ "type": "0,1",
+ "projectIds": "1,2",
+ "assigneeIds": "3",
+ "creatorIds": "4,5",
+ "targetLanguageIds": "uk,de",
+ "sourceLanguageIds": "en",
+ "createdAtFrom": datetime(year=2024, month=1, day=1),
+ "createdAtTo": datetime(year=2024, month=2, day=1),
+ "deadlineFrom": datetime(year=2024, month=3, day=1),
+ "deadlineTo": datetime(year=2024, month=4, day=1),
+ "offset": 0,
+ "limit": 10,
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_specific_user_tasks(
+ 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_specific_user_tasks(userId=12, **incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="users/12/tasks",
+ params=request_params,
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -970,7 +1369,7 @@ def test_add_general_task(
resource = self.get_resource(base_absolut_url)
assert resource.add_general_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**E_GENERAL_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -1056,7 +1455,7 @@ def test_add_general_by_string_ids_task(
resource.add_general_by_string_ids_task(projectId=1, **incoming_data)
== "response"
)
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**E_GENERAL_STRING_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -1125,7 +1524,7 @@ def test_add_vendor_task(
resource = self.get_resource(base_absolut_url)
assert resource.add_vendor_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**E_VENDOR_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -1191,7 +1590,7 @@ def test_add_vendor_by_string_ids_task(
resource.add_vendor_by_string_ids_task(projectId=1, **incoming_data)
== "response"
)
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**E_VENDOR_STRING_NEW, **request_data})
@pytest.mark.parametrize(
"incoming_data, request_data",
@@ -1242,4 +1641,325 @@ def test_add_pending_task(
resource = self.get_resource(base_absolut_url)
assert resource.add_pending_task(projectId=1, **incoming_data) == "response"
- m_add_task.assert_called_once_with(projectId=1, request_data=request_data)
+ m_add_task.assert_called_once_with(projectId=1, request_data={**E_PENDING_NEW, **request_data})
+
+ @mock.patch(
+ "crowdin_api.api_resources.tasks.resource.EnterpriseTasksResource.add_task"
+ )
+ def test_add_pending_task_by_workflow_step(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_pending_task(
+ projectId=1, title="title", precedingTaskId=1, workflowStepId=5, vendor="oht"
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "precedingTaskId": 1,
+ "type": None,
+ "workflowStepId": 5,
+ "vendor": "oht",
+ "title": "title",
+ "description": None,
+ "assignees": None,
+ "assignedTeams": None,
+ "deadline": None,
+ },
+ )
+
+ COST_FIELDS_VALUES = {
+ "translationsUpdatedDateFrom": datetime(year=2024, month=1, day=1),
+ "translationsUpdatedDateTo": datetime(year=2024, month=2, day=1),
+ "generateCostEstimate": True,
+ "generateTranslationCost": False,
+ "reportSettingsTemplateId": 7,
+ }
+
+ @mock.patch(
+ "crowdin_api.api_resources.tasks.resource.EnterpriseTasksResource.add_task"
+ )
+ def test_add_general_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "directoryIds": [10],
+ "branchIds": None,
+ "labelMatchRule": TaskLabelMatchRule.ALL,
+ "excludeLabelMatchRule": TaskLabelMatchRule.ANY,
+ "skipAssignedStringsScope": TaskSkipAssignedStringsScope.SAME_WORKFLOW_STEP,
+ **self.COST_FIELDS_VALUES,
+ "fields": {"some-field": "value"},
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_general_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ fileIds=None,
+ type=None,
+ workflowStepId=3,
+ status=CrowdinTaskStatus.IN_PROGRESS,
+ **new_fields,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "fileIds": None,
+ "type": None,
+ "workflowStepId": 3,
+ "status": CrowdinTaskStatus.IN_PROGRESS,
+ "description": None,
+ "splitContent": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "labelIds": None,
+ "excludeLabelIds": None,
+ "assignees": None,
+ "assignedTeams": None,
+ "deadline": None,
+ "startedAt": None,
+ "dateFrom": None,
+ "dateTo": None,
+ "batchId": None,
+ **new_fields,
+ },
+ )
+
+ @mock.patch(
+ "crowdin_api.api_resources.tasks.resource.EnterpriseTasksResource.add_task"
+ )
+ def test_add_general_by_string_ids_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "skipAssignedStringsScope": TaskSkipAssignedStringsScope.ALL,
+ **self.COST_FIELDS_VALUES,
+ "fields": {"some-field": 1},
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_general_by_string_ids_task(
+ projectId=1, title="title", languageId="uk", stringIds=[1], **new_fields
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "stringIds": [1],
+ "type": None,
+ "workflowStepId": None,
+ "status": None,
+ "description": None,
+ "splitContent": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "assignees": None,
+ "assignedTeams": None,
+ "deadline": None,
+ "startedAt": None,
+ "dateFrom": None,
+ "dateTo": None,
+ "batchId": None,
+ **new_fields,
+ },
+ )
+
+ @mock.patch(
+ "crowdin_api.api_resources.tasks.resource.EnterpriseTasksResource.add_task"
+ )
+ def test_add_vendor_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "directoryIds": None,
+ "branchIds": [2],
+ "labelMatchRule": TaskLabelMatchRule.ANY,
+ "excludeLabelMatchRule": TaskLabelMatchRule.ALL,
+ "skipAssignedStringsScope": TaskSkipAssignedStringsScope.ALL,
+ **self.COST_FIELDS_VALUES,
+ "fields": {"some-field": True},
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_vendor_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ workflowStepId=3,
+ fileIds=None,
+ **new_fields,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "fileIds": None,
+ "workflowStepId": 3,
+ "description": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "labelIds": None,
+ "excludeLabelIds": None,
+ "deadline": None,
+ "startedAt": None,
+ "dateTo": None,
+ **new_fields,
+ },
+ )
+
+ @mock.patch(
+ "crowdin_api.api_resources.tasks.resource.EnterpriseTasksResource.add_task"
+ )
+ def test_add_vendor_by_string_ids_task_new_fields(self, m_add_task, base_absolut_url):
+ m_add_task.return_value = "response"
+ new_fields = {
+ "labelIds": [1],
+ "labelMatchRule": TaskLabelMatchRule.ALL,
+ "skipAssignedStringsScope": TaskSkipAssignedStringsScope.SAME_WORKFLOW_STEP,
+ **self.COST_FIELDS_VALUES,
+ "fields": {"some-field": "value"},
+ }
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.add_vendor_by_string_ids_task(
+ projectId=1,
+ title="title",
+ languageId="uk",
+ workflowStepId=3,
+ stringIds=[1, 2],
+ **new_fields,
+ )
+ == "response"
+ )
+ m_add_task.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "title": "title",
+ "languageId": "uk",
+ "stringIds": [1, 2],
+ "workflowStepId": 3,
+ "description": None,
+ "skipAssignedStrings": None,
+ "includePreTranslatedStringsOnly": None,
+ "deadline": None,
+ "startedAt": None,
+ "dateTo": None,
+ **new_fields,
+ },
+ )
+
+ @pytest.mark.parametrize(
+ "path",
+ (
+ EnterpriseTaskOperationPatchPath.ASSIGNED_TEAMS,
+ EnterpriseTaskOperationPatchPath.SKIP_ASSIGNED_STRINGS_SCOPE,
+ EnterpriseTaskOperationPatchPath.FIELDS,
+ "/fields/some-field",
+ EnterpriseVendorTaskOperationPatchPath.REPORT_SETTINGS_TEMPLATE_ID,
+ EnterpriseInterOrganizationalTaskOperationPatchPath.GENERATE_COST_ESTIMATE,
+ EnterpriseInterOrganizationalTaskOperationPatchPath.GENERATE_TRANSLATION_COST,
+ EnterpriseInterOrganizationalTaskOperationPatchPath.REPORT_SETTINGS_TEMPLATE_ID,
+ EnterprisePendingTaskOperationPatchPath.ASSIGNED_TEAMS,
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_task(self, m_request, path, base_absolut_url):
+ m_request.return_value = "response"
+ data = [{"value": 1, "op": PatchOperation.REPLACE, "path": path}]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_task(projectId=1, taskId=2, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch",
+ request_data=data,
+ path="projects/1/tasks/2",
+ )
+
+ @pytest.mark.parametrize(
+ "incoming_data, request_params",
+ (
+ (
+ {},
+ {
+ "orderBy": None,
+ "status": None,
+ "type": None,
+ "projectIds": None,
+ "groupIds": None,
+ "assigneeIds": None,
+ "creatorIds": None,
+ "targetLanguageIds": None,
+ "sourceLanguageIds": None,
+ "createdAtFrom": None,
+ "createdAtTo": None,
+ "deadlineFrom": None,
+ "deadlineTo": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ ),
+ (
+ {
+ "orderBy": Sorting([SortingRule(ListTasksOrderBy.ID, SortingOrder.ASC)]),
+ "status": [CrowdinTaskStatus.TODO, CrowdinTaskStatus.DONE],
+ "type": CrowdinTaskType.TRANSLATE_BY_VENDOR,
+ "groupIds": [1, 2],
+ "assigneeIds": [3, 4],
+ "creatorIds": [5],
+ "targetLanguageIds": ["uk"],
+ "sourceLanguageIds": ["en", "fr"],
+ "createdAtFrom": datetime(year=2024, month=1, day=1),
+ "createdAtTo": datetime(year=2024, month=2, day=1),
+ "deadlineFrom": datetime(year=2024, month=3, day=1),
+ "deadlineTo": datetime(year=2024, month=4, day=1),
+ "offset": 5,
+ },
+ {
+ "orderBy": Sorting([SortingRule(ListTasksOrderBy.ID, SortingOrder.ASC)]),
+ "status": "todo,done",
+ "type": CrowdinTaskType.TRANSLATE_BY_VENDOR,
+ "projectIds": None,
+ "groupIds": "1,2",
+ "assigneeIds": "3,4",
+ "creatorIds": "5",
+ "targetLanguageIds": "uk",
+ "sourceLanguageIds": "en,fr",
+ "createdAtFrom": datetime(year=2024, month=1, day=1),
+ "createdAtTo": datetime(year=2024, month=2, day=1),
+ "deadlineFrom": datetime(year=2024, month=3, day=1),
+ "deadlineTo": datetime(year=2024, month=4, day=1),
+ "offset": 5,
+ "limit": 25,
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_organization_tasks(
+ 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_organization_tasks(**incoming_data) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="tasks",
+ params=request_params,
+ )
diff --git a/crowdin_api/api_resources/tasks/types.py b/crowdin_api/api_resources/tasks/types.py
index c94e4cc8..e46740c2 100644
--- a/crowdin_api/api_resources/tasks/types.py
+++ b/crowdin_api/api_resources/tasks/types.py
@@ -5,6 +5,13 @@
TaskOperationPatchPath,
VendorTaskOperationPatchPath,
ConfigTaskOperationPatchPath,
+ PendingTaskOperationPatchPath,
+ VendorPendingTaskOperationPatchPath,
+ EnterpriseTaskOperationPatchPath,
+ EnterpriseVendorTaskOperationPatchPath,
+ EnterpriseInterOrganizationalTaskOperationPatchPath,
+ EnterprisePendingTaskOperationPatchPath,
+ TaskCommentPatchPath,
)
from crowdin_api.typing import TypedDict
@@ -26,6 +33,42 @@ class VendorPatchRequest(TypedDict):
path: VendorTaskOperationPatchPath
+class PendingTaskPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: PendingTaskOperationPatchPath
+
+
+class VendorPendingTaskPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: VendorPendingTaskOperationPatchPath
+
+
+class EnterpriseTaskPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: Union[EnterpriseTaskOperationPatchPath, str]
+
+
+class EnterpriseVendorTaskPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: EnterpriseVendorTaskOperationPatchPath
+
+
+class EnterpriseInterOrganizationalTaskPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: EnterpriseInterOrganizationalTaskOperationPatchPath
+
+
+class EnterprisePendingTaskPatchRequest(TypedDict):
+ value: Any
+ op: PatchOperation
+ path: EnterprisePendingTaskOperationPatchPath
+
+
class ConfigPatchRequest(TypedDict):
value: Union[str, int]
op: PatchOperation
@@ -60,5 +103,4 @@ class EnterpriseTaskSettingsTemplateLanguages(TypedDict):
class TaskCommentPatchRequest(TypedDict):
value: Any
op: PatchOperation
- # For now only /text is supported in edit operations
- path: str
+ path: Union[TaskCommentPatchPath, str]
diff --git a/crowdin_api/api_resources/teams/resource.py b/crowdin_api/api_resources/teams/resource.py
index acd0fd87..e96ff981 100644
--- a/crowdin_api/api_resources/teams/resource.py
+++ b/crowdin_api/api_resources/teams/resource.py
@@ -2,7 +2,13 @@
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.teams.types \
- import Permissions, TeamPatchRequest, TeamByProjectRole, GroupTeamPatchRequest
+ import (
+ Permissions,
+ TeamPatchRequest,
+ TeamByProjectRole,
+ GroupTeamPatchRequest,
+ TeamProjectPermissionsPatchRequest,
+ )
from crowdin_api.api_resources.users.enums import ProjectRole
from crowdin_api.sorting import Sorting
from crowdin_api.utils import convert_to_query_string, convert_enum_to_string_if_exists
@@ -17,7 +23,7 @@ class TeamsResource(BaseResource):
Use API to create, modify, and delete specific teams and members.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Teams
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Teams
"""
def get_teams_path(self, teamId: Optional[int] = None):
@@ -41,7 +47,9 @@ def get_group_teams_path(self, group_id: int, team_id: Optional[int] = None):
def list_group_teams(
self,
group_id: int,
- order_by: Optional[Sorting] = None
+ order_by: Optional[Sorting] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
):
"""
List Group Teams
@@ -53,8 +61,9 @@ def list_group_teams(
params = {
"orderBy": order_by
}
+ params.update(self.get_page_params(offset=offset, limit=limit))
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_group_teams_path(group_id),
params=params
@@ -99,16 +108,22 @@ def add_team_to_project(
self,
teamId: int,
projectId: Optional[int] = None,
- accessToAllWorkflowSteps: bool = True,
- managerAccess: bool = False,
+ accessToAllWorkflowSteps: Optional[bool] = None,
+ managerAccess: Optional[bool] = None,
permissions: Optional[Permissions] = None,
- roles: Optional[Iterable[TeamByProjectRole]] = None
+ roles: Optional[Iterable[TeamByProjectRole]] = None,
+ developerAccess: Optional[bool] = None,
):
"""
Add Team To Project.
+ `managerAccess`, `developerAccess`, `accessToAllWorkflowSteps`, `permissions` and `roles`
+ are mutually exclusive. `accessToAllWorkflowSteps` and `permissions` are deprecated by
+ the API, use `roles` instead. Omitted values fall back to the API defaults
+ (`accessToAllWorkflowSteps` - `true`, `managerAccess` - `false`).
+
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.teams.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.teams.post
"""
projectId = projectId or self.get_project_id()
@@ -120,6 +135,7 @@ def add_team_to_project(
"teamId": teamId,
"accessToAllWorkflowSteps": accessToAllWorkflowSteps,
"managerAccess": managerAccess,
+ "developerAccess": developerAccess,
"permissions": permissions,
"roles": roles
},
@@ -140,7 +156,7 @@ def list_teams(
List Teams.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.getMany
"""
params = {
@@ -164,7 +180,7 @@ def add_team(self, name: str):
Add Team.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.post
"""
return self.requester.request(
@@ -178,7 +194,7 @@ def get_team(self, teamId: int):
Get Team.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.get
"""
return self.requester.request(method="get", path=self.get_teams_path(teamId=teamId))
@@ -188,7 +204,7 @@ def delete_team(self, teamId: int):
Delete Team.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.delete
"""
return self.requester.request(method="delete", path=self.get_teams_path(teamId=teamId))
@@ -198,7 +214,7 @@ def edit_team(self, teamId: int, data: Iterable[TeamPatchRequest]):
Edit Team.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.patch
"""
return self.requester.request(
@@ -207,6 +223,43 @@ def edit_team(self, teamId: int, data: Iterable[TeamPatchRequest]):
request_data=data
)
+ def list_team_project_permissions(
+ self,
+ teamId: int,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List Team Project Permissions.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.projects.permissions.getMany
+ """
+
+ return self._get_entire_data(
+ method="get",
+ path=f"{self.get_teams_path(teamId=teamId)}/projects/permissions",
+ params=self.get_page_params(offset=offset, limit=limit),
+ )
+
+ def edit_team_project_permissions(
+ self,
+ teamId: int,
+ data: Iterable[TeamProjectPermissionsPatchRequest],
+ ):
+ """
+ Edit Team Project Permissions.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.projects.permissions.patch
+ """
+
+ return self.requester.request(
+ method="patch",
+ path=f"{self.get_teams_path(teamId=teamId)}/projects/permissions",
+ request_data=data,
+ )
+
def teams_member_list(
self,
teamId: int,
@@ -217,7 +270,7 @@ def teams_member_list(
Team Members List.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.members.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.members.getMany
"""
return self._get_entire_data(
@@ -231,7 +284,7 @@ def add_team_members(self, teamId: int, userIds: Iterable[int]):
Add Team Members.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.members.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.members.post
"""
return self.requester.request(
@@ -245,7 +298,7 @@ def delete_all_team_members(self, teamId: int):
Delete All Team Members.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.members.deleteMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.members.deleteMany
"""
return self.requester.request(method="delete", path=self.get_members_path(teamId=teamId))
@@ -255,7 +308,7 @@ def delete_team_member(self, teamId: int, memberId: int):
Delete Team Member.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.members.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.teams.members.delete
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/teams/tests/test_teams_resources.py b/crowdin_api/api_resources/teams/tests/test_teams_resources.py
index eed42362..57d72633 100644
--- a/crowdin_api/api_resources/teams/tests/test_teams_resources.py
+++ b/crowdin_api/api_resources/teams/tests/test_teams_resources.py
@@ -64,7 +64,9 @@ def test_get_group_teams_path(self, group_id, team_id, path, base_absolut_url):
(
{},
{
- "orderBy": None
+ "orderBy": None,
+ "offset": 0,
+ "limit": 25,
}
),
(
@@ -87,6 +89,8 @@ def test_get_group_teams_path(self, group_id, team_id, path, base_absolut_url):
)
]
),
+ "offset": 0,
+ "limit": 25,
}
)
)
@@ -166,10 +170,11 @@ def test_get_group_team(self, m_request, base_absolut_url):
{"teamId": 1},
{
"teamId": 1,
- "accessToAllWorkflowSteps": True,
- "managerAccess": False,
+ "accessToAllWorkflowSteps": None,
+ "managerAccess": None,
"permissions": None,
"roles": None,
+ "developerAccess": None,
}
),
(
@@ -194,7 +199,8 @@ def test_get_group_team(self, m_request, base_absolut_url):
},
"de": {"workflowStepIds": "all"}
},
- "roles": None
+ "roles": None,
+ "developerAccess": None,
},
),
(
@@ -278,7 +284,8 @@ def test_get_group_team(self, m_request, base_absolut_url):
"languagesAccess": []
}
}
- ]
+ ],
+ "developerAccess": None,
},
),
),
@@ -477,3 +484,60 @@ def test_delete_team_member(self, m_request, base_absolut_url):
method="delete",
path=resource.get_members_path(teamId=1, memberId=2)
)
+
+
+class TestTeamsProjectPermissions:
+ resource_class = TeamsResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_team_to_project_developer_access(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_team_to_project(
+ teamId=1, projectId=2, developerAccess=True
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/2/teams",
+ request_data={
+ "teamId": 1,
+ "accessToAllWorkflowSteps": None,
+ "managerAccess": None,
+ "developerAccess": True,
+ "permissions": None,
+ "roles": None,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_team_project_permissions(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_team_project_permissions(teamId=1, offset=5, limit=10) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="teams/1/projects/permissions",
+ params={"offset": 5, "limit": 10},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_team_project_permissions(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": "/45/roles",
+ "value": [{"name": "proofreader", "permissions": {"allLanguages": True}}],
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_team_project_permissions(teamId=1, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch", path="teams/1/projects/permissions", request_data=data
+ )
diff --git a/crowdin_api/api_resources/teams/types.py b/crowdin_api/api_resources/teams/types.py
index 050962fa..be7700f1 100644
--- a/crowdin_api/api_resources/teams/types.py
+++ b/crowdin_api/api_resources/teams/types.py
@@ -44,3 +44,15 @@ class GroupTeamPatchRequest(TypedDict):
op: PatchOperation
path: str
value: Any
+
+
+class TeamProjectPermissionsPatchRequest(TypedDict):
+ """
+ JSON Patch operation for team project permissions.
+
+ `path` is `/{projectId}/roles` (replace), `/{projectId}/roles/-` (add) or `/{projectId}` (remove).
+ """
+
+ op: PatchOperation
+ path: str
+ value: Any
diff --git a/crowdin_api/api_resources/translation_memory/enums.py b/crowdin_api/api_resources/translation_memory/enums.py
index 7507fe88..9f24dd9c 100644
--- a/crowdin_api/api_resources/translation_memory/enums.py
+++ b/crowdin_api/api_resources/translation_memory/enums.py
@@ -3,6 +3,15 @@
class TranslationMemoryPatchPath(Enum):
NAME = "/name"
+ LANGUAGE_ID = "/languageId"
+ IS_SHARED = "/isShared"
+ GROUP_ID = "/groupId" # Enterprise only
+
+
+class TranslationMemoryExportFormat(Enum):
+ TMX = "tmx"
+ CSV = "csv"
+ XLSX = "xlsx"
class TranslationMemorySegmentRecordOperation(Enum):
diff --git a/crowdin_api/api_resources/translation_memory/resource.py b/crowdin_api/api_resources/translation_memory/resource.py
index 3880c0c4..c50e8474 100644
--- a/crowdin_api/api_resources/translation_memory/resource.py
+++ b/crowdin_api/api_resources/translation_memory/resource.py
@@ -2,6 +2,7 @@
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.enums import ExportFormat
+from crowdin_api.api_resources.translation_memory.enums import TranslationMemoryExportFormat
from crowdin_api.api_resources.translation_memory.types import (
OrganizationConcordanceSearchRequest,
TranslationMemoryPatchRequest,
@@ -28,7 +29,7 @@ class TranslationMemoryResource(BaseResource):
are asynchronous operations and shall be completed with sequence of API methods.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Translation-Memory
+ https://support.crowdin.com/developer/api/v2/#tag/Translation-Memory
"""
def get_tms_path(self, tmId: Optional[int] = None):
@@ -43,15 +44,23 @@ def list_tms(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ filter: Optional[str] = None,
+ userId: Optional[int] = None,
+ groupId: Optional[int] = None,
):
"""
List TMs.
+ :param filter: Filter TMs by `name`.
+ :param userId: Project Member Identifier. Crowdin only.
+ :param groupId: Group Identifier. Set 0 to see TMs of root group.
+ Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.getMany
"""
- params = {"orderBy": orderBy}
+ params = {"orderBy": orderBy, "filter": filter, "userId": userId, "groupId": groupId}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
return self._get_entire_data(
@@ -60,18 +69,34 @@ def list_tms(
params=params,
)
- def add_tm(self, name: str, languageId: str):
+ def add_tm(
+ self,
+ name: str,
+ languageId: str,
+ isShared: Optional[bool] = None,
+ groupId: Optional[int] = None,
+ ):
"""
- Add Glossary.
+ Add TM.
+
+ :param isShared: Whether the TM should be shared to all projects within the account
+ (Crowdin) or within the group (Crowdin Enterprise).
+ :param groupId: Group Identifier. If 0 – the TM will be available for all projects
+ and groups in the workspace. Crowdin Enterprise only.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.post
"""
return self.requester.request(
method="post",
path=self.get_tms_path(),
- request_data={"name": name, "languageId": languageId},
+ request_data={
+ "name": name,
+ "languageId": languageId,
+ "isShared": isShared,
+ "groupId": groupId,
+ },
)
def get_tm(self, tmId: int):
@@ -79,7 +104,7 @@ def get_tm(self, tmId: int):
Get TM.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.get
"""
return self.requester.request(method="get", path=self.get_tms_path(tmId=tmId))
@@ -89,7 +114,7 @@ def delete_tm(self, tmId: int):
Delete TM.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.delete
"""
return self.requester.request(method="delete", path=self.get_tms_path(tmId=tmId))
@@ -98,8 +123,10 @@ def edit_tm(self, tmId: int, data: Iterable[TranslationMemoryPatchRequest]):
"""
Edit TM.
+ `TranslationMemoryPatchPath.GROUP_ID` (`/groupId`) is Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.patch
"""
return self.requester.request(
@@ -111,7 +138,7 @@ def clear_tm(self, tmId: int):
Clear TM.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.clear
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.clear
"""
return self.requester.request(
@@ -131,14 +158,17 @@ def list_tm_segments(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ croql: Optional[str] = None,
):
"""
List TM Segments.
+ :param croql: Filter segments by CroQL.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.getMany
"""
- params = {"orderBy": orderBy}
+ params = {"orderBy": orderBy, "croql": croql}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
return self._get_entire_data(
@@ -154,7 +184,7 @@ def create_tm_segment(
Create TM Segment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.post
"""
data = {"records": records}
@@ -169,7 +199,7 @@ def get_tm_segment(self, tmId: int, segmentId: int):
Get TM Segment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.get
"""
return self.requester.request(
method="get", path=self.get_tm_segments_path(tmId=tmId, segmentId=segmentId)
@@ -180,7 +210,7 @@ def delete_tm_segment(self, tmId: int, segmentId: int):
Delete TM Segment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.delete
"""
return self.requester.request(
method="delete",
@@ -203,7 +233,7 @@ def edit_tm_segment(
Edit TM Segment.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.patch
"""
return self.requester.request(
method="patch",
@@ -226,7 +256,7 @@ def tm_segment_batch_operations(
TM Segment Batch Operations.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.segments.patchBatch
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.segments.patchBatch
"""
return self.requester.request(
method="patch",
@@ -246,13 +276,16 @@ def export_tm(
tmId: int,
sourceLanguageId: Optional[str] = None,
targetLanguageId: Optional[str] = None,
- format: Optional[ExportFormat] = None,
+ format: Optional[Union[TranslationMemoryExportFormat, ExportFormat]] = None,
):
"""
Export TM.
+ :param format: TM file format: `tmx` (default), `csv` or `xlsx`.
+ Use `TranslationMemoryExportFormat`.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.exports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.exports.post
"""
return self.requester.request(
@@ -270,7 +303,7 @@ def check_tm_export_status(self, tmId: int, exportId: str):
Check TM Export Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.exports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.exports.get
"""
return self.requester.request(
@@ -282,7 +315,7 @@ def download_tm(self, tmId: int, exportId: str):
Download TM.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.exports.download.download
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.exports.download.download
"""
return self.requester.request(
@@ -303,7 +336,7 @@ def concordance_search_in_tms(
Concordance search in TMs
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.tms.concordance.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.tms.concordance.post
"""
projectId = projectId or self.get_project_id()
@@ -326,10 +359,10 @@ def organization_concordance_search(self, request_data: OrganizationConcordanceS
Concordance search in organization TMs.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.concordance.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.concordance.post
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.tms.concordance.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.tms.concordance.post
"""
return self.requester.request(
@@ -350,7 +383,7 @@ def import_tm(
Import TM.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.imports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.imports.post
"""
return self.requester.request(
@@ -368,7 +401,7 @@ def check_tm_import_status(self, tmId: int, importId: str):
Check TM Import Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.tms.imports.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.tms.imports.get
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/translation_memory/tests/test_translation_memory_resources.py b/crowdin_api/api_resources/translation_memory/tests/test_translation_memory_resources.py
index eeb7992a..7b5b732b 100644
--- a/crowdin_api/api_resources/translation_memory/tests/test_translation_memory_resources.py
+++ b/crowdin_api/api_resources/translation_memory/tests/test_translation_memory_resources.py
@@ -5,6 +5,7 @@
from crowdin_api.api_resources.translation_memory.enums import (
ListTmSegmentsOrderBy,
ListTmsOrderBy,
+ TranslationMemoryExportFormat,
TranslationMemoryPatchPath,
TranslationMemorySegmentBatchOperation,
TranslationMemorySegmentBatchOperationPath,
@@ -48,6 +49,9 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
{},
{
"orderBy": None,
+ "filter": None,
+ "userId": None,
+ "groupId": None,
"limit": 25,
"offset": 0,
},
@@ -59,11 +63,17 @@ def test_get_screenshots_path(self, in_params, path, base_absolut_url):
),
"limit": 25,
"offset": 0,
+ "filter": "name",
+ "userId": 1,
+ "groupId": 2,
},
{
"orderBy": Sorting(
[SortingRule(ListTmsOrderBy.ID, SortingOrder.DESC)]
),
+ "filter": "name",
+ "userId": 1,
+ "groupId": 2,
"limit": 25,
"offset": 0,
},
@@ -84,16 +94,29 @@ def test_list_tms(
path=resource.get_tms_path(),
)
+ @pytest.mark.parametrize(
+ "incoming_data, request_data",
+ (
+ (
+ {"name": "test", "languageId": "fr"},
+ {"name": "test", "languageId": "fr", "isShared": None, "groupId": None},
+ ),
+ (
+ {"name": "test", "languageId": "fr", "isShared": True, "groupId": 2},
+ {"name": "test", "languageId": "fr", "isShared": True, "groupId": 2},
+ ),
+ ),
+ )
@mock.patch("crowdin_api.requester.APIRequester.request")
- def test_add_tm(self, m_request, base_absolut_url):
+ def test_add_tm(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_tm(name="test", languageId="fr") == "response"
+ assert resource.add_tm(**incoming_data) == "response"
m_request.assert_called_once_with(
method="post",
path=resource.get_tms_path(),
- request_data={"name": "test", "languageId": "fr"},
+ request_data=request_data,
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -127,7 +150,22 @@ def test_edit_tm(self, m_request, base_absolut_url):
"value": "value",
"op": PatchOperation.REPLACE,
"path": TranslationMemoryPatchPath.NAME,
- }
+ },
+ {
+ "value": "uk",
+ "op": PatchOperation.REPLACE,
+ "path": TranslationMemoryPatchPath.LANGUAGE_ID,
+ },
+ {
+ "value": True,
+ "op": PatchOperation.REPLACE,
+ "path": TranslationMemoryPatchPath.IS_SHARED,
+ },
+ {
+ "value": 1,
+ "op": PatchOperation.REPLACE,
+ "path": TranslationMemoryPatchPath.GROUP_ID,
+ },
]
resource = self.get_resource(base_absolut_url)
@@ -166,6 +204,7 @@ def test_get_tm_segments_path(self, in_params, path, base_absolut_url):
{},
{
"orderBy": None,
+ "croql": None,
"limit": 25,
"offset": 0,
},
@@ -175,6 +214,7 @@ def test_get_tm_segments_path(self, in_params, path, base_absolut_url):
"orderBy": Sorting(
[SortingRule(ListTmSegmentsOrderBy.ID, SortingOrder.DESC)]
),
+ "croql": "count of records > 1",
"limit": 25,
"offset": 0,
},
@@ -182,6 +222,7 @@ def test_get_tm_segments_path(self, in_params, path, base_absolut_url):
"orderBy": Sorting(
[SortingRule(ListTmSegmentsOrderBy.ID, SortingOrder.DESC)]
),
+ "croql": "count of records > 1",
"limit": 25,
"offset": 0,
},
@@ -358,6 +399,14 @@ def test_get_tm_export_path(self, in_params, path, base_absolut_url):
"format": ExportFormat.CSV,
},
),
+ (
+ {"format": TranslationMemoryExportFormat.TMX},
+ {
+ "sourceLanguageId": None,
+ "targetLanguageId": None,
+ "format": TranslationMemoryExportFormat.TMX,
+ },
+ ),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
diff --git a/crowdin_api/api_resources/translation_memory/types.py b/crowdin_api/api_resources/translation_memory/types.py
index 44ac2072..0e3c4afd 100644
--- a/crowdin_api/api_resources/translation_memory/types.py
+++ b/crowdin_api/api_resources/translation_memory/types.py
@@ -23,7 +23,7 @@ class OrganizationConcordanceSearchRequest(TypedDict):
autoSubstitution: bool
minRelevant: int
expressions: Iterable[str]
- userId: Optional[int]
+ userId: Optional[int] # Crowdin only
class TranslationMemorySegmentRecord(TypedDict):
diff --git a/crowdin_api/api_resources/translation_status/enums.py b/crowdin_api/api_resources/translation_status/enums.py
index 237134ad..911fd663 100644
--- a/crowdin_api/api_resources/translation_status/enums.py
+++ b/crowdin_api/api_resources/translation_status/enums.py
@@ -53,3 +53,8 @@ class Validation(Enum):
WRONG_TRANSLATION_ISSUE_CHECK = "wrong_translation_issue_check"
SPELLCHECK = "spellcheck"
ICU_CHECK = "icu_check"
+
+
+class QaChecksRevalidationCategory(Enum):
+ TERMS = "terms"
+ AI = "ai"
diff --git a/crowdin_api/api_resources/translation_status/resource.py b/crowdin_api/api_resources/translation_status/resource.py
index 7a150d03..b06be2a9 100644
--- a/crowdin_api/api_resources/translation_status/resource.py
+++ b/crowdin_api/api_resources/translation_status/resource.py
@@ -1,7 +1,13 @@
+import warnings
from typing import Iterable, Optional
from crowdin_api.api_resources.abstract.resources import BaseResource
-from crowdin_api.api_resources.translation_status.enums import Category, Validation
+from crowdin_api.api_resources.translation_status.enums import (
+ Category,
+ QaChecksRevalidationCategory,
+ Validation,
+)
+from crowdin_api.api_resources.translation_status.types import ValidateQaChecksRequest
class TranslationStatusResource(BaseResource):
@@ -14,7 +20,7 @@ class TranslationStatusResource(BaseResource):
file, language, branch, directory.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Translation-Status
+ https://support.crowdin.com/developer/api/v2/#tag/Translation-Status
"""
def get_branch_progress(
@@ -29,7 +35,7 @@ def get_branch_progress(
Get Branch Progress.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.branches.languages.progress.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.branches.languages.progress.getMany
"""
projectId = projectId or self.get_project_id()
@@ -52,7 +58,7 @@ def get_directory_progress(
Get Directory Progress.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.directories.languages.progress.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.directories.languages.progress.getMany
"""
projectId = projectId or self.get_project_id()
@@ -75,7 +81,7 @@ def get_file_progress(
Get File Progress.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.files.languages.progress.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.files.languages.progress.getMany
"""
projectId = projectId or self.get_project_id()
@@ -98,7 +104,7 @@ def get_language_progress(
Get Language Progress.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.languages.files.progress.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.languages.files.progress.getMany
"""
projectId = projectId or self.get_project_id()
@@ -121,7 +127,7 @@ def get_project_progress(
Get Project Progress.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.languages.progress.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.languages.progress.getMany
"""
projectId = projectId or self.get_project_id()
@@ -134,17 +140,38 @@ def get_project_progress(
params=params,
)
+ def get_qa_checks_revalidation_path(self, projectId: int, revalidationId: Optional[str] = None):
+ if revalidationId is not None:
+ return f"projects/{projectId}/qa-checks/revalidate/{revalidationId}"
+
+ warnings.warn(
+ "Calling QA checks revalidation status/cancel without `revalidationId` is deprecated, "
+ "pass the `revalidationId` returned by `start_qa_checks_revalidation`",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+ return f"projects/{projectId}/qa-checks/revalidate"
+
def start_qa_checks_revalidation(
self,
projectId: Optional[int] = None,
+ qaCheckCategories: Optional[Iterable[QaChecksRevalidationCategory]] = None,
+ languageIds: Optional[Iterable[str]] = None,
+ failedOnly: Optional[bool] = None,
+ externalQaCheckIds: Optional[Iterable[int]] = None,
):
"""
Start QA Checks Revalidation.
Triggers a new QA checks revalidation job for the project.
+ `externalQaCheckIds` is available in Crowdin Enterprise only.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/Translation-Status/operation/api.projects.qa-checks.revalidate.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Translation-Status/operation/api.projects.qa-checks.revalidate.post
"""
projectId = projectId or self.get_project_id()
@@ -152,17 +179,25 @@ def start_qa_checks_revalidation(
return self.requester.request(
method="post",
path=f"projects/{projectId}/qa-checks/revalidate",
+ request_data={
+ "qaCheckCategories": qaCheckCategories,
+ "languageIds": languageIds,
+ "failedOnly": failedOnly,
+ "externalQaCheckIds": externalQaCheckIds,
+ },
)
def get_qa_checks_revalidation_status(
self,
projectId: Optional[int] = None,
+ revalidationId: Optional[str] = None,
):
"""
Get QA Checks Revalidation Status.
- Returns the status of the currently running or last completed QA checks
- revalidation job.
+ Returns the status of the QA checks revalidation job identified by `revalidationId`
+ (the identifier returned by `start_qa_checks_revalidation`). Omitting `revalidationId`
+ is deprecated.
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/Translation-Status/operation/api.projects.qa-checks.revalidate.get
@@ -172,17 +207,22 @@ def get_qa_checks_revalidation_status(
return self.requester.request(
method="get",
- path=f"projects/{projectId}/qa-checks/revalidate",
+ path=self.get_qa_checks_revalidation_path(
+ projectId=projectId, revalidationId=revalidationId
+ ),
)
def cancel_qa_checks_revalidation(
self,
projectId: Optional[int] = None,
+ revalidationId: Optional[str] = None,
):
"""
Cancel QA Checks Revalidation.
- Cancels the currently running QA checks revalidation job.
+ Cancels the QA checks revalidation job identified by `revalidationId`
+ (the identifier returned by `start_qa_checks_revalidation`). Omitting `revalidationId`
+ is deprecated.
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/Translation-Status/operation/api.projects.qa-checks.revalidate.delete
@@ -192,7 +232,34 @@ def cancel_qa_checks_revalidation(
return self.requester.request(
method="delete",
- path=f"projects/{projectId}/qa-checks/revalidate",
+ path=self.get_qa_checks_revalidation_path(
+ projectId=projectId, revalidationId=revalidationId
+ ),
+ )
+
+ def validate_text_by_qa_checks(
+ self,
+ data: Iterable[ValidateQaChecksRequest],
+ projectId: Optional[int] = None,
+ ):
+ """
+ Validate Text by QA Checks.
+
+ Runs QA checks against the passed texts without saving them as translations.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.validate-qa-checks.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.translations.validate-qa-checks.post
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="post",
+ path=f"projects/{projectId}/translations/validate-qa-checks",
+ request_data=data,
)
def list_qa_check_issues(
@@ -204,12 +271,17 @@ def list_qa_check_issues(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ taskId: Optional[int] = None,
+ fileId: Optional[int] = None,
+ branchId: Optional[int] = None,
):
"""
List QA Check Issues.
+ `fileId` is available in file-based projects only, `branchId` in string-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.qa-checks.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.qa-checks.getMany
"""
projectId = projectId or self.get_project_id()
@@ -217,6 +289,9 @@ def list_qa_check_issues(
"languageIds": None if languageIds is None else ",".join(languageIds),
"category": ",".join((item.value for item in category)) if category else None,
"validation": ",".join((item.value for item in validation)) if validation else None,
+ "taskId": taskId,
+ "fileId": fileId,
+ "branchId": branchId,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))
diff --git a/crowdin_api/api_resources/translation_status/tests/test_translation_status_resources.py b/crowdin_api/api_resources/translation_status/tests/test_translation_status_resources.py
index d5f56b1c..a90d3afa 100644
--- a/crowdin_api/api_resources/translation_status/tests/test_translation_status_resources.py
+++ b/crowdin_api/api_resources/translation_status/tests/test_translation_status_resources.py
@@ -1,7 +1,12 @@
from unittest import mock
import pytest
-from crowdin_api.api_resources.translation_status.enums import Category, Validation
+from crowdin_api.api_resources.enums import PluralCategoryName
+from crowdin_api.api_resources.translation_status.enums import (
+ Category,
+ QaChecksRevalidationCategory,
+ Validation,
+)
from crowdin_api.api_resources.translation_status.resource import TranslationStatusResource
from crowdin_api.requester import APIRequester
@@ -85,15 +90,44 @@ def test_get_project_progress(self, m_request, base_absolut_url):
path="projects/1/languages/progress",
)
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {},
+ {
+ "qaCheckCategories": None,
+ "languageIds": None,
+ "failedOnly": None,
+ "externalQaCheckIds": None,
+ },
+ ),
+ (
+ {
+ "qaCheckCategories": [QaChecksRevalidationCategory.AI],
+ "languageIds": ["uk", "fr"],
+ "failedOnly": True,
+ "externalQaCheckIds": [1, 2],
+ },
+ {
+ "qaCheckCategories": [QaChecksRevalidationCategory.AI],
+ "languageIds": ["uk", "fr"],
+ "failedOnly": True,
+ "externalQaCheckIds": [1, 2],
+ },
+ ),
+ ),
+ )
@mock.patch("crowdin_api.requester.APIRequester.request")
- def test_start_qa_checks_revalidation(self, m_request, base_absolut_url):
+ def test_start_qa_checks_revalidation(self, m_request, in_params, request_data, base_absolut_url):
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
- assert resource.start_qa_checks_revalidation(projectId=1) == "response"
+ assert resource.start_qa_checks_revalidation(projectId=1, **in_params) == "response"
m_request.assert_called_once_with(
method="post",
path="projects/1/qa-checks/revalidate",
+ request_data=request_data,
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -101,7 +135,24 @@ def test_get_qa_checks_revalidation_status(self, m_request, base_absolut_url):
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
- assert resource.get_qa_checks_revalidation_status(projectId=1) == "response"
+ assert (
+ resource.get_qa_checks_revalidation_status(projectId=1, revalidationId="rev-id")
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/qa-checks/revalidate/rev-id",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_qa_checks_revalidation_status_without_id_is_deprecated(
+ self, m_request, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ with pytest.warns(DeprecationWarning):
+ assert resource.get_qa_checks_revalidation_status(projectId=1) == "response"
m_request.assert_called_once_with(
method="get",
path="projects/1/qa-checks/revalidate",
@@ -112,12 +163,51 @@ def test_cancel_qa_checks_revalidation(self, m_request, base_absolut_url):
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
- assert resource.cancel_qa_checks_revalidation(projectId=1) == "response"
+ assert (
+ resource.cancel_qa_checks_revalidation(projectId=1, revalidationId="rev-id")
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="delete",
+ path="projects/1/qa-checks/revalidate/rev-id",
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_cancel_qa_checks_revalidation_without_id_is_deprecated(
+ self, m_request, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ with pytest.warns(DeprecationWarning):
+ assert resource.cancel_qa_checks_revalidation(projectId=1) == "response"
m_request.assert_called_once_with(
method="delete",
path="projects/1/qa-checks/revalidate",
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_validate_text_by_qa_checks(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {"stringId": 1, "languageId": "uk", "text": "text"},
+ {
+ "stringId": 2,
+ "languageId": "uk",
+ "text": "texts",
+ "pluralCategoryName": PluralCategoryName.FEW,
+ },
+ ]
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.validate_text_by_qa_checks(projectId=1, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/translations/validate-qa-checks",
+ request_data=data,
+ )
+
@pytest.mark.parametrize(
"in_params,request_params",
(
@@ -127,6 +217,9 @@ def test_cancel_qa_checks_revalidation(self, m_request, base_absolut_url):
"languageIds": None,
"category": None,
"validation": None,
+ "taskId": None,
+ "fileId": None,
+ "branchId": None,
"offset": 0,
"limit": 25,
},
@@ -141,6 +234,22 @@ def test_cancel_qa_checks_revalidation(self, m_request, base_absolut_url):
"languageIds": "some,string",
"category": "icu,empty",
"validation": "icu_check,tags_check",
+ "taskId": None,
+ "fileId": None,
+ "branchId": None,
+ "offset": 0,
+ "limit": 25,
+ },
+ ),
+ (
+ {"taskId": 1, "fileId": 2, "branchId": 3},
+ {
+ "languageIds": None,
+ "category": None,
+ "validation": None,
+ "taskId": 1,
+ "fileId": 2,
+ "branchId": 3,
"offset": 0,
"limit": 25,
},
diff --git a/crowdin_api/api_resources/translation_status/types.py b/crowdin_api/api_resources/translation_status/types.py
new file mode 100644
index 00000000..75e4a789
--- /dev/null
+++ b/crowdin_api/api_resources/translation_status/types.py
@@ -0,0 +1,14 @@
+from typing import Optional
+
+from crowdin_api.api_resources.enums import PluralCategoryName
+from crowdin_api.typing import TypedDict
+
+
+class _ValidateQaChecksRequestRequired(TypedDict):
+ stringId: int
+ languageId: str
+ text: str
+
+
+class ValidateQaChecksRequest(_ValidateQaChecksRequestRequired, total=False):
+ pluralCategoryName: Optional[PluralCategoryName]
diff --git a/crowdin_api/api_resources/translations/enums.py b/crowdin_api/api_resources/translations/enums.py
index 06d1eae7..547e1ba2 100644
--- a/crowdin_api/api_resources/translations/enums.py
+++ b/crowdin_api/api_resources/translations/enums.py
@@ -11,6 +11,7 @@ class PreTranslationAutoApproveOption(Enum):
ALL = "all"
EXCEPT_AUTO_SUBSTITUTED = "exceptAutoSubstituted"
PERFECT_MATCH_ONLY = "perfectMatchOnly"
+ PERFECT_MATCH_APPROVED_ONLY = "perfectMatchApprovedOnly"
NONE = "none"
@@ -36,3 +37,14 @@ class CharTransformation(Enum):
class PreTranslationEditOperation(Enum):
REPLACE = "replace"
TEST = "test"
+
+
+class PreTranslationPriority(Enum):
+ LOW = "low"
+ NORMAL = "normal"
+ HIGH = "high"
+
+
+class PreTranslationPatchPath(Enum):
+ STATUS = "/status"
+ PRIORITY = "/priority"
diff --git a/crowdin_api/api_resources/translations/resource.py b/crowdin_api/api_resources/translations/resource.py
index 2c5b29cd..257244ac 100644
--- a/crowdin_api/api_resources/translations/resource.py
+++ b/crowdin_api/api_resources/translations/resource.py
@@ -1,19 +1,25 @@
+import warnings
from typing import Dict, Iterable, Optional
+from deprecated import deprecated
+
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.enums import ExportProjectTranslationFormat
from crowdin_api.api_resources.translations.types import (
FallbackLanguages,
EditPreTranslationScheme,
+ ImportTranslationsOptions,
UploadTranslationRequest,
)
from crowdin_api.api_resources.translations.enums import (
CharTransformation,
PreTranslationApplyMethod,
PreTranslationAutoApproveOption,
+ PreTranslationPriority,
PreTranslationReplaceTranslationsOption,
PreTranslationScope,
)
+from crowdin_api.sorting import Sorting
class TranslationsResource(BaseResource):
@@ -28,7 +34,7 @@ class TranslationsResource(BaseResource):
are asynchronous operations and shall be completed with sequence of API methods.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Translations
+ https://support.crowdin.com/developer/api/v2/#tag/Translations
"""
def get_builds_path(self, projectId: int, buildId: Optional[int] = None):
@@ -43,8 +49,10 @@ def pre_translation_status(
"""
Pre-Translation Status.
+ `preTranslationId` is the auto-translation job identifier (`jobIdentifier` in the API reference).
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Translations/paths/~1projects~1{projectId}~1pre-translations~1{preTranslationId}/get
+ https://support.crowdin.com/developer/api/v2/#tag/Translations/paths/~1projects~1{projectId}~1pre-translations~1{preTranslationId}/get
"""
projectId = projectId or self.get_project_id()
@@ -60,6 +68,7 @@ def list_pre_translations(
page: Optional[int] = None,
offset: Optional[int] = None,
limit: Optional[int] = None,
+ orderBy: Optional[Sorting] = None,
):
"""
List Pre-Translations
@@ -69,9 +78,10 @@ def list_pre_translations(
"""
projectId = projectId or self.get_project_id()
- params = self.get_page_params(page=page, offset=offset, limit=limit)
+ params = {"orderBy": orderBy}
+ params.update(self.get_page_params(page=page, offset=offset, limit=limit))
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=f"projects/{projectId}/pre-translations",
params=params,
@@ -79,8 +89,8 @@ def list_pre_translations(
def apply_pre_translation(
self,
- languageIds: Iterable[str],
- fileIds: Iterable[int],
+ languageIds: Optional[Iterable[str]] = None,
+ fileIds: Optional[Iterable[int]] = None,
projectId: Optional[int] = None,
method: Optional[PreTranslationApplyMethod] = None,
engineId: Optional[int] = None,
@@ -98,15 +108,36 @@ def apply_pre_translation(
labelIds: Optional[Iterable[int]] = None,
excludeLabelIds: Optional[Iterable[int]] = None,
branchIds: Optional[Iterable[int]] = None,
+ directoryIds: Optional[Iterable[int]] = None,
+ taskId: Optional[int] = None,
+ priority: Optional[PreTranslationPriority] = None,
+ translationModifiedAfter: Optional[str] = None,
+ notifyOnCompletion: Optional[bool] = None,
+ sourceLanguageId: Optional[str] = None,
+ customInstruction: Optional[str] = None,
+ minimumMatchRatio: Optional[int] = None,
):
"""
Apply Pre-Translation.
+ Pre-translate by files: pass `languageIds` together with `fileIds`, `directoryIds`
+ (file-based projects only) or `branchIds`. `fileIds` is required only when neither
+ `directoryIds` nor `branchIds` is set.
+
+ Pre-translate by task: pass `taskId` (the target language is taken from the task),
+ `languageIds` and files are not required in this case.
+
`translateUntranslatedOnly` is deprecated in favor of `scope` and cannot be
combined with it in the same request.
+ `sourceLanguageId` is available in Crowdin only, `minimumMatchRatio` in
+ Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.pre-translations.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.pre-translations.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.pre-translations.post
"""
if translateUntranslatedOnly is not None and scope is not None:
raise ValueError(
@@ -114,6 +145,12 @@ def apply_pre_translation(
"cannot be combined with it in the same request."
)
+ if translateUntranslatedOnly is not None:
+ warnings.warn(
+ "`translateUntranslatedOnly` is deprecated, use `scope` instead",
+ DeprecationWarning,
+ )
+
if fallbackLanguages is None:
fallbackLanguages = []
@@ -150,6 +187,14 @@ def apply_pre_translation(
"labelIds": labelIds,
"excludeLabelIds": excludeLabelIds,
"branchIds": branchIds,
+ "directoryIds": directoryIds,
+ "taskId": taskId,
+ "priority": priority,
+ "translationModifiedAfter": translationModifiedAfter,
+ "notifyOnCompletion": notifyOnCompletion,
+ "sourceLanguageId": sourceLanguageId,
+ "customInstruction": customInstruction,
+ "minimumMatchRatio": minimumMatchRatio,
},
)
@@ -180,6 +225,8 @@ def edit_pre_translation(
"""
Edit Pre-Translation
+ Supported patch paths: `PreTranslationPatchPath` (`/status`, `/priority`).
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/Translations/operation/api.projects.pre-translations.patch
"""
@@ -218,12 +265,21 @@ def build_project_directory_translation(
skipUntranslatedStrings: Optional[bool] = None,
skipUntranslatedFiles: Optional[bool] = None,
exportApprovedOnly: Optional[bool] = None,
+ exportWithMinApprovalsCount: Optional[int] = None,
+ exportStringsThatPassedWorkflow: Optional[bool] = None,
+ preserveFolderHierarchy: Optional[bool] = None,
):
"""
Build Project Directory Translation.
+ `exportApprovedOnly` is available in Crowdin only; `exportWithMinApprovalsCount` and
+ `exportStringsThatPassedWorkflow` in Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.directories.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.directories.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.translations.builds.directories.post
"""
projectId = projectId or self.get_project_id()
@@ -236,6 +292,9 @@ def build_project_directory_translation(
"skipUntranslatedStrings": skipUntranslatedStrings,
"skipUntranslatedFiles": skipUntranslatedFiles,
"exportApprovedOnly": exportApprovedOnly,
+ "exportWithMinApprovalsCount": exportWithMinApprovalsCount,
+ "exportStringsThatPassedWorkflow": exportStringsThatPassedWorkflow,
+ "preserveFolderHierarchy": preserveFolderHierarchy,
},
)
@@ -248,12 +307,20 @@ def build_project_file_translation(
skipUntranslatedFiles: Optional[bool] = None,
exportApprovedOnly: Optional[bool] = None,
eTag: Optional[str] = None,
+ exportWithMinApprovalsCount: Optional[int] = None,
+ exportStringsThatPassedWorkflow: Optional[bool] = None,
):
"""
Build Project File Translation.
+ `exportApprovedOnly` is available in Crowdin only; `exportWithMinApprovalsCount` and
+ `exportStringsThatPassedWorkflow` in Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.files.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.files.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.translations.builds.files.post
"""
if eTag is not None:
@@ -272,6 +339,8 @@ def build_project_file_translation(
"skipUntranslatedStrings": skipUntranslatedStrings,
"skipUntranslatedFiles": skipUntranslatedFiles,
"exportApprovedOnly": exportApprovedOnly,
+ "exportWithMinApprovalsCount": exportWithMinApprovalsCount,
+ "exportStringsThatPassedWorkflow": exportStringsThatPassedWorkflow,
},
)
@@ -287,7 +356,7 @@ def list_project_builds(
List Project Builds.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.getMany
"""
projectId = projectId or self.get_project_id()
@@ -307,7 +376,7 @@ def build_project_translation(
Build Project Translation.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.post
"""
projectId = projectId or self.get_project_id()
@@ -327,12 +396,19 @@ def build_crowdin_project_translation(
skipUntranslatedFiles: Optional[bool] = None,
exportApprovedOnly: Optional[bool] = None,
exportWithMinApprovalsCount: Optional[int] = None,
+ exportStringsThatPassedWorkflow: Optional[bool] = None,
):
"""
Build Project Translation(Crowdin Translation Create Project Build Form).
+ File-based projects only. `exportApprovedOnly` is available in Crowdin only;
+ `exportWithMinApprovalsCount` and `exportStringsThatPassedWorkflow` in Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.translations.builds.post
"""
projectId = projectId or self.get_project_id()
@@ -346,6 +422,7 @@ def build_crowdin_project_translation(
"skipUntranslatedFiles": skipUntranslatedFiles,
"exportApprovedOnly": exportApprovedOnly,
"exportWithMinApprovalsCount": exportWithMinApprovalsCount,
+ "exportStringsThatPassedWorkflow": exportStringsThatPassedWorkflow,
},
)
@@ -357,12 +434,15 @@ def build_pseudo_project_translation(
suffix: Optional[str] = None,
lengthTransformation: Optional[int] = None,
charTransformation: Optional[CharTransformation] = None,
+ branchId: Optional[int] = None,
):
"""
Build Project Translation(Translation Create Project Pseudo Build Form).
+ File-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.post
"""
projectId = projectId or self.get_project_id()
@@ -375,9 +455,11 @@ def build_pseudo_project_translation(
"suffix": suffix,
"lengthTransformation": lengthTransformation,
"charTransformation": charTransformation,
+ "branchId": branchId,
},
)
+ @deprecated("Use `import_translations` instead")
def upload_translation(
self,
languageId: str,
@@ -392,8 +474,10 @@ def upload_translation(
"""
Upload Translations.
+ Deprecated by the API: use `import_translations` instead.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.postOnLanguage
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.postOnLanguage
"""
projectId = projectId or self.get_project_id()
@@ -419,7 +503,7 @@ def download_project_translations(
Download Project Translations.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.download.download
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.download.download
"""
projectId = projectId or self.get_project_id()
@@ -434,7 +518,7 @@ def check_project_build_status(self, buildId: int, projectId: Optional[int] = No
Check Project Build Status.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.get
"""
projectId = projectId or self.get_project_id()
@@ -449,7 +533,7 @@ def cancel_build(self, buildId: int, projectId: Optional[int] = None):
Cancel Build.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.builds.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.builds.delete
"""
projectId = projectId or self.get_project_id()
@@ -471,12 +555,21 @@ def export_project_translation(
skipUntranslatedStrings: Optional[bool] = None,
skipUntranslatedFiles: Optional[bool] = None,
exportApprovedOnly: Optional[bool] = None,
+ exportWithMinApprovalsCount: Optional[int] = None,
+ exportStringsThatPassedWorkflow: Optional[bool] = None,
):
"""
Export Project Translation.
+ `directoryIds`, `fileIds` and `skipUntranslatedFiles` are available in file-based projects only.
+ `exportApprovedOnly` is available in Crowdin only; `exportWithMinApprovalsCount` and
+ `exportStringsThatPassedWorkflow` in Crowdin Enterprise only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.translations.exports.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.translations.exports.post
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.translations.exports.post
"""
projectId = projectId or self.get_project_id()
@@ -494,6 +587,8 @@ def export_project_translation(
"skipUntranslatedStrings": skipUntranslatedStrings,
"skipUntranslatedFiles": skipUntranslatedFiles,
"exportApprovedOnly": exportApprovedOnly,
+ "exportWithMinApprovalsCount": exportWithMinApprovalsCount,
+ "exportStringsThatPassedWorkflow": exportStringsThatPassedWorkflow,
},
)
@@ -501,16 +596,21 @@ def import_translations(
self,
project_id: int,
storage_id: int,
- language_ids: Optional[Iterable[int]] = None,
+ language_ids: Optional[Iterable[str]] = None,
file_id: Optional[int] = None,
import_eq_suggestions: Optional[bool] = None,
auto_approve_imported: Optional[bool] = None,
translate_hidden: Optional[bool] = None,
add_to_tm: Optional[bool] = None,
+ branch_id: Optional[int] = None,
+ import_options: Optional[ImportTranslationsOptions] = None,
):
"""
Import Translations
+ `file_id` is used in file-based projects; `branch_id` and `import_options`
+ (spreadsheet columns mapping) in string-based projects.
+
Link to documentation:
https://support.crowdin.com/developer/api/v2/#tag/Translations/operation/api.projects.translations.imports
"""
@@ -526,6 +626,8 @@ def import_translations(
"autoApproveImported": auto_approve_imported,
"translateHidden": translate_hidden,
"addToTm": add_to_tm,
+ "branchId": branch_id,
+ "importOptions": import_options,
}
)
diff --git a/crowdin_api/api_resources/translations/tests/test_translations_resources.py b/crowdin_api/api_resources/translations/tests/test_translations_resources.py
index 176fcf17..d26d9094 100644
--- a/crowdin_api/api_resources/translations/tests/test_translations_resources.py
+++ b/crowdin_api/api_resources/translations/tests/test_translations_resources.py
@@ -1,3 +1,4 @@
+from enum import Enum
from unittest import mock
import pytest
@@ -7,11 +8,18 @@
PreTranslationApplyMethod,
PreTranslationAutoApproveOption,
PreTranslationEditOperation,
+ PreTranslationPatchPath,
+ PreTranslationPriority,
PreTranslationReplaceTranslationsOption,
PreTranslationScope,
)
from crowdin_api.api_resources.translations.resource import TranslationsResource
from crowdin_api.requester import APIRequester
+from crowdin_api.sorting import Sorting, SortingOrder, SortingRule
+
+
+class ListPreTranslationsTestOrderBy(Enum):
+ ID = "id"
class TestTranslationsResource:
@@ -43,7 +51,7 @@ def test_list_pre_translations(self, m_request, base_absolut_url):
m_request.return_value = "response"
resource = self.get_resource(base_absolut_url)
- params = resource.get_page_params()
+ params = {"orderBy": None, **resource.get_page_params()}
assert resource.list_pre_translations(projectId=1) == "response"
m_request.assert_called_once_with(
method="get",
@@ -51,6 +59,126 @@ def test_list_pre_translations(self, m_request, base_absolut_url):
params=params,
)
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_pre_translations_with_order_by(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ order_by = Sorting([SortingRule(ListPreTranslationsTestOrderBy.ID, SortingOrder.DESC)])
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.list_pre_translations(projectId=1, orderBy=order_by, limit=10) == "response"
+ m_request.assert_called_once_with(
+ method="get",
+ path="projects/1/pre-translations",
+ params={"orderBy": order_by, "offset": 0, "limit": 10},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_apply_pre_translation_by_directories_and_new_fields(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.apply_pre_translation(
+ projectId=1,
+ languageIds=["uk"],
+ directoryIds=[3, 4],
+ method=PreTranslationApplyMethod.AI,
+ aiPromptId=5,
+ autoApproveOption=PreTranslationAutoApproveOption.PERFECT_MATCH_APPROVED_ONLY,
+ scope=PreTranslationScope.ALL,
+ priority=PreTranslationPriority.HIGH,
+ translationModifiedAfter="2025-01-01T00:00:00+00:00",
+ notifyOnCompletion=True,
+ sourceLanguageId="en",
+ customInstruction="Be concise",
+ minimumMatchRatio=80,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/pre-translations",
+ request_data={
+ "languageIds": ["uk"],
+ "fileIds": None,
+ "method": PreTranslationApplyMethod.AI,
+ "engineId": None,
+ "aiPromptId": 5,
+ "autoApproveOption": PreTranslationAutoApproveOption.PERFECT_MATCH_APPROVED_ONLY,
+ "duplicateTranslations": None,
+ "skipApprovedTranslations": None,
+ "translateUntranslatedOnly": None,
+ "scope": PreTranslationScope.ALL,
+ "translationModifiedBefore": None,
+ "replaceTranslationsOption": None,
+ "resetApprovalStatus": None,
+ "translateWithPerfectMatchOnly": None,
+ "fallbackLanguages": [],
+ "labelIds": [],
+ "excludeLabelIds": [],
+ "branchIds": [],
+ "directoryIds": [3, 4],
+ "taskId": None,
+ "priority": PreTranslationPriority.HIGH,
+ "translationModifiedAfter": "2025-01-01T00:00:00+00:00",
+ "notifyOnCompletion": True,
+ "sourceLanguageId": "en",
+ "customInstruction": "Be concise",
+ "minimumMatchRatio": 80,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_apply_pre_translation_by_task(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.apply_pre_translation(projectId=1, taskId=7) == "response"
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/pre-translations",
+ request_data={
+ "languageIds": None,
+ "fileIds": None,
+ "method": None,
+ "engineId": None,
+ "aiPromptId": None,
+ "autoApproveOption": None,
+ "duplicateTranslations": None,
+ "skipApprovedTranslations": None,
+ "translateUntranslatedOnly": None,
+ "scope": None,
+ "translationModifiedBefore": None,
+ "replaceTranslationsOption": None,
+ "resetApprovalStatus": None,
+ "translateWithPerfectMatchOnly": None,
+ "fallbackLanguages": [],
+ "labelIds": [],
+ "excludeLabelIds": [],
+ "branchIds": [],
+ "directoryIds": None,
+ "taskId": 7,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_apply_pre_translation_translate_untranslated_only_warns(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ with pytest.warns(DeprecationWarning):
+ resource.apply_pre_translation(
+ projectId=1, languageIds=["uk"], fileIds=[1], translateUntranslatedOnly=True
+ )
+ m_request.assert_called_once()
+
@pytest.mark.parametrize(
"in_params, request_data",
(
@@ -78,6 +206,14 @@ def test_list_pre_translations(self, m_request, base_absolut_url):
"labelIds": [],
"excludeLabelIds": [],
"branchIds": [],
+ "directoryIds": None,
+ "taskId": None,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
},
),
(
@@ -118,6 +254,14 @@ def test_list_pre_translations(self, m_request, base_absolut_url):
"labelIds": [1],
"excludeLabelIds": [1],
"branchIds": [],
+ "directoryIds": None,
+ "taskId": None,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
},
),
),
@@ -169,6 +313,14 @@ def test_apply_pre_translation_with_branchids(self, m_request, base_absolut_url)
"labelIds": [],
"excludeLabelIds": [],
"branchIds": [10, 20, 30],
+ "directoryIds": None,
+ "taskId": None,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
},
path="projects/1/pre-translations",
)
@@ -212,6 +364,14 @@ def test_apply_pre_translation_edge_cases(self, m_request, base_absolut_url):
"labelIds": [],
"excludeLabelIds": [],
"branchIds": [],
+ "directoryIds": None,
+ "taskId": None,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
},
path="projects/1/pre-translations",
)
@@ -267,6 +427,14 @@ def test_apply_pre_translation_with_all_optional_params(self, m_request, base_ab
"labelIds": [1, 2, 3],
"excludeLabelIds": [4, 5],
"branchIds": [10, 20],
+ "directoryIds": None,
+ "taskId": None,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
},
path="projects/1/pre-translations",
)
@@ -312,6 +480,14 @@ def test_apply_pre_translation_with_scope_and_retranslation_options(
"labelIds": [],
"excludeLabelIds": [],
"branchIds": [],
+ "directoryIds": None,
+ "taskId": None,
+ "priority": None,
+ "translationModifiedAfter": None,
+ "notifyOnCompletion": None,
+ "sourceLanguageId": None,
+ "customInstruction": None,
+ "minimumMatchRatio": None,
},
path="projects/1/pre-translations",
)
@@ -354,7 +530,7 @@ def test_edit_bundle(self, m_request, base_absolut_url):
{
"value": "value",
"op": PreTranslationEditOperation.REPLACE,
- "path": "/status",
+ "path": PreTranslationPatchPath.STATUS,
}
]
@@ -401,6 +577,8 @@ def test_pre_translation_batch_operations(self, m_request, base_absolut_url):
"skipUntranslatedStrings": None,
"skipUntranslatedFiles": None,
"exportApprovedOnly": None,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
},
None,
),
@@ -417,6 +595,8 @@ def test_pre_translation_batch_operations(self, m_request, base_absolut_url):
"skipUntranslatedStrings": False,
"skipUntranslatedFiles": False,
"exportApprovedOnly": False,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
},
{"If-None-Match": "eTag"},
),
@@ -481,6 +661,7 @@ def test_build_project_translation(self, m_request, base_absolut_url):
"skipUntranslatedFiles": None,
"exportApprovedOnly": None,
"exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
},
),
(
@@ -499,6 +680,7 @@ def test_build_project_translation(self, m_request, base_absolut_url):
"skipUntranslatedFiles": True,
"exportApprovedOnly": False,
"exportWithMinApprovalsCount": True,
+ "exportStringsThatPassedWorkflow": None,
},
),
),
@@ -527,6 +709,7 @@ def test_build_crowdin_project_translation(
"suffix": None,
"lengthTransformation": None,
"charTransformation": None,
+ "branchId": None,
},
),
(
@@ -543,6 +726,7 @@ def test_build_crowdin_project_translation(
"suffix": "nohtyp",
"lengthTransformation": 2,
"charTransformation": CharTransformation.ARABIC,
+ "branchId": None,
},
),
),
@@ -707,6 +891,8 @@ def test_cancel_build(self, m_request, base_absolut_url):
"skipUntranslatedStrings": None,
"skipUntranslatedFiles": None,
"exportApprovedOnly": None,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
},
),
(
@@ -731,6 +917,8 @@ def test_cancel_build(self, m_request, base_absolut_url):
"skipUntranslatedStrings": False,
"skipUntranslatedFiles": True,
"exportApprovedOnly": False,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
},
),
),
@@ -767,7 +955,9 @@ def test_export_project_translation(self, m_request, in_params, request_data, ba
"importEqSuggestions": True,
"autoApproveImported": True,
"translateHidden": True,
- "addToTm": True
+ "addToTm": True,
+ "branchId": None,
+ "importOptions": None,
},
),
)
@@ -816,3 +1006,207 @@ def test_import_translations_report(self, m_request, base_absolut_url):
method="get",
path=f"projects/{project_id}/translations/imports/{import_translation_id}/report",
)
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {},
+ {
+ "targetLanguageIds": None,
+ "skipUntranslatedStrings": None,
+ "skipUntranslatedFiles": None,
+ "exportApprovedOnly": None,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": None,
+ "preserveFolderHierarchy": None,
+ },
+ ),
+ (
+ {
+ "targetLanguageIds": ["uk"],
+ "skipUntranslatedStrings": True,
+ "skipUntranslatedFiles": False,
+ "exportApprovedOnly": True,
+ "exportWithMinApprovalsCount": 2,
+ "exportStringsThatPassedWorkflow": False,
+ "preserveFolderHierarchy": True,
+ },
+ {
+ "targetLanguageIds": ["uk"],
+ "skipUntranslatedStrings": True,
+ "skipUntranslatedFiles": False,
+ "exportApprovedOnly": True,
+ "exportWithMinApprovalsCount": 2,
+ "exportStringsThatPassedWorkflow": False,
+ "preserveFolderHierarchy": True,
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_build_project_directory_translation(
+ self, m_request, in_params, request_data, base_absolut_url
+ ):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.build_project_directory_translation(projectId=1, directoryId=3, **in_params)
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/translations/builds/directories/3",
+ request_data=request_data,
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_build_project_file_translation_enterprise_fields(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.build_project_file_translation(
+ projectId=1,
+ fileId=2,
+ targetLanguageId="uk",
+ exportWithMinApprovalsCount=1,
+ exportStringsThatPassedWorkflow=True,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ headers=None,
+ path="projects/1/translations/builds/files/2",
+ request_data={
+ "targetLanguageId": "uk",
+ "skipUntranslatedStrings": None,
+ "skipUntranslatedFiles": None,
+ "exportApprovedOnly": None,
+ "exportWithMinApprovalsCount": 1,
+ "exportStringsThatPassedWorkflow": True,
+ },
+ )
+
+ @mock.patch(
+ "crowdin_api.api_resources.translations.resource."
+ "TranslationsResource.build_project_translation"
+ )
+ def test_build_crowdin_project_translation_passed_workflow(
+ self, m_build_project_translation, base_absolut_url
+ ):
+ m_build_project_translation.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.build_crowdin_project_translation(
+ projectId=1, exportStringsThatPassedWorkflow=True
+ )
+ == "response"
+ )
+ m_build_project_translation.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "branchId": None,
+ "targetLanguageIds": None,
+ "skipUntranslatedStrings": None,
+ "skipUntranslatedFiles": None,
+ "exportApprovedOnly": None,
+ "exportWithMinApprovalsCount": None,
+ "exportStringsThatPassedWorkflow": True,
+ },
+ )
+
+ @mock.patch(
+ "crowdin_api.api_resources.translations.resource."
+ "TranslationsResource.build_project_translation"
+ )
+ def test_build_pseudo_project_translation_with_branch(
+ self, m_build_project_translation, base_absolut_url
+ ):
+ m_build_project_translation.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.build_pseudo_project_translation(projectId=1, pseudo=True, branchId=4) == "response"
+ m_build_project_translation.assert_called_once_with(
+ projectId=1,
+ request_data={
+ "pseudo": True,
+ "prefix": None,
+ "suffix": None,
+ "lengthTransformation": None,
+ "charTransformation": None,
+ "branchId": 4,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_export_project_translation_enterprise_fields(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.export_project_translation(
+ projectId=1,
+ targetLanguageId="uk",
+ exportWithMinApprovalsCount=0,
+ exportStringsThatPassedWorkflow=True,
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/translations/exports",
+ request_data={
+ "targetLanguageId": "uk",
+ "format": None,
+ "labelIds": None,
+ "branchIds": None,
+ "directoryIds": None,
+ "fileIds": None,
+ "skipUntranslatedStrings": None,
+ "skipUntranslatedFiles": None,
+ "exportApprovedOnly": None,
+ "exportWithMinApprovalsCount": 0,
+ "exportStringsThatPassedWorkflow": True,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_import_translations_string_based(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ import_options = {"scheme": {"identifier": 0, "en": 1, "uk": 2}}
+
+ resource = self.get_resource(base_absolut_url)
+ assert (
+ resource.import_translations(
+ 1, 2, language_ids=["uk"], branch_id=3, import_options=import_options
+ )
+ == "response"
+ )
+ m_request.assert_called_once_with(
+ method="post",
+ path="projects/1/translations/imports",
+ request_data={
+ "storageId": 2,
+ "languageIds": ["uk"],
+ "fileId": None,
+ "importEqSuggestions": None,
+ "autoApproveImported": None,
+ "translateHidden": None,
+ "addToTm": None,
+ "branchId": 3,
+ "importOptions": import_options,
+ },
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_upload_translation_is_deprecated(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ with pytest.warns(DeprecationWarning):
+ resource.upload_translation(projectId=1, languageId="uk", storageId=1, fileId=2)
diff --git a/crowdin_api/api_resources/translations/types.py b/crowdin_api/api_resources/translations/types.py
index d42b55a6..4ac5d147 100644
--- a/crowdin_api/api_resources/translations/types.py
+++ b/crowdin_api/api_resources/translations/types.py
@@ -1,6 +1,9 @@
-from typing import Iterable, Optional
+from typing import Dict, Iterable, Optional, Union
from crowdin_api.typing import TypedDict
-from crowdin_api.api_resources.translations.enums import PreTranslationEditOperation
+from crowdin_api.api_resources.translations.enums import (
+ PreTranslationEditOperation,
+ PreTranslationPatchPath,
+)
class FallbackLanguages(TypedDict):
@@ -9,7 +12,7 @@ class FallbackLanguages(TypedDict):
class EditPreTranslationScheme(TypedDict):
op: PreTranslationEditOperation
- path: str
+ path: Union[PreTranslationPatchPath, str]
value: str
@@ -20,3 +23,14 @@ class UploadTranslationRequest(TypedDict):
autoApproveImported: Optional[bool]
translateHidden: Optional[bool]
addToTm: Optional[bool]
+
+
+class ImportTranslationsOptions(TypedDict, total=False):
+ """
+ Import options for spreadsheet files in string-based projects.
+
+ `scheme` maps column names (`none`, `identifier`, `sourceOrTranslation`, `translation`
+ or a language identifier such as `en`) to column numbers, starting at 0.
+ """
+
+ scheme: Dict[str, int]
diff --git a/crowdin_api/api_resources/users/enums.py b/crowdin_api/api_resources/users/enums.py
index 559873ee..519f4daa 100644
--- a/crowdin_api/api_resources/users/enums.py
+++ b/crowdin_api/api_resources/users/enums.py
@@ -14,6 +14,15 @@ class UserPatchPath(Enum):
LAST_NAME = "/lastName"
TIMEZONE = "/timezone"
STATUS = "/status"
+ ADMIN_ACCESS = "/adminAccess"
+ AVATAR_STORAGE_ID = "/avatarStorageId"
+
+
+class AuthenticatedUserPatchPath(Enum):
+ USERNAME = "/username"
+ FULL_NAME = "/fullName"
+ TIMEZONE = "/timezone"
+ AVATAR_STORAGE_ID = "/avatarStorageId"
class ProjectRole(Enum):
diff --git a/crowdin_api/api_resources/users/resource.py b/crowdin_api/api_resources/users/resource.py
index 8cd21286..4292ef10 100644
--- a/crowdin_api/api_resources/users/resource.py
+++ b/crowdin_api/api_resources/users/resource.py
@@ -3,14 +3,32 @@
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.users.enums import UserRole, OrganizationRole, UserStatus
-from crowdin_api.api_resources.users.types import UserPatchRequest, ProjectMemberRole, GroupManagerPatchRequest
+from crowdin_api.api_resources.users.types import (
+ UserPatchRequest,
+ ProjectMemberRole,
+ GroupManagerPatchRequest,
+ AuthenticatedUserPatchRequest,
+ UserProjectPermissionsPatchRequest,
+)
from crowdin_api.sorting import Sorting
from crowdin_api.utils import convert_to_query_string, convert_enum_to_string_if_exists
class BaseUsersResource(BaseResource):
+ """
+ Base class with methods shared by `UsersResource` and `EnterpriseUsersResource`.
+ """
def get_authenticated_user(self):
+ """
+ Get Authenticated User.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.user.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.user.get
+ """
return self.requester.request(method="get", path="user")
def get_members_path(self, projectId: int, memberId: Optional[int] = None):
@@ -34,7 +52,7 @@ def _list_project_members(
List Project Members.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.members.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.members.getMany
"""
projectId = projectId or self.get_project_id()
@@ -50,6 +68,45 @@ def _list_project_members(
params=params,
)
+ def get_member_info(self, memberId: int, projectId: Optional[int] = None):
+ """
+ Get Member Info.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.members.get
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.members.get
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="get",
+ path=self.get_members_path(projectId=projectId, memberId=memberId)
+ )
+
+ def delete_member_from_project(
+ self,
+ memberId: int,
+ projectId: Optional[int] = None,
+ ):
+ """
+ Delete Member From Project.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.members.delete
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.members.delete
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="delete", path=self.get_members_path(projectId=projectId, memberId=memberId)
+ )
+
class UsersResource(BaseUsersResource):
"""
@@ -59,7 +116,7 @@ class UsersResource(BaseUsersResource):
authenticated user.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Users
+ https://support.crowdin.com/developer/api/v2/#tag/Users
"""
def list_project_members(
@@ -77,7 +134,7 @@ def list_project_members(
List Project Members.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.members.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.members.getMany
"""
return self._list_project_members(
projectId=projectId,
@@ -90,19 +147,80 @@ def list_project_members(
extraParams={"role": role}
)
- def get_member_info(self, memberId: int, projectId: Optional[int] = None):
+ def edit_authenticated_user(self, data: Iterable[AuthenticatedUserPatchRequest]):
"""
- Get Member Info.
+ Edit Authenticated User.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.user.patch
+ """
+
+ return self.requester.request(method="patch", path="user", request_data=data)
+
+ def add_project_member(
+ self,
+ userIds: Optional[Iterable[int]] = None,
+ projectId: Optional[int] = None,
+ usernames: Optional[Iterable[str]] = None,
+ emails: Optional[Iterable[str]] = None,
+ managerAccess: Optional[bool] = None,
+ developerAccess: Optional[bool] = None,
+ roles: Optional[Iterable[ProjectMemberRole]] = None,
+ message: Optional[str] = None,
+ ):
+ """
+ Add Project Member.
+
+ One of `userIds`, `usernames` or `emails` is required.
+ `managerAccess`, `developerAccess` and `roles` are mutually exclusive.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.members.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.members.post
"""
projectId = projectId or self.get_project_id()
return self.requester.request(
- method="get",
- path=self.get_members_path(projectId=projectId, memberId=memberId)
+ method="post",
+ path=self.get_members_path(projectId=projectId),
+ request_data={
+ "userIds": userIds,
+ "usernames": usernames,
+ "emails": emails,
+ "managerAccess": managerAccess,
+ "developerAccess": developerAccess,
+ "roles": roles,
+ "message": message,
+ },
+ )
+
+ def replace_project_member_permissions(
+ self,
+ memberId: int,
+ projectId: Optional[int] = None,
+ managerAccess: Optional[bool] = None,
+ developerAccess: Optional[bool] = None,
+ roles: Optional[Iterable[ProjectMemberRole]] = None,
+ ):
+ """
+ Replace Project Member Permissions.
+
+ `managerAccess`, `developerAccess` and `roles` are mutually exclusive.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.members.put
+ """
+
+ projectId = projectId or self.get_project_id()
+
+ return self.requester.request(
+ method="put",
+ path=self.get_members_path(projectId=projectId, memberId=memberId),
+ request_data={
+ "managerAccess": managerAccess,
+ "developerAccess": developerAccess,
+ "roles": roles,
+ },
)
@@ -114,7 +232,7 @@ class EnterpriseUsersResource(BaseUsersResource):
authenticated user.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Users
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Users
"""
def get_users_path(self, userId: Optional[int] = None):
if userId is not None:
@@ -132,7 +250,9 @@ def list_group_managers(
self,
group_id: int,
team_ids: Optional[Iterable[int]] = None,
- order_by: Optional[Sorting] = None
+ order_by: Optional[Sorting] = None,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
):
"""
List Group Managers
@@ -142,11 +262,12 @@ def list_group_managers(
"""
params = {
- "team_ids": ",".join(str(teamId) for teamId in team_ids) if team_ids is not None else None,
- "order_by": order_by
+ "teamIds": ",".join(str(teamId) for teamId in team_ids) if team_ids is not None else None,
+ "orderBy": order_by
}
+ params.update(self.get_page_params(offset=offset, limit=limit))
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_group_managers_path(group_id),
params=params
@@ -202,7 +323,7 @@ def list_project_members(
List Project Members.
Link to documentation for enterprise:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.members.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.members.getMany
"""
return self._list_project_members(
@@ -218,18 +339,25 @@ def list_project_members(
def add_project_member(
self,
- userIds: Iterable[int],
+ userIds: Optional[Iterable[int]] = None,
projectId: Optional[int] = None,
accessToAllWorkflowSteps: Optional[bool] = None,
managerAccess: Optional[bool] = None,
permissions: Optional[Dict] = None,
- roles: Optional[Iterable[ProjectMemberRole]] = None
+ roles: Optional[Iterable[ProjectMemberRole]] = None,
+ usernames: Optional[Iterable[str]] = None,
+ emails: Optional[Iterable[str]] = None,
+ developerAccess: Optional[bool] = None,
):
"""
Add Project Member.
+ One of `userIds`, `usernames` or `emails` is required.
+
+ `accessToAllWorkflowSteps` and `permissions` are deprecated by the API, use `roles` instead.
+
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.members.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.members.post
"""
projectId = projectId or self.get_project_id()
@@ -239,8 +367,11 @@ def add_project_member(
path=self.get_members_path(projectId=projectId),
request_data={
"userIds": userIds,
+ "usernames": usernames,
+ "emails": emails,
"accessToAllWorkflowSteps": accessToAllWorkflowSteps,
"managerAccess": managerAccess,
+ "developerAccess": developerAccess,
"permissions": permissions,
"roles": roles
},
@@ -253,13 +384,16 @@ def replace_project_member_permissions(
accessToAllWorkflowSteps: Optional[bool] = None,
managerAccess: Optional[bool] = None,
permissions: Optional[Dict] = None,
- roles: Optional[Iterable[ProjectMemberRole]] = None
+ roles: Optional[Iterable[ProjectMemberRole]] = None,
+ developerAccess: Optional[bool] = None,
):
"""
Replace Project Member Permissions.
+ `accessToAllWorkflowSteps` and `permissions` are deprecated by the API, use `roles` instead.
+
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.members.put
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.members.put
"""
projectId = projectId or self.get_project_id()
@@ -270,41 +404,25 @@ def replace_project_member_permissions(
request_data={
"accessToAllWorkflowSteps": accessToAllWorkflowSteps,
"managerAccess": managerAccess,
+ "developerAccess": developerAccess,
"permissions": permissions,
"roles": roles
},
)
- def delete_member_from_project(
- self,
- memberId: int,
- projectId: Optional[int] = None,
- ):
- """
- Delete Member From Project.
-
- Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.members.delete
- """
-
- projectId = projectId or self.get_project_id()
-
- return self.requester.request(
- method="delete", path=self.get_members_path(projectId=projectId, memberId=memberId)
- )
-
def invite_user(
self,
email: str,
firstName: Optional[str] = None,
lastName: Optional[str] = None,
- timezone: Optional[str] = None
+ timezone: Optional[str] = None,
+ adminAccess: Optional[bool] = None,
):
"""
Invite User.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.post
"""
return self.requester.request(
method="post",
@@ -313,16 +431,83 @@ def invite_user(
"email": email,
"firstName": firstName,
"lastName": lastName,
- "timezone": timezone
+ "timezone": timezone,
+ "adminAccess": adminAccess,
}
)
+ def get_user(self, userId: int):
+ """
+ Get User.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.getById
+ """
+
+ return self.requester.request(method="get", path=self.get_users_path(userId=userId))
+
+ def list_user_project_contributions(
+ self,
+ userId: int,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List User Project Contributions.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.projects.contributions.getMany
+ """
+
+ return self._get_entire_data(
+ method="get",
+ path=f"{self.get_users_path(userId=userId)}/projects/contributions",
+ params=self.get_page_params(offset=offset, limit=limit),
+ )
+
+ def list_user_project_permissions(
+ self,
+ userId: int,
+ offset: Optional[int] = None,
+ limit: Optional[int] = None,
+ ):
+ """
+ List User Project Permissions.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.projects.permissions.getMany
+ """
+
+ return self._get_entire_data(
+ method="get",
+ path=f"{self.get_users_path(userId=userId)}/projects/permissions",
+ params=self.get_page_params(offset=offset, limit=limit),
+ )
+
+ def edit_user_project_permissions(
+ self,
+ userId: int,
+ data: Iterable[UserProjectPermissionsPatchRequest],
+ ):
+ """
+ Edit User Project Permissions.
+
+ Link to documentation:
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.projects.permissions.patch
+ """
+
+ return self.requester.request(
+ method="patch",
+ path=f"{self.get_users_path(userId=userId)}/projects/permissions",
+ request_data=data,
+ )
+
def edit_user(self, userId: int, data: Iterable[UserPatchRequest]):
"""
Edit User.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.patch
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.patch
"""
return self.requester.request(
@@ -336,7 +521,7 @@ def delete_user(self, userId: int):
Delete User.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.users.delete
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.users.delete
"""
return self.requester.request(
@@ -392,7 +577,7 @@ def list_users(
}
params.update(self.get_page_params(offset=offset, limit=limit))
- return self.requester.request(
+ return self._get_entire_data(
method="get",
path=self.get_users_path(),
params=params
diff --git a/crowdin_api/api_resources/users/tests/test_users_resources.py b/crowdin_api/api_resources/users/tests/test_users_resources.py
index ce0587d4..f3bc47aa 100644
--- a/crowdin_api/api_resources/users/tests/test_users_resources.py
+++ b/crowdin_api/api_resources/users/tests/test_users_resources.py
@@ -14,7 +14,8 @@
UserStatus,
UserTwoFactorAuthStatus,
OrganizationRole,
- ProjectRole
+ ProjectRole,
+ AuthenticatedUserPatchPath,
)
from crowdin_api.api_resources.users.resource import (
UsersResource,
@@ -198,8 +199,10 @@ def test_get_group_managers_path(self, group_id, user_id, path, base_absolut_url
(
{},
{
- "team_ids": None,
- "order_by": None
+ "teamIds": None,
+ "orderBy": None,
+ "offset": 0,
+ "limit": 25,
},
),
(
@@ -215,8 +218,8 @@ def test_get_group_managers_path(self, group_id, user_id, path, base_absolut_url
),
},
{
- "team_ids": "1,2,3",
- "order_by": Sorting(
+ "teamIds": "1,2,3",
+ "orderBy": Sorting(
[
SortingRule(
ListGroupManagersOrderBy.ID,
@@ -224,6 +227,8 @@ def test_get_group_managers_path(self, group_id, user_id, path, base_absolut_url
)
]
),
+ "offset": 0,
+ "limit": 25,
}
)
)
@@ -363,7 +368,8 @@ def test_list_project_members(self, m_request, in_params, request_params, base_a
"email": "",
"firstName": None,
"lastName": None,
- "timezone": None
+ "timezone": None,
+ "adminAccess": None,
},
),
(
@@ -377,7 +383,8 @@ def test_list_project_members(self, m_request, in_params, request_params, base_a
"email": "john@example.com",
"firstName": "Jon",
"lastName": "Doe",
- "timezone": "America/New_York"
+ "timezone": "America/New_York",
+ "adminAccess": None,
},
),
),
@@ -404,7 +411,10 @@ def test_invite_user(self, m_request, in_params, request_params, base_absolut_ur
"accessToAllWorkflowSteps": None,
"managerAccess": None,
"permissions": None,
- "roles": None
+ "roles": None,
+ "usernames": None,
+ "emails": None,
+ "developerAccess": None,
},
),
(
@@ -474,7 +484,10 @@ def test_invite_user(self, m_request, in_params, request_params, base_absolut_ur
"languagesAccess": []
}
}
- ]
+ ],
+ "usernames": None,
+ "emails": None,
+ "developerAccess": None,
},
),
),
@@ -498,7 +511,8 @@ def test_add_project_member(self, m_request, in_params, request_params, base_abs
"accessToAllWorkflowSteps": None,
"managerAccess": None,
"permissions": None,
- "roles": None
+ "roles": None,
+ "developerAccess": None,
},
),
(
@@ -564,7 +578,8 @@ def test_add_project_member(self, m_request, in_params, request_params, base_abs
"languagesAccess": []
}
}
- ]
+ ],
+ "developerAccess": None,
},
),
),
@@ -687,3 +702,155 @@ def test_list_users(self, m_request, in_params, request_params, base_absolut_url
path="users",
params=request_params
)
+
+
+class TestUsersResourceNewMethods:
+ resource_class = UsersResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_authenticated_user(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {
+ "op": PatchOperation.REPLACE,
+ "path": AuthenticatedUserPatchPath.FULL_NAME,
+ "value": "John Doe",
+ }
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_authenticated_user(data) == "response"
+ m_request.assert_called_once_with(method="patch", path="user", request_data=data)
+
+ @pytest.mark.parametrize(
+ "in_params, request_data",
+ (
+ (
+ {"userIds": [1]},
+ {
+ "userIds": [1],
+ "usernames": None,
+ "emails": None,
+ "managerAccess": None,
+ "developerAccess": None,
+ "roles": None,
+ "message": None,
+ },
+ ),
+ (
+ {
+ "usernames": ["john"],
+ "emails": ["john@example.com"],
+ "developerAccess": True,
+ "message": "Welcome",
+ },
+ {
+ "userIds": None,
+ "usernames": ["john"],
+ "emails": ["john@example.com"],
+ "managerAccess": None,
+ "developerAccess": True,
+ "roles": None,
+ "message": "Welcome",
+ },
+ ),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_add_project_member(self, m_request, in_params, request_data, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.add_project_member(projectId=1, **in_params) == "response"
+ m_request.assert_called_once_with(
+ method="post", path="projects/1/members", request_data=request_data
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_replace_project_member_permissions(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ roles = [{"name": ProjectRole.TRANSLATOR, "permissions": {"allLanguages": True}}]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.replace_project_member_permissions(
+ memberId=2, projectId=1, roles=roles
+ ) == "response"
+ m_request.assert_called_once_with(
+ method="put",
+ path="projects/1/members/2",
+ request_data={"managerAccess": None, "developerAccess": None, "roles": roles},
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_delete_member_from_project(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.delete_member_from_project(memberId=2, projectId=1) == "response"
+ m_request.assert_called_once_with(method="delete", path="projects/1/members/2")
+
+
+class TestEnterpriseUsersResourceNewMethods:
+ resource_class = EnterpriseUsersResource
+
+ def get_resource(self, base_absolut_url):
+ return self.resource_class(requester=APIRequester(base_url=base_absolut_url))
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_member_info(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_member_info(memberId=2, projectId=1) == "response"
+ m_request.assert_called_once_with(method="get", path="projects/1/members/2")
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_get_user(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert resource.get_user(1) == "response"
+ m_request.assert_called_once_with(method="get", path="users/1")
+
+ @pytest.mark.parametrize(
+ "method_name, path",
+ (
+ ("list_user_project_contributions", "users/1/projects/contributions"),
+ ("list_user_project_permissions", "users/1/projects/permissions"),
+ ),
+ )
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_list_user_projects(self, m_request, method_name, path, base_absolut_url):
+ m_request.return_value = "response"
+
+ resource = self.get_resource(base_absolut_url)
+ assert getattr(resource, method_name)(userId=1, offset=10, limit=5) == "response"
+ m_request.assert_called_once_with(
+ method="get", path=path, params={"offset": 10, "limit": 5}
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_user_project_permissions(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [{"op": PatchOperation.REMOVE, "path": "/2815"}]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_user_project_permissions(userId=1, data=data) == "response"
+ m_request.assert_called_once_with(
+ method="patch", path="users/1/projects/permissions", request_data=data
+ )
+
+ @mock.patch("crowdin_api.requester.APIRequester.request")
+ def test_edit_user_admin_access(self, m_request, base_absolut_url):
+ m_request.return_value = "response"
+
+ data = [
+ {"op": PatchOperation.REPLACE, "path": UserPatchPath.ADMIN_ACCESS, "value": True},
+ {"op": PatchOperation.REPLACE, "path": UserPatchPath.AVATAR_STORAGE_ID, "value": 5},
+ ]
+ resource = self.get_resource(base_absolut_url)
+ assert resource.edit_user(userId=1, data=data) == "response"
+ m_request.assert_called_once_with(method="patch", path="users/1", request_data=data)
diff --git a/crowdin_api/api_resources/users/types.py b/crowdin_api/api_resources/users/types.py
index d64e70b4..79f8e296 100644
--- a/crowdin_api/api_resources/users/types.py
+++ b/crowdin_api/api_resources/users/types.py
@@ -1,7 +1,11 @@
from typing import Any, Optional, Iterable
from crowdin_api.api_resources.enums import PatchOperation
-from crowdin_api.api_resources.users.enums import UserPatchPath, ProjectRole
+from crowdin_api.api_resources.users.enums import (
+ UserPatchPath,
+ ProjectRole,
+ AuthenticatedUserPatchPath,
+)
from crowdin_api.typing import TypedDict
@@ -11,6 +15,12 @@ class UserPatchRequest(TypedDict):
path: UserPatchPath
+class AuthenticatedUserPatchRequest(TypedDict):
+ value: str
+ op: PatchOperation
+ path: AuthenticatedUserPatchPath
+
+
class LanguageData(TypedDict):
allContent: bool
workflowStepIds: Optional[Iterable[Any]]
@@ -35,3 +45,15 @@ class GroupManagerPatchRequest(TypedDict):
op: PatchOperation
path: str
value: Any
+
+
+class UserProjectPermissionsPatchRequest(TypedDict):
+ """
+ JSON Patch operation for project permissions.
+
+ `path` is `/{projectId}/roles` (replace), `/{projectId}/roles/-` (add) or `/{projectId}` (remove).
+ """
+
+ op: PatchOperation
+ path: str
+ value: Any
diff --git a/crowdin_api/api_resources/vendors/resource.py b/crowdin_api/api_resources/vendors/resource.py
index 7e07cf32..b5a2032b 100644
--- a/crowdin_api/api_resources/vendors/resource.py
+++ b/crowdin_api/api_resources/vendors/resource.py
@@ -14,15 +14,15 @@ class VendorsResource(BaseResource):
Use API to get the list of the Vendors you already invited to your organization.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Vendors
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Vendors
"""
def list_vendors(self, offset: Optional[int] = None, limit: Optional[int] = None):
"""
- List Teams.
+ List Vendors.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.vendors.getMany
"""
return self._get_entire_data(
diff --git a/crowdin_api/api_resources/webhooks/enums.py b/crowdin_api/api_resources/webhooks/enums.py
index e588564a..7cb1cc9a 100644
--- a/crowdin_api/api_resources/webhooks/enums.py
+++ b/crowdin_api/api_resources/webhooks/enums.py
@@ -2,19 +2,35 @@
class WebhookEvents(Enum):
+ FILE_ADDED = "file.added"
+ FILE_UPDATED = "file.updated"
+ FILE_REVERTED = "file.reverted"
+ FILE_DELETED = "file.deleted"
FILE_TRANSLATED = "file.translated"
FILE_APPROVED = "file.approved"
+ FILE_QA_FINISHED = "file.qa.finished"
PROJECT_TRANSLATED = "project.translated"
PROJECT_APPROVED = "project.approved"
+ PROJECT_QA_FINISHED = "project.qa.finished"
+ PROJECT_BUILT = "project.built"
+ PRE_TRANSLATION_COMPLETED = "preTranslation.completed"
TRANSLATION_UPDATED = "translation.updated"
STRING_ADDED = "string.added"
STRING_UPDATED = "string.updated"
STRING_DELETED = "string.deleted"
+ STRING_COMMENT_CREATED = "stringComment.created"
+ STRING_COMMENT_UPDATED = "stringComment.updated"
+ STRING_COMMENT_DELETED = "stringComment.deleted"
+ STRING_COMMENT_RESTORED = "stringComment.restored"
SUGGESTION_ADDED = "suggestion.added"
SUGGESTION_UPDATED = "suggestion.updated"
SUGGESTION_DELETED = "suggestion.deleted"
SUGGESTION_APPROVED = "suggestion.approved"
SUGGESTION_DISAPPROVED = "suggestion.disapproved"
+ TASK_ADDED = "task.added"
+ TASK_STATUS_CHANGED = "task.statusChanged"
+ TASK_UPDATED = "task.updated"
+ TASK_DELETED = "task.deleted"
class WebhookRequestType(Enum):
diff --git a/crowdin_api/api_resources/webhooks/organization/resource.py b/crowdin_api/api_resources/webhooks/organization/resource.py
index 14fd33d6..cd3f13c6 100644
--- a/crowdin_api/api_resources/webhooks/organization/resource.py
+++ b/crowdin_api/api_resources/webhooks/organization/resource.py
@@ -3,34 +3,45 @@
from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.webhooks.enums import WebhookRequestType, WebhookContentType
-from crowdin_api.api_resources.webhooks.organization.enums import OrganizationWebhookEvent, EnterpriseOrgWebhookEvent
+from crowdin_api.api_resources.webhooks.organization.enums import (
+ OrganizationWebhookEvent,
+ EnterpriseOrgWebhookEvent,
+)
from crowdin_api.api_resources.webhooks.organization.types import OrganizationWebhookPatchRequest
class OrganizationWebhooksResource(BaseResource):
- BASE_URL = "/webhooks"
-
- def get_webhooks_path(
- self,
- organization_webhook_id: int
- ):
- return f"{self.BASE_URL}/{organization_webhook_id}"
+ """
+ Resource for Organization Webhooks.
- """Webhooks allow you to collect information about events that happen in your Crowdin account. You can select the
- request type, content type, and add a custom payload, which allows you to create integrations with other systems
- on your own.
+ Webhooks allow you to collect information about events that happen in your Crowdin account.
+ You can select the request type, content type, and add a custom payload, which allows you to
+ create integrations with other systems on your own.
You can configure webhooks for the following events:
- project is created
- project is deleted
+ - group is created (Crowdin Enterprise only)
+ - group is deleted (Crowdin Enterprise only)
Use API to create, modify, and delete specific webhooks.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Organization-Webhooks
+ https://support.crowdin.com/developer/api/v2/#tag/Organization-Webhooks
+
+ Link to documentation for enterprise:
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Organization-Webhooks
"""
+ BASE_URL = "webhooks"
+
+ def get_webhooks_path(
+ self,
+ organization_webhook_id: int
+ ):
+ return f"{self.BASE_URL}/{organization_webhook_id}"
+
def list_webhooks(
self,
page: Optional[int] = None,
@@ -41,7 +52,7 @@ def list_webhooks(
List Webhooks
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.webhooks.getMany
+ https://support.crowdin.com/developer/api/v2/#operation/api.webhooks.getMany
"""
return self._get_entire_data(
method="get",
@@ -70,7 +81,7 @@ def add_webhook(
For Enterprise please use method "add_webhook_enterprise"
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.webhooks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.webhooks.post
"""
return self.requester.request(
@@ -106,7 +117,7 @@ def add_webhook_enterprise(
Events list is different
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.webhooks.post
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.webhooks.post
"""
return self.requester.request(
@@ -133,7 +144,7 @@ def get_webhook(
Get webhook
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.webhooks.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.webhooks.get
"""
return self.requester.request(
method="get",
@@ -148,7 +159,7 @@ def delete_webhook(
Delete webhook
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.webhooks.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.webhooks.delete
"""
return self.requester.request(
@@ -165,7 +176,7 @@ def edit_webhook(
Edit webhook
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.webhooks.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.webhooks.patch
"""
return self.requester.request(
diff --git a/crowdin_api/api_resources/webhooks/organization/tests/test_organization_webhooks_resources.py b/crowdin_api/api_resources/webhooks/organization/tests/test_organization_webhooks_resources.py
index 0707414b..2fef13f7 100644
--- a/crowdin_api/api_resources/webhooks/organization/tests/test_organization_webhooks_resources.py
+++ b/crowdin_api/api_resources/webhooks/organization/tests/test_organization_webhooks_resources.py
@@ -31,7 +31,7 @@ def test_list_webhooks(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="get",
params=resource.get_page_params(),
- path=resource.BASE_URL
+ path="webhooks"
)
@pytest.mark.parametrize(
@@ -102,7 +102,7 @@ def test_add_webhook(self, m_request, in_params, request_data, base_absolut_url)
assert resource.add_webhook(**in_params) == "response"
m_request.assert_called_once_with(
method="post",
- path=resource.BASE_URL,
+ path="webhooks",
request_data=request_data
)
@@ -174,7 +174,7 @@ def test_add_webhook_enterprise(self, m_request, in_params, request_data, base_a
assert resource.add_webhook(**in_params) == "response"
m_request.assert_called_once_with(
method="post",
- path=resource.BASE_URL,
+ path="webhooks",
request_data=request_data
)
@@ -186,7 +186,7 @@ def test_get_webhook(self, m_request, base_absolut_url):
assert resource.get_webhook(organization_webhook_id=1) == "response"
m_request.assert_called_once_with(
method="get",
- path=resource.get_webhooks_path(organization_webhook_id=1)
+ path="webhooks/1"
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -197,7 +197,7 @@ def test_delete_webhook(self, m_request, base_absolut_url):
assert resource.delete_webhook(organization_webhook_id=1) == "response"
m_request.assert_called_once_with(
method="delete",
- path=resource.get_webhooks_path(organization_webhook_id=1)
+ path="webhooks/1"
)
@mock.patch("crowdin_api.requester.APIRequester.request")
@@ -229,5 +229,5 @@ def test_edit_webhook(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="patch",
request_data=data,
- path=resource.get_webhooks_path(organization_webhook_id=1)
+ path="webhooks/1"
)
diff --git a/crowdin_api/api_resources/webhooks/organization/types.py b/crowdin_api/api_resources/webhooks/organization/types.py
index d4c39cff..0b16114b 100644
--- a/crowdin_api/api_resources/webhooks/organization/types.py
+++ b/crowdin_api/api_resources/webhooks/organization/types.py
@@ -1,8 +1,9 @@
-from typing import TypedDict, Any
+from typing import Any
from crowdin_api.api_resources.enums import PatchOperation
from crowdin_api.api_resources.webhooks.organization.enums import OrganizationWebhookPatchPath
+from crowdin_api.typing import TypedDict
class OrganizationWebhookPatchRequest(TypedDict):
diff --git a/crowdin_api/api_resources/webhooks/resource.py b/crowdin_api/api_resources/webhooks/resource.py
index d344d2fa..2fd754dc 100644
--- a/crowdin_api/api_resources/webhooks/resource.py
+++ b/crowdin_api/api_resources/webhooks/resource.py
@@ -35,7 +35,7 @@ class WebhooksResource(BaseResource):
Use API to create, modify, and delete specific webhooks.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Webhooks
+ https://support.crowdin.com/developer/api/v2/#tag/Webhooks
"""
def get_webhooks_path(self, projectId: int, webhookId: Optional[int] = None):
@@ -55,7 +55,7 @@ def list_webhooks(
List Webhooks.
Link to documentation:
- https://developer.crowdin.com/api/v2/#tag/Webhooks
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.webhooks.getMany
"""
projectId = projectId or self.get_project_id()
@@ -82,8 +82,10 @@ def add_webhook(
"""
Add Webhook.
+ Note: `file.*` events are available for file-based projects only.
+
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.post
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.webhooks.post
"""
projectId = projectId or self.get_project_id()
@@ -109,7 +111,7 @@ def get_webhook(self, webhookId: int, projectId: Optional[int] = None):
Get Webhook.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.get
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.webhooks.get
"""
projectId = projectId or self.get_project_id()
@@ -124,7 +126,7 @@ def delete_webhook(self, webhookId: int, projectId: Optional[int] = None):
Delete Webhook.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.delete
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.webhooks.delete
"""
projectId = projectId or self.get_project_id()
@@ -141,10 +143,10 @@ def edit_webhook(
projectId: Optional[int] = None,
):
"""
- Edit Custom Language.
+ Edit Webhook.
Link to documentation:
- https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.patch
+ https://support.crowdin.com/developer/api/v2/#operation/api.projects.webhooks.patch
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/workflows/resource.py b/crowdin_api/api_resources/workflows/resource.py
index 227737fe..ec92e44e 100644
--- a/crowdin_api/api_resources/workflows/resource.py
+++ b/crowdin_api/api_resources/workflows/resource.py
@@ -16,7 +16,7 @@ class WorkflowsResource(BaseResource):
the details of a specific template.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#tag/Workflows
+ https://support.crowdin.com/developer/enterprise/api/v2/#tag/Workflows
"""
def get_workflow_steps_path(self, projectId: int, stepId: Optional[int] = None):
@@ -31,18 +31,24 @@ def get_workflow_templates_path(self, templateId: Optional[int] = None):
return "workflow-templates"
- def list_workflow_steps(self, projectId: Optional[int] = None):
+ def list_workflow_steps(
+ self,
+ projectId: Optional[int] = None,
+ limit: Optional[int] = None,
+ offset: Optional[int] = None,
+ ):
"""
List Workflow Steps.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.workflow-steps.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.workflow-steps.getMany
"""
projectId = projectId or self.get_project_id()
return self._get_entire_data(
method="get",
path=self.get_workflow_steps_path(projectId=projectId),
+ params=self.get_page_params(offset=offset, limit=limit),
)
def get_workflow_step(self, stepId: int, projectId: Optional[int] = None):
@@ -50,7 +56,7 @@ def get_workflow_step(self, stepId: int, projectId: Optional[int] = None):
Get Workflow Step.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.workflow-steps.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.workflow-steps.get
"""
projectId = projectId or self.get_project_id()
@@ -69,7 +75,7 @@ def list_workflow_templates(
List Workflow Templates.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.workflow-templates.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.workflow-templates.getMany
"""
params = {"groupId": groupId}
params.update(self.get_page_params(offset=offset, limit=limit))
@@ -85,7 +91,7 @@ def get_workflow_template(self, templateId: int):
Get Workflow Template.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.workflow-templates.get
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.workflow-templates.get
"""
return self.requester.request(
method="get",
@@ -109,7 +115,7 @@ def list_workflow_step_strings(
List Strings on the Workflow Step.
Link to documentation:
- https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.workflow-steps.strings.getMany
+ https://support.crowdin.com/developer/enterprise/api/v2/#operation/api.projects.workflow-steps.strings.getMany
"""
projectId = projectId or self.get_project_id()
diff --git a/crowdin_api/api_resources/workflows/tests/test_workflows_resources.py b/crowdin_api/api_resources/workflows/tests/test_workflows_resources.py
index 5ff8c33d..7ff94402 100644
--- a/crowdin_api/api_resources/workflows/tests/test_workflows_resources.py
+++ b/crowdin_api/api_resources/workflows/tests/test_workflows_resources.py
@@ -52,7 +52,7 @@ def test_list_workflow_steps(self, m_request, base_absolut_url):
m_request.assert_called_once_with(
method="get",
path=resource.get_workflow_steps_path(projectId=1),
- params=None
+ params={"offset": 0, "limit": 25},
)
@mock.patch("crowdin_api.requester.APIRequester.request")
diff --git a/crowdin_api/client.py b/crowdin_api/client.py
index 1ad27856..f8cf3934 100644
--- a/crowdin_api/client.py
+++ b/crowdin_api/client.py
@@ -104,6 +104,19 @@ def graphql(self, query: str, variables: Optional[Dict] = None) -> Dict:
request_data=data
)
+ @property
+ def advisors(self) -> api_resources.AdvisorsResource:
+ if self.PROJECT_ID:
+ return api_resources.AdvisorsResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.AdvisorsResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
@property
def ai(self) -> Union[api_resources.AIResource, api_resources.EnterpriseAIResource]:
if self._is_enterprise_platform:
@@ -125,6 +138,19 @@ def applications(self) -> api_resources.ApplicationResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)
+ @property
+ def branches(self) -> api_resources.BranchesResource:
+ if self.PROJECT_ID:
+ return api_resources.BranchesResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.BranchesResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
@property
def bundles(self) -> api_resources.BundlesResource:
if self.PROJECT_ID:
@@ -138,6 +164,54 @@ def bundles(self) -> api_resources.BundlesResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)
+ @property
+ def clients(self) -> api_resources.ClientsResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.ClientsResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.ClientsResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
+ @property
+ def custom_placeholders(self) -> api_resources.CustomPlaceholdersResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.CustomPlaceholdersResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.CustomPlaceholdersResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
+ @property
+ def custom_spellcheckers(self) -> api_resources.CustomSpellcheckersResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.CustomSpellcheckersResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.CustomSpellcheckersResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
@property
def dictionaries(self) -> api_resources.DictionariesResource:
if self.PROJECT_ID:
@@ -164,6 +238,22 @@ def distributions(self) -> api_resources.DistributionsResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)
+ @property
+ def external_qa_checks(self) -> api_resources.ExternalQaChecksResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.ExternalQaChecksResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.ExternalQaChecksResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
@property
def fields(self) -> api_resources.FieldsResource:
if not self._is_enterprise_platform:
@@ -235,6 +325,64 @@ def languages(self) -> api_resources.LanguagesResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)
+ @property
+ def notifications(self) -> api_resources.NotificationResource:
+ if self.PROJECT_ID:
+ return api_resources.NotificationResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.NotificationResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
+ @property
+ def organization(self) -> api_resources.OrganizationResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.OrganizationResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.OrganizationResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
+ @property
+ def organization_webhooks(self) -> api_resources.OrganizationWebhooksResource:
+ if self.PROJECT_ID:
+ return api_resources.OrganizationWebhooksResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.OrganizationWebhooksResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
+ @property
+ def project_placeholders(self) -> api_resources.ProjectPlaceholdersResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.ProjectPlaceholdersResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.ProjectPlaceholdersResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
@property
def projects(self) -> api_resources.ProjectsResource:
if self.PROJECT_ID:
@@ -309,17 +457,22 @@ def source_files(self) -> api_resources.SourceFilesResource:
)
@property
- def source_strings(self) -> api_resources.SourceStringsResource:
+ def source_strings(
+ self,
+ ) -> Union[api_resources.SourceStringsResource, api_resources.EnterpriseSourceStringsResource]:
+ if self._is_enterprise_platform:
+ source_strings_class = api_resources.EnterpriseSourceStringsResource
+ else:
+ source_strings_class = api_resources.SourceStringsResource
+
if self.PROJECT_ID:
- return api_resources.SourceStringsResource(
+ return source_strings_class(
requester=self.get_api_requestor(),
project_id=self.PROJECT_ID,
page_size=self.PAGE_SIZE,
)
- return api_resources.SourceStringsResource(
- requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
- )
+ return source_strings_class(requester=self.get_api_requestor(), page_size=self.PAGE_SIZE)
@property
def storages(self) -> api_resources.StoragesResource:
@@ -347,6 +500,22 @@ def string_comments(self) -> api_resources.StringCommentsResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)
+ @property
+ def string_corrections(self) -> api_resources.StringCorrectionsResource:
+ if not self._is_enterprise_platform:
+ raise CrowdinException(detail="Not implemented for the base API")
+
+ if self.PROJECT_ID:
+ return api_resources.StringCorrectionsResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.StringCorrectionsResource(
+ requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
+ )
+
@property
def string_translations(self) -> api_resources.StringTranslationsResource:
if self.PROJECT_ID:
@@ -373,6 +542,19 @@ def style_guides(self) -> api_resources.StyleGuidesResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)
+ @property
+ def system_placeholders(self) -> api_resources.SystemPlaceholdersResource:
+ if self.PROJECT_ID:
+ return api_resources.SystemPlaceholdersResource(
+ requester=self.get_api_requestor(),
+ project_id=self.PROJECT_ID,
+ page_size=self.PAGE_SIZE,
+ )
+
+ return api_resources.SystemPlaceholdersResource(
+ 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:
@@ -448,17 +630,25 @@ def translations(self) -> api_resources.TranslationsResource:
)
@property
- def machine_translations(self) -> api_resources.MachineTranslationEnginesResource:
+ def machine_translations(
+ self,
+ ) -> Union[
+ api_resources.MachineTranslationEnginesResource,
+ api_resources.EnterpriseMachineTranslationEnginesResource,
+ ]:
+ if self._is_enterprise_platform:
+ mt_class = api_resources.EnterpriseMachineTranslationEnginesResource
+ else:
+ mt_class = api_resources.MachineTranslationEnginesResource
+
if self.PROJECT_ID:
- return api_resources.MachineTranslationEnginesResource(
+ return mt_class(
requester=self.get_api_requestor(),
project_id=self.PROJECT_ID,
page_size=self.PAGE_SIZE,
)
- return api_resources.MachineTranslationEnginesResource(
- requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
- )
+ return mt_class(requester=self.get_api_requestor(), page_size=self.PAGE_SIZE)
@property
def users(self) -> Union[api_resources.UsersResource, api_resources.EnterpriseUsersResource]:
diff --git a/crowdin_api/tests/test_client.py b/crowdin_api/tests/test_client.py
index 9ee4cc87..1b3b2f8e 100644
--- a/crowdin_api/tests/test_client.py
+++ b/crowdin_api/tests/test_client.py
@@ -2,6 +2,7 @@
import pytest
from crowdin_api import CrowdinClient
+from crowdin_api.exceptions import CrowdinException
class MockCrowdinClientEnterprise(CrowdinClient):
@@ -160,7 +161,9 @@ def test_api_requestor_custom_values(self, m_APIRequester):
@pytest.mark.parametrize(
"property_name, class_name",
(
+ ("advisors", "AdvisorsResource"),
("ai", "AIResource"),
+ ("branches", "BranchesResource"),
("bundles", "BundlesResource"),
("dictionaries", "DictionariesResource"),
("distributions", "DistributionsResource"),
@@ -168,6 +171,8 @@ def test_api_requestor_custom_values(self, m_APIRequester):
("labels", "LabelsResource"),
("languages", "LanguagesResource"),
("machine_translations", "MachineTranslationEnginesResource"),
+ ("notifications", "NotificationResource"),
+ ("organization_webhooks", "OrganizationWebhooksResource"),
("projects", "ProjectsResource"),
("reports", "ReportsResource"),
("screenshots", "ScreenshotsResource"),
@@ -178,6 +183,7 @@ def test_api_requestor_custom_values(self, m_APIRequester):
("string_comments", "StringCommentsResource"),
("string_translations", "StringTranslationsResource"),
("style_guides", "StyleGuidesResource"),
+ ("system_placeholders", "SystemPlaceholdersResource"),
("tasks", "TasksResource"),
("translation_memory", "TranslationMemoryResource"),
("translation_status", "TranslationStatusResource"),
@@ -211,6 +217,22 @@ def test_storages(self, _m_api_requestor, property_name, class_name):
requester="api_requestor", project_id=1, page_size=25
)
+ @pytest.mark.parametrize(
+ "property_name",
+ (
+ "clients",
+ "custom_placeholders",
+ "custom_spellcheckers",
+ "external_qa_checks",
+ "organization",
+ "project_placeholders",
+ "string_corrections",
+ ),
+ )
+ def test_enterprise_only_resources(self, property_name):
+ with pytest.raises(CrowdinException):
+ getattr(CrowdinClient(), property_name)
+
@mock.patch("crowdin_api.client.CrowdinClient.get_api_requestor")
def test_graphql(self, mock_get_requestor):
"""Test GraphQL functionality with basic request validation."""
@@ -235,26 +257,38 @@ class TestCrowdinClientEnterprise:
@pytest.mark.parametrize(
"property_name, class_name",
(
+ ("advisors", "AdvisorsResource"),
("ai", "EnterpriseAIResource"),
+ ("branches", "BranchesResource"),
("bundles", "BundlesResource"),
+ ("clients", "ClientsResource"),
+ ("custom_placeholders", "CustomPlaceholdersResource"),
+ ("custom_spellcheckers", "CustomSpellcheckersResource"),
("dictionaries", "DictionariesResource"),
("distributions", "DistributionsResource"),
+ ("external_qa_checks", "ExternalQaChecksResource"),
("fields", "FieldsResource"),
("glossaries", "GlossariesResource"),
("groups", "GroupsResource"),
("labels", "LabelsResource"),
("languages", "LanguagesResource"),
- ("machine_translations", "MachineTranslationEnginesResource"),
+ ("machine_translations", "EnterpriseMachineTranslationEnginesResource"),
+ ("notifications", "NotificationResource"),
+ ("organization", "OrganizationResource"),
+ ("organization_webhooks", "OrganizationWebhooksResource"),
+ ("project_placeholders", "ProjectPlaceholdersResource"),
("projects", "ProjectsResource"),
("reports", "EnterpriseReportsResource"),
("screenshots", "ScreenshotsResource"),
("security_logs", "SecurityLogsResource"),
("source_files", "SourceFilesResource"),
- ("source_strings", "SourceStringsResource"),
+ ("source_strings", "EnterpriseSourceStringsResource"),
("storages", "StoragesResource"),
("string_comments", "StringCommentsResource"),
+ ("string_corrections", "StringCorrectionsResource"),
("string_translations", "StringTranslationsResource"),
("style_guides", "StyleGuidesResource"),
+ ("system_placeholders", "SystemPlaceholdersResource"),
("tasks", "EnterpriseTasksResource"),
("teams", "TeamsResource"),
("translation_memory", "TranslationMemoryResource"),
diff --git a/crowdin_api/utils.py b/crowdin_api/utils.py
index 7f78bf3c..7dec18d4 100644
--- a/crowdin_api/utils.py
+++ b/crowdin_api/utils.py
@@ -1,5 +1,5 @@
from enum import Enum
-from typing import Optional, Iterable, Callable
+from typing import Any, Callable, Iterable, Optional
def convert_to_query_string(
@@ -23,3 +23,20 @@ def convert_enum_collection_to_string_if_exists(value: Optional[Iterable[Enum]])
if value is None:
return None
return ','.join([item.value for item in value if isinstance(item, Enum)])
+
+
+def convert_to_query_list(value: Any) -> Any:
+ """
+ Serialize a query value that the API expects as a comma-separated list.
+
+ Iterables (except strings) are joined with commas, using `.value` for enum members; an empty
+ iterable becomes `None` so the filter is omitted. Single values are returned as they are.
+ """
+ if value is None or isinstance(value, (str, bytes, Enum)) or not isinstance(value, Iterable):
+ return value
+
+ items = list(value)
+ if not items:
+ return None
+
+ return ",".join(str(item.value if isinstance(item, Enum) else item) for item in items)