diff --git a/atlassian/confluence/cloud/cloud.py b/atlassian/confluence/cloud/cloud.py index 860f82806..3648b6c35 100644 --- a/atlassian/confluence/cloud/cloud.py +++ b/atlassian/confluence/cloud/cloud.py @@ -1154,12 +1154,16 @@ def add_page_label(self, page_id: str, label: str) -> Dict[str, Any]: """ Adds a label to a page. + The Confluence Cloud v2 API only exposes labels for reading, so this call is + made against the v1 endpoint. + Args: page_id: The ID of the page label: The label to add Returns: - The created label object + The response of the v1 label endpoint, holding the created labels in + its ``results`` list Raises: HTTPError: If the API call fails @@ -1168,9 +1172,9 @@ def add_page_label(self, page_id: str, label: str) -> Dict[str, Any]: if not label: raise ValueError("Label cannot be empty") - endpoint = self.get_endpoint("page_labels", id=page_id) + endpoint = self.get_endpoint("page_labels_write", id=page_id) - data = {"name": label} + data = [{"prefix": "global", "name": label}] try: return self.post(endpoint, data=data) @@ -1182,12 +1186,16 @@ def add_page_labels(self, page_id: str, labels: List[str]) -> List[Dict[str, Any """ Adds multiple labels to a page. + The Confluence Cloud v2 API only exposes labels for reading, so this call is + made against the v1 endpoint. + Args: page_id: The ID of the page labels: List of labels to add Returns: - List of created label objects + The response of the v1 label endpoint, holding the created labels in + its ``results`` list Raises: HTTPError: If the API call fails @@ -1196,9 +1204,9 @@ def add_page_labels(self, page_id: str, labels: List[str]) -> List[Dict[str, Any if not labels: raise ValueError("Labels list cannot be empty") - endpoint = self.get_endpoint("page_labels", id=page_id) + endpoint = self.get_endpoint("page_labels_write", id=page_id) - data = [{"name": label} for label in labels] + data = [{"prefix": "global", "name": label} for label in labels] try: return self.post(endpoint, data=data) @@ -1210,6 +1218,9 @@ def delete_page_label(self, page_id: str, label: str) -> bool: """ Deletes a label from a page. + The Confluence Cloud v2 API only exposes labels for reading, so this call is + made against the v1 endpoint. + Args: page_id: The ID of the page label: The label to delete @@ -1223,7 +1234,7 @@ def delete_page_label(self, page_id: str, label: str) -> bool: if not label: raise ValueError("Label cannot be empty") - endpoint = self.get_endpoint("page_labels", id=page_id) + endpoint = self.get_endpoint("page_labels_write", id=page_id) params = {"name": label} try: diff --git a/atlassian/confluence_base.py b/atlassian/confluence_base.py index 59cca8b0f..98694ae7a 100644 --- a/atlassian/confluence_base.py +++ b/atlassian/confluence_base.py @@ -44,6 +44,9 @@ class ConfluenceEndpoints: "page_properties": "api/v2/pages/{id}/properties", "page_property_by_key": "api/v2/pages/{id}/properties/{key}", "page_labels": "api/v2/pages/{id}/labels", + # The v2 label endpoints are read-only (only GET is defined in the Confluence + # Cloud v2 OpenAPI spec), so adding and deleting labels stays on the v1 API. + "page_labels_write": "rest/api/content/{id}/label", "space_labels": "api/v2/spaces/{id}/labels", # Comment endpoints for V2 API "page_footer_comments": "api/v2/pages/{id}/footer-comments", diff --git a/docs/confluence_v2_migration_guide.md b/docs/confluence_v2_migration_guide.md index d1e57dfc3..b67847b60 100644 --- a/docs/confluence_v2_migration_guide.md +++ b/docs/confluence_v2_migration_guide.md @@ -231,6 +231,9 @@ response = confluence.delete_custom_content(content_id) ### Labels +The v2 API only exposes labels for reading, so adding and deleting them is served +by the v1 endpoint `rest/api/content/{id}/label`. + ```python # Get page labels labels = confluence.get_page_labels(page_id) diff --git a/tests/test_confluence_v2.py b/tests/test_confluence_v2.py index 9036cb849..4c417949a 100644 --- a/tests/test_confluence_v2.py +++ b/tests/test_confluence_v2.py @@ -838,9 +838,9 @@ def test_add_page_label(self, mock_post): # Call the method response = self.confluence_v2.add_page_label("PAGE123", "test-label") - # Assertions - expected_data = {"name": "test-label"} - mock_post.assert_called_once_with("api/v2/pages/PAGE123/labels", data=expected_data) + # Assertions: v2 has no write endpoint for labels, so v1 is used + expected_data = [{"prefix": "global", "name": "test-label"}] + mock_post.assert_called_once_with("rest/api/content/PAGE123/label", data=expected_data) self.assertEqual(response, mock_response) def test_add_page_label_empty(self): @@ -857,9 +857,9 @@ def test_add_page_labels(self, mock_post): # Call the method response = self.confluence_v2.add_page_labels("PAGE123", ["label1", "label2"]) - # Assertions - expected_data = [{"name": "label1"}, {"name": "label2"}] - mock_post.assert_called_once_with("api/v2/pages/PAGE123/labels", data=expected_data) + # Assertions: v2 has no write endpoint for labels, so v1 is used + expected_data = [{"prefix": "global", "name": "label1"}, {"prefix": "global", "name": "label2"}] + mock_post.assert_called_once_with("rest/api/content/PAGE123/label", data=expected_data) self.assertEqual(response, mock_response) def test_add_page_labels_empty(self): @@ -876,7 +876,7 @@ def test_delete_page_label(self, mock_delete): result = self.confluence_v2.delete_page_label("PAGE123", "test-label") # Assertions - mock_delete.assert_called_once_with("api/v2/pages/PAGE123/labels", params={"name": "test-label"}) + mock_delete.assert_called_once_with("rest/api/content/PAGE123/label", params={"name": "test-label"}) self.assertTrue(result) def test_delete_page_label_empty(self): diff --git a/tests/test_confluence_v2_with_mocks.py b/tests/test_confluence_v2_with_mocks.py index adc0aab91..c0614824a 100644 --- a/tests/test_confluence_v2_with_mocks.py +++ b/tests/test_confluence_v2_with_mocks.py @@ -273,7 +273,8 @@ def test_add_page_label(self): """Test adding a label to a page.""" page_id = "123456" label = "test-label" - endpoint = f"api/v2/pages/{page_id}/labels" + # v2 has no write endpoint for labels, so the v1 endpoint is used + endpoint = f"rest/api/content/{page_id}/label" # Mock the response expected_data = self.mock_response_for_endpoint(endpoint, mock_data=LABEL_MOCK)