From 4463d448878261112ee4cb713bf6ab6afe733d48 Mon Sep 17 00:00:00 2001 From: Max Inno Date: Tue, 1 Sep 2026 14:17:31 +0300 Subject: [PATCH] feat: add support for asynchronous Branch, Directory and File deletion Add optional Prefer: respond-async header support to delete_branch, delete_directory and delete_file, plus new check_*_deletion_status methods for polling the resulting delete job (Closes #270). --- .../api_resources/source_files/resource.py | 87 ++++++++++++++- .../tests/test_source_files_resources.py | 102 +++++++++++++++++- crowdin_api/tests/test_requester.py | 2 +- 3 files changed, 185 insertions(+), 6 deletions(-) diff --git a/crowdin_api/api_resources/source_files/resource.py b/crowdin_api/api_resources/source_files/resource.py index 4b26c47..e9a149b 100644 --- a/crowdin_api/api_resources/source_files/resource.py +++ b/crowdin_api/api_resources/source_files/resource.py @@ -116,7 +116,12 @@ def get_branch(self, branchId: int, projectId: Optional[int] = None): path=self.get_branch_path(projectId=projectId, branchId=branchId), ) - def delete_branch(self, branchId: int, projectId: Optional[int] = None): + def delete_branch( + self, + branchId: int, + projectId: Optional[int] = None, + prefer: Optional[str] = None, + ): """ Delete Branch. @@ -125,12 +130,34 @@ def delete_branch(self, branchId: int, projectId: Optional[int] = None): """ projectId = projectId or self.get_project_id() + headers = {"Prefer": prefer} if prefer is not None else None return self.requester.request( method="delete", + headers=headers, path=f"projects/{projectId}/branches/{branchId}", ) + def check_branch_deletion_status( + self, + branchId: int, + jobIdentifier: str, + projectId: Optional[int] = None, + ): + """ + Check Branch Deletion Status. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.branches.jobs.get + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="get", + path=f"projects/{projectId}/branches/{branchId}/jobs/{jobIdentifier}", + ) + def edit_branch( self, branchId: int, @@ -241,7 +268,12 @@ def get_directory(self, directoryId: int, projectId: Optional[int] = None): path=self.get_directory_path(projectId=projectId, directoryId=directoryId), ) - def delete_directory(self, directoryId: int, projectId: Optional[int] = None): + def delete_directory( + self, + directoryId: int, + projectId: Optional[int] = None, + prefer: Optional[str] = None, + ): """ Delete Directory. @@ -250,12 +282,34 @@ def delete_directory(self, directoryId: int, projectId: Optional[int] = None): """ projectId = projectId or self.get_project_id() + headers = {"Prefer": prefer} if prefer is not None else None return self.requester.request( method="delete", + headers=headers, path=self.get_directory_path(projectId=projectId, directoryId=directoryId), ) + def check_directory_deletion_status( + self, + directoryId: int, + jobIdentifier: str, + projectId: Optional[int] = None, + ): + """ + Check Directory Deletion Status. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.directories.jobs.get + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="get", + path=f"projects/{projectId}/directories/{directoryId}/jobs/{jobIdentifier}", + ) + def edit_directory( self, directoryId: int, @@ -451,7 +505,12 @@ def update_file( }, ) - def delete_file(self, fileId: int, projectId: Optional[int] = None): + def delete_file( + self, + fileId: int, + projectId: Optional[int] = None, + prefer: Optional[str] = None, + ): """ Delete File. @@ -460,12 +519,34 @@ def delete_file(self, fileId: int, projectId: Optional[int] = None): """ projectId = projectId or self.get_project_id() + headers = {"Prefer": prefer} if prefer is not None else None return self.requester.request( method="delete", + headers=headers, path=self.get_file_path(projectId=projectId, fileId=fileId), ) + def check_file_deletion_status( + self, + fileId: int, + jobIdentifier: str, + projectId: Optional[int] = None, + ): + """ + Check File Deletion Status. + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.projects.files.jobs.get + """ + + projectId = projectId or self.get_project_id() + + return self.requester.request( + method="get", + path=f"projects/{projectId}/files/{fileId}/jobs/{jobIdentifier}", + ) + def edit_file( self, fileId: int, 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 d5d7b38..8546953 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 @@ -141,7 +141,40 @@ def test_delete_branch(self, m_request, base_absolut_url): resource = self.get_resource(base_absolut_url) assert resource.delete_branch(projectId=1, branchId=2) == "response" m_request.assert_called_once_with( - method="delete", path=resource.get_branch_path(projectId=1, branchId=2) + method="delete", + headers=None, + path=resource.get_branch_path(projectId=1, branchId=2), + ) + + @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(projectId=1, branchId=2, prefer="respond-async") + == "response" + ) + m_request.assert_called_once_with( + method="delete", + headers={"Prefer": "respond-async"}, + path=resource.get_branch_path(projectId=1, branchId=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( + projectId=1, branchId=2, jobIdentifier="job" + ) + == "response" + ) + m_request.assert_called_once_with( + method="get", + path="projects/1/branches/2/jobs/job", ) @mock.patch("crowdin_api.requester.APIRequester.request") @@ -298,9 +331,41 @@ def test_delete_directory(self, m_request, base_absolut_url): assert resource.delete_directory(projectId=1, directoryId=2) == "response" m_request.assert_called_once_with( method="delete", + headers=None, + path=resource.get_directory_path(projectId=1, directoryId=2), + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_delete_directory_async(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.delete_directory(projectId=1, directoryId=2, prefer="respond-async") + == "response" + ) + m_request.assert_called_once_with( + method="delete", + headers={"Prefer": "respond-async"}, path=resource.get_directory_path(projectId=1, directoryId=2), ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_check_directory_deletion_status(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.check_directory_deletion_status( + projectId=1, directoryId=2, jobIdentifier="job" + ) + == "response" + ) + m_request.assert_called_once_with( + method="get", + path="projects/1/directories/2/jobs/job", + ) + @mock.patch("crowdin_api.requester.APIRequester.request") def test_edit_directory(self, m_request, base_absolut_url): m_request.return_value = "response" @@ -551,7 +616,40 @@ def test_delete_file(self, m_request, base_absolut_url): resource = self.get_resource(base_absolut_url) assert resource.delete_file(projectId=1, fileId=2) == "response" m_request.assert_called_once_with( - method="delete", path=resource.get_file_path(projectId=1, fileId=2) + method="delete", + headers=None, + path=resource.get_file_path(projectId=1, fileId=2), + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_delete_file_async(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.delete_file(projectId=1, fileId=2, prefer="respond-async") + == "response" + ) + m_request.assert_called_once_with( + method="delete", + headers={"Prefer": "respond-async"}, + path=resource.get_file_path(projectId=1, fileId=2), + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_check_file_deletion_status(self, m_request, base_absolut_url): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.check_file_deletion_status( + projectId=1, fileId=2, jobIdentifier="job" + ) + == "response" + ) + m_request.assert_called_once_with( + method="get", + path="projects/1/files/2/jobs/job", ) @mock.patch("crowdin_api.requester.APIRequester.request") diff --git a/crowdin_api/tests/test_requester.py b/crowdin_api/tests/test_requester.py index 15da549..a8b4544 100644 --- a/crowdin_api/tests/test_requester.py +++ b/crowdin_api/tests/test_requester.py @@ -195,7 +195,7 @@ def test__request_wrong_response(self, requests_mock, base_absolut_url): with pytest.raises(ParsingError): requester._request(method="get", path=path) - @pytest.mark.parametrize("status_code", (200, 299)) + @pytest.mark.parametrize("status_code", (200, 202, 299)) def test__request_with_success_status(self, status_code, requests_mock, base_absolut_url): path = "test" requester = APIRequester(base_url=base_absolut_url)