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

52 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-10 03:26 +0900

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

2 

3import logging 

4from typing import cast 

5 

6import requests 

7 

8from .client import ConfluenceClient 

9 

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

11 

12 

13class 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 {}