From 070ec9acc0dfc08638ed58431766b79dc08cd635 Mon Sep 17 00:00:00 2001 From: LuisFigueroaG Date: Mon, 26 Oct 2026 22:06:20 -0300 Subject: [PATCH] [Confluence] ConfluenceCloud: add remove_content docs/confluence.rst documents remove_content(content_id), and the Server client has it, but the Cloud client does not, so Confluence(url="https://.atlassian.net") and ConfluenceCloud raise "'Cloud' object has no attribute 'remove_content'". Add remove_content to Cloud with the same behaviour as the Server client: it sends DELETE to /wiki/rest/api/content/{id}, the endpoint delete_content and remove_page already use, returns the response and maps 403/404/409 to the same errors. --- atlassian/confluence/cloud/__init__.py | 32 +++++++++++++++++++++++ docs/confluence.rst | 2 ++ tests/confluence/test_confluence_cloud.py | 24 +++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/atlassian/confluence/cloud/__init__.py b/atlassian/confluence/cloud/__init__.py index d5d0c785c..5a72a9fc3 100644 --- a/atlassian/confluence/cloud/__init__.py +++ b/atlassian/confluence/cloud/__init__.py @@ -509,6 +509,38 @@ def remove_page_as_draft(self, page_id): """ return self.remove_page(page_id=page_id, status="draft") + def remove_content(self, content_id): + """ + Remove any content (page, blog post, comment, attachment, ...). + + :param content_id: content ID + :return: the response of the DELETE request + """ + try: + response = self.delete(f"rest/api/content/{content_id}") + except HTTPError as e: + if e.response.status_code == 403: + raise ApiPermissionError( + "The calling user does not have permission to trash or purge the content", + reason=e, + ) + if e.response.status_code == 404: + # Raise ApiError as the documented reason is ambiguous + raise ApiError( + "There is no content with the given id, or the calling " + "user does not have permission to trash or purge the content", + reason=e, + ) + if e.response.status_code == 409: + raise ApiConflictError( + "There is a stale data object conflict when trying to delete a draft", + reason=e, + ) + + raise + + return response + def get_page_child_count(self, page_id, type="page"): """Return the number of direct children of ``type`` without listing them.""" page = self.get_page_by_id(page_id, expand=f"children.{type}") diff --git a/docs/confluence.rst b/docs/confluence.rst index c1f3493f0..0dc44fb6d 100644 --- a/docs/confluence.rst +++ b/docs/confluence.rst @@ -282,6 +282,8 @@ Page actions confluence.remove_page(page_id, status=None, recursive=False) # Remove any content + # ``ConfluenceCloud`` (and ``Confluence`` with a Cloud URL) also provide + # this, through ``/wiki/rest/api/content``. confluence.remove_content(content_id): # Remove page from trash diff --git a/tests/confluence/test_confluence_cloud.py b/tests/confluence/test_confluence_cloud.py index c59aeab4c..4fed088f0 100644 --- a/tests/confluence/test_confluence_cloud.py +++ b/tests/confluence/test_confluence_cloud.py @@ -513,6 +513,30 @@ def test_remove_page_from_trash_through_confluence_with_cloud_url(self, mock_del ("rest/api/content/456", {"status": "draft"}), ] + @patch.object(ConfluenceCloud, "delete") + def test_remove_content(self, mock_delete, confluence_cloud): + mock_delete.return_value = None + + assert confluence_cloud.remove_content("123") is None + mock_delete.assert_called_once_with("rest/api/content/123") + + @patch.object(ConfluenceCloud, "delete") + def test_remove_content_404_raises_api_error(self, mock_delete, confluence_cloud): + response = Response() + response.status_code = 404 + mock_delete.side_effect = HTTPError(response=response) + + with pytest.raises(ApiError, match="There is no content with the given id"): + confluence_cloud.remove_content("123") + + @patch.object(ConfluenceCloud, "delete") + def test_remove_content_through_confluence_with_cloud_url(self, mock_delete): + mock_delete.return_value = None + confluence = Confluence(url="https://test.atlassian.net", username="user", password="token") + + assert confluence.remove_content("123") is None + mock_delete.assert_called_once_with("rest/api/content/123") + @patch.object(ConfluenceCloud, "delete") def test_delete_content(self, mock_delete, confluence_cloud): """Test delete_content method."""