Coverage for src/mcp_atlassian/confluence/spaces.py: 88%

1"""Module for Confluence space operations."""

2

3 import logging

4 from typing import cast

5

6 import requests

7

8 from .client import ConfluenceClient

9

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

11

12

13 class SpacesMixin(ConfluenceClient):

14 """Mixin for Confluence space operations."""

15

16 def get_spaces(self, start: int = 0, limit: int = 10) -> dict[str, object]:

17 """

18 Get all available spaces.

19

20 Args:

21 start: The starting index for pagination

22 limit: Maximum number of spaces to return

23

24 Returns:

25 Dictionary containing space information with results and metadata

26 """

27 spaces = self.confluence.get_all_spaces(start=start, limit=limit)

28 # Cast the return value to the expected type

29 return cast(dict[str, object], spaces)

30

31 def get_user_contributed_spaces(self, limit: int = 250) -> dict:

32 """

33 Get spaces the current user has contributed to.

34

35 Args:

36 limit: Maximum number of results to return

37

38 Returns:

39 Dictionary of space keys to space information

40 """

41 try:

42 # Use CQL to find content the user has contributed to

43 cql = "contributor = currentUser() order by lastmodified DESC"

44 results = self.confluence.cql(cql=cql, limit=limit)

45

46 # Extract and deduplicate spaces

47 spaces = {}

48 for result in results.get("results", []):

49 space_key = None

50 space_name = None

51

52 # Try to extract space from container

53 if "resultGlobalContainer" in result:

54 container = result.get("resultGlobalContainer", {})

55 space_name = container.get("title")

56 display_url = container.get("displayUrl", "")

57 if display_url and "/spaces/" in display_url:

58 space_key = display_url.split("/spaces/")[1].split("/")[0]

59

60 # Try to extract from content expandable

61 if (

62 not space_key

63 and "content" in result

64 and "_expandable" in result["content"]

65 ):

66 expandable = result["content"].get("_expandable", {})

67 space_path = expandable.get("space", "")

68 if space_path and space_path.startswith("/rest/api/space/"):

69 space_key = space_path.split("/rest/api/space/")[1]

70

71 # Try to extract from URL

72 if not space_key and "url" in result:

73 url = result.get("url", "")

74 if url and url.startswith("/spaces/"):

75 space_key = url.split("/spaces/")[1].split("/")[0]

76

77 # Only add if we found a space key and it's not already in our results

78 if space_key and space_key not in spaces:

79 # Add some defaults if we couldn't extract all fields

80 space_name = space_name or f"Space {space_key}"

81 spaces[space_key] = {"key": space_key, "name": space_name}

82

83 return spaces

84

85 except KeyError as e:

86 logger.error(f"Missing key in Confluence spaces data: {str(e)}")

87 return {}

88 except ValueError as e:

89 logger.error(f"Invalid value in Confluence spaces: {str(e)}")

90 return {}

91 except TypeError as e:

92 logger.error(f"Type error when processing Confluence spaces: {str(e)}")

93 return {}

94 except requests.RequestException as e:

95 logger.error(f"Network error when fetching spaces: {str(e)}")

96 return {}

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

98 logger.error(f"Unexpected error fetching Confluence spaces: {str(e)}")

99 logger.debug("Full exception details for Confluence spaces:", exc_info=True)

100 return {}