Coverage for src/mcp_atlassian/confluence/pages.py: 93%

1"""Module for Confluence page operations."""

2

3 import logging

4

5 import requests

6

7 from ..document_types import Document

8 from .client import ConfluenceClient

9

10 logger = logging.getLogger("mcp-atlassian")

11

12

13 class PagesMixin(ConfluenceClient):

14 """Mixin for Confluence page operations."""

15

16 def get_page_content(

17 self, page_id: str, *, convert_to_markdown: bool = True

18 ) -> Document:

19 """

20 Get content of a specific page.

21

22 Args:

23 page_id: The ID of the page to retrieve

24 convert_to_markdown: When True, returns content in markdown format,

25 otherwise returns raw HTML (keyword-only)

26

27 Returns:

28 Document containing the page content and metadata

29 """

30 page = self.confluence.get_page_by_id(

31 page_id=page_id, expand="body.storage,version,space"

32 )

33 space_key = page.get("space", {}).get("key", "")

34 content = page["body"]["storage"]["value"]

35 processed_html, processed_markdown = self._process_html_content(

36 content, space_key

37 )

38

39 metadata = {

40 "page_id": page_id,

41 "title": page.get("title", ""),

42 "version": page.get("version", {}).get("number"),

43 "space_key": space_key,

44 "space_name": page.get("space", {}).get("name", ""),

45 "last_modified": page.get("version", {}).get("when"),

46 "author_name": page.get("version", {}).get("by", {}).get("displayName", ""),

47 "url": f"{self.config.url}/spaces/{space_key}/pages/{page_id}",

48 }

49

50 return Document(

51 page_content=processed_markdown if convert_to_markdown else processed_html,

52 metadata=metadata,

53 )

54

55 def get_page_by_title(

56 self, space_key: str, title: str, *, convert_to_markdown: bool = True

57 ) -> Document | None:

58 """

59 Get a specific page by its title from a Confluence space.

60

61 Args:

62 space_key: The key of the space containing the page

63 title: The title of the page to retrieve

64 convert_to_markdown: When True, returns content in markdown format,

65 otherwise returns raw HTML (keyword-only)

66

67 Returns:

68 Document containing the page content and metadata, or None if not found

69 """

70 try:

71 # First check if the space exists

72 spaces = self.confluence.get_all_spaces(start=0, limit=500)

73

74 # Handle case where spaces can be a dictionary with a "results" key

75 if isinstance(spaces, dict) and "results" in spaces:

76 space_keys = [s["key"] for s in spaces["results"]]

77 else:

78 space_keys = [s["key"] for s in spaces]

79

80 if space_key not in space_keys:

81 logger.warning(f"Space {space_key} not found")

82 return None

83

84 # Then try to find the page by title

85 page = self.confluence.get_page_by_title(

86 space=space_key, title=title, expand="body.storage,version"

87 )

88

89 if not page:

90 logger.warning(f"Page '{title}' not found in space {space_key}")

91 return None

92

93 content = page["body"]["storage"]["value"]

94 processed_html, processed_markdown = self._process_html_content(

95 content, space_key

96 )

97

98 metadata = {

99 "page_id": page["id"],

100 "title": page["title"],

101 "version": page.get("version", {}).get("number"),

102 "space_key": space_key,

103 "space_name": page.get("space", {}).get("name", ""),

104 "last_modified": page.get("version", {}).get("when"),

105 "author_name": page.get("version", {})

106 .get("by", {})

107 .get("displayName", ""),

108 "url": f"{self.config.url}/spaces/{space_key}/pages/{page['id']}",

109 }

110

111 return Document(

112 page_content=processed_markdown

113 if convert_to_markdown

114 else processed_html,

115 metadata=metadata,

116 )

117

118 except KeyError as e:

119 logger.error(f"Missing key in page data: {str(e)}")

120 return None

121 except requests.RequestException as e:

122 logger.error(f"Network error when fetching page: {str(e)}")

123 return None

124 except (ValueError, TypeError) as e:

