from enum import Enum import logging from typing import Optional from requests.exceptions import HTTPError from atlassian import Confluence as BaseConfluence from pydantic import BaseModel from atlassian.errors import ApiError logger = logging.getLogger(__name__) class PresetContentStates(str, Enum): """Preset Confluence content states enum.""" ROUGH_DRAFT = "Rough draft" class LabelCreate(BaseModel): prefix: Optional[str] = None name: str class PageInitialStatus(str, Enum): CURRENT = "current" DRAFT = "draft" class PageRepresentation(str, Enum): STORAGE = "storage" ATLAS_DOC_FORMAT = "atlas_doc_format" WIKI = "wiki" class Confluence(BaseConfluence): def create_new_page( self, space_id: str, title: str, body: str, parent_id: Optional[str] = None, status: PageInitialStatus = PageInitialStatus.CURRENT, representation: PageRepresentation = PageRepresentation.ATLAS_DOC_FORMAT, ) -> dict: """Create a new page in Confluence. :param space_id: The ID of the space where the page will be created :param title: The title of the page :param body: The body content of the page in storage format :param parent_id: Optional ID of the parent page :param status: The status of the page (default: "current") :param representation: The representation of the page body (default: "storage") :return: The created page data """ url = "/api/v2/pages" data = { "spaceId": space_id, "status": status, "title": title, "body": self._create_body(body, representation), } if parent_id: data["parentId"] = parent_id try: return self.post(url, json=data) except HTTPError as e: raise e def delete_page(self, page_id: int, purge: bool = False, draft: bool = False): url = f"/api/v2/pages/{page_id}" params = { "purge": purge, "draft": draft, } try: return self.delete(url, params=params) except HTTPError as e: raise e def get_space_by_id(self, space_id: str): url = f"/api/v2/spaces/{space_id}" return self.get(url) def add_content_labels(self, content_id: str, labels: list[dict]): """Add labels to content :param content_id: The ID of the content to get states for. :param labels: The labels to add. :return: None """ path = f"rest/api/content/{content_id}/label" try: return self.post(path, data=labels) except HTTPError as e: raise e def get_available_content_states(self, content_id: str) -> dict: """Gets available content states for content. Docs: https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-states/#api-wiki-rest-api-content-id-state-available-get :param content_id: The ID of the content to get states for :return: """ url = f"rest/api/content/{content_id}/state/available" try: return self.get(url) except HTTPError as e: if e.response.status_code == 404: raise ApiError( "There is no content with the given id, " "or the calling user does not have permission to view the content", reason=e, ) raise def set_content_state( self, content_id: str, content_state: dict, page_status: PageInitialStatus = PageInitialStatus.CURRENT, ): """Set the content state of a content and publishes a new version of the content. Docs: https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-states/#api-wiki-rest-api-content-id-state-put :param content_id: The ID of the content to set state for :param content_state: The page status to set. :param page_status: The new content state to set. :return: """ url = f"rest/api/content/{content_id}/state?status={page_status.value}" try: return self.put(url, data=content_state) except HTTPError as e: raise e def set_content_state_with_preset( self, content_id: str, content_state: PresetContentStates, page_status: PageInitialStatus = PageInitialStatus.CURRENT, ) -> dict: """Set a predefined content state. :param content_id: The ID of the content to set state for :param content_state: The predefined state from ContentStates enum :param page_status: The new content state to set. :return: The response from setting the content state """ states = self.get_available_content_states(content_id) matching_state = next( ( s for s in states["spaceContentStates"] if s["name"] == content_state.value ), None, ) if not matching_state: raise ValueError(f"No state found with name: {content_state.value}") return self.set_content_state(content_id, matching_state, page_status) def raise_for_status(self, response): """ Checks the response for an error status and raises an exception with the error message provided by the server :param response: :return: """ if ( response.status_code == 401 and response.headers.get("Content-Type") != "application/json;charset=UTF-8" ): raise HTTPError("Unauthorized (401)", response=response) if 400 <= response.status_code < 600: try: j = response.json() if "errors" in j: error_msg = j["errors"][0]["title"] else: error_msg = j["message"] except Exception as e: logger.error(e) response.raise_for_status() else: raise HTTPError(error_msg, response=response)