125 logger.error(f"Error processing page data: {str(e)}")

126 return None

127 except Exception as e: # noqa: BLE001 - Intentional fallback with full logging

128 logger.error(f"Unexpected error fetching page: {str(e)}")

129 # Log the full traceback at debug level for troubleshooting

130 logger.debug("Full exception details:", exc_info=True)

131 return None

132

133 def get_space_pages(

134 self,

135 space_key: str,

136 start: int = 0,

137 limit: int = 10,

138 *,

139 convert_to_markdown: bool = True,

140 ) -> list[Document]:

141 """

142 Get all pages from a specific space.

143

144 Args:

145 space_key: The key of the space to get pages from

146 start: The starting index for pagination

147 limit: Maximum number of pages to return

148 convert_to_markdown: When True, returns content in markdown format,

149 otherwise returns raw HTML (keyword-only)

150

151 Returns:

152 List of Document objects containing page content and metadata

153 """

154 pages = self.confluence.get_all_pages_from_space(

155 space=space_key, start=start, limit=limit, expand="body.storage"

156 )

157

158 documents = []

159 for page in pages:

160 content = page["body"]["storage"]["value"]

161 processed_html, processed_markdown = self._process_html_content(

162 content, space_key

163 )

164

165 metadata = {

166 "page_id": page["id"],

167 "title": page["title"],

168 "space_key": space_key,

169 "space_name": page.get("space", {}).get("name", ""),

170 "version": page.get("version", {}).get("number"),

171 "last_modified": page.get("version", {}).get("when"),

172 "author_name": page.get("version", {})

173 .get("by", {})

174 .get("displayName", ""),

175 "url": f"{self.config.url}/spaces/{space_key}/pages/{page['id']}",

176 }

177

178 documents.append(

179 Document(

180 page_content=processed_markdown

181 if convert_to_markdown

182 else processed_html,

183 metadata=metadata,

184 )

185 )

186

187 return documents

188

189 def create_page(

190 self, space_key: str, title: str, body: str, parent_id: str | None = None

191 ) -> Document:

192 """

193 Create a new page in a Confluence space.

194

195 Args:

196 space_key: The key of the space

197 title: The title of the page

198 body: The content of the page in storage format (HTML)

199 parent_id: Optional parent page ID

200

201 Returns:

202 Document with the created page content and metadata

203

204 Raises:

205 Exception: If there is an error creating the page

206 """

207 try:

208 # Create the page

209 page = self.confluence.create_page(

210 space=space_key,

211 title=title,

212 body=body,

213 parent_id=parent_id,

214 representation="storage",

215 )

216

217 # Return the created page as a Document

218 return self.get_page_content(page["id"])

219 except Exception as e:

220 logger.error(f"Error creating page in space {space_key}: {str(e)}")

221 raise

222

223 def update_page(

224 self,

225 page_id: str,

226 title: str,

227 body: str,

228 *,

229 is_minor_edit: bool = False,

230 version_comment: str = "",

231 ) -> Document:

232 """

233 Update an existing page in Confluence.

234

235 Args:

236 page_id: The ID of the page to update

237 title: The new title of the page

238 body: The new content of the page in storage format (HTML)

239 is_minor_edit: Whether this is a minor edit (affects notifications,

240 keyword-only)

241 version_comment: Optional comment for this version (keyword-only)

242

243 Returns:

244 Document with the updated page content and metadata

245

246 Raises:

247 Exception: If there is an error updating the page

248 """

249 try:

250 # Get the current page first for consistency with the original

251 # implementation

252 # This is needed for the test_update_page_with_error test

253 self.confluence.get_page_by_id(page_id=page_id)

254

255 # Update the page

256 self.confluence.update_page(

257 page_id=page_id,

258 title=title,

259 body=body,

260 minor_edit=is_minor_edit,

261 version_comment=version_comment,

262 )

263

264 # Return the updated page as a Document

265 return self.get_page_content(page_id)

266 except Exception as e:

267 logger.error(f"Error updating page {page_id}: {str(e)}")

268 raise