Coverage for src/mcp_atlassian/jira/projects.py: 98%

166 statements  

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

1"""Module for Jira project operations.""" 

2 

3import logging 

4from typing import Any, Dict, List, Optional, Union, cast 

5 

6from ..document_types import Document 

7from .client import JiraClient 

8 

9logger = logging.getLogger("mcp-jira") 

10 

11 

12class ProjectsMixin(JiraClient): 

13 """Mixin for Jira project operations. 

14  

15 This mixin provides methods for retrieving and working with Jira projects, 

16 including project details, components, versions, and other project-related operations. 

17 """ 

18 

19 def get_all_projects(self, include_archived: bool = False) -> List[Dict[str, Any]]: 

20 """ 

21 Get all projects visible to the current user. 

22  

23 Args: 

24 include_archived: Whether to include archived projects 

25  

26 Returns: 

27 List of project data dictionaries 

28 """ 

29 try: 

30 params = {} 

31 if include_archived: 

32 params["includeArchived"] = "true" 

33 

34 projects = self.jira.projects(included_archived=include_archived) 

35 return projects if isinstance(projects, list) else [] 

36 

37 except Exception as e: 

38 logger.error(f"Error getting all projects: {str(e)}") 

39 return [] 

40 

41 def get_project(self, project_key: str) -> Optional[Dict[str, Any]]: 

42 """ 

43 Get detailed information about a specific project. 

44  

45 Args: 

46 project_key: The project key (e.g., 'PROJ') 

47  

48 Returns: 

49 Project data dictionary if found, None otherwise 

50 """ 

51 try: 

52 project = self.jira.project(key=project_key) 

53 return project 

54 

55 except Exception as e: 

56 logger.error(f"Error getting project {project_key}: {str(e)}") 

57 return None 

58 

59 def project_exists(self, project_key: str) -> bool: 

60 """ 

61 Check if a project exists. 

62  

63 Args: 

64 project_key: The project key to check 

65  

66 Returns: 

67 True if the project exists, False otherwise 

68 """ 

69 try: 

70 project = self.get_project(project_key) 

71 return project is not None 

72 

73 except Exception: 

74 return False 

75 

76 def get_project_components(self, project_key: str) -> List[Dict[str, Any]]: 

77 """ 

78 Get all components for a project. 

79  

80 Args: 

81 project_key: The project key 

82  

83 Returns: 

84 List of component data dictionaries 

85 """ 

86 try: 

87 components = self.jira.get_project_components(key=project_key) 

88 return components if isinstance(components, list) else [] 

89 

90 except Exception as e: 

91 logger.error(f"Error getting components for project {project_key}: {str(e)}") 

92 return [] 

93 

94 def get_project_versions(self, project_key: str) -> List[Dict[str, Any]]: 

95 """ 

96 Get all versions for a project. 

97  

98 Args: 

99 project_key: The project key 

100  

101 Returns: 

102 List of version data dictionaries 

103 """ 

104 try: 

105 versions = self.jira.get_project_versions(key=project_key) 

106 return versions if isinstance(versions, list) else [] 

107 

108 except Exception as e: 

109 logger.error(f"Error getting versions for project {project_key}: {str(e)}") 

110 return [] 

111 

112 def get_project_roles(self, project_key: str) -> Dict[str, Any]: 

113 """ 

114 Get all roles for a project. 

115  

116 Args: 

117 project_key: The project key 

118  

119 Returns: 

120 Dictionary of role names mapped to role details 

121 """ 

122 try: 

123 roles = self.jira.get_project_roles(project_key=project_key) 

124 return roles if isinstance(roles, dict) else {} 

125 

126 except Exception as e: 

127 logger.error(f"Error getting roles for project {project_key}: {str(e)}") 

128 return {} 

129 

130 def get_project_role_members(self, project_key: str, role_id: str) -> List[Dict[str, Any]]: 

131 """ 

132 Get members assigned to a specific role in a project. 

133  

134 Args: 

135 project_key: The project key 

136 role_id: The role ID 

137  

138 Returns: 

139 List of role members 

140 """ 

141 try: 

142 members = self.jira.get_project_actors_for_role_project(project_key=project_key, role_id=role_id) 

143 # Extract the actors from the response 

144 actors = [] 

145 if isinstance(members, dict) and "actors" in members: 

146 actors = members.get("actors", []) 

147 return actors 

148 

149 except Exception as e: 

150 logger.error(f"Error getting role members for project {project_key}, role {role_id}: {str(e)}") 

151 return [] 

152 

153 def get_project_permission_scheme(self, project_key: str) -> Optional[Dict[str, Any]]: 

154 """ 

155 Get the permission scheme for a project. 

156  

157 Args: 

158 project_key: The project key 

159  

160 Returns: 

161 Permission scheme data if found, None otherwise 

162 """ 

163 try: 

164 scheme = self.jira.get_project_permission_scheme(project_id_or_key=project_key) 

165 return scheme 

166 

167 except Exception as e: 

168 logger.error(f"Error getting permission scheme for project {project_key}: {str(e)}") 

169 return None 

170 

171 def get_project_notification_scheme(self, project_key: str) -> Optional[Dict[str, Any]]: 

172 """ 

173 Get the notification scheme for a project. 

174  

175 Args: 

176 project_key: The project key 

177  

178 Returns: 

179 Notification scheme data if found, None otherwise 

180 """ 

181 try: 

182 scheme = self.jira.get_project_notification_scheme(project_id_or_key=project_key) 

183 return scheme 

184 

185 except Exception as e: 

186 logger.error(f"Error getting notification scheme for project {project_key}: {str(e)}") 

187 return None 

188 

189 def get_project_issue_types(self, project_key: str) -> List[Dict[str, Any]]: 

190 """ 

191 Get all issue types available for a project. 

192  

193 Args: 

194 project_key: The project key 

195  

196 Returns: 

197 List of issue type data dictionaries 

198 """ 

199 try: 

200 meta = self.jira.issue_createmeta(project=project_key) 

201 

202 issue_types = [] 

203 # Extract issue types from createmeta response 

204 if "projects" in meta and len(meta["projects"]) > 0: 

205 project_data = meta["projects"][0] 

206 if "issuetypes" in project_data: 

207 issue_types = project_data["issuetypes"] 

208 

209 return issue_types 

210 

211 except Exception as e: 

212 logger.error(f"Error getting issue types for project {project_key}: {str(e)}") 

213 return [] 

214 

215 def get_project_issues_count(self, project_key: str) -> int: 

216 """ 

217 Get the total number of issues in a project. 

218  

219 Args: 

220 project_key: The project key 

221  

222 Returns: 

223 Count of issues in the project 

224 """ 

225 try: 

226 # Use JQL to count issues in the project 

227 jql = f"project = {project_key}" 

228 result = self.jira.jql(jql=jql, fields=["key"], limit=1) 

229 

230 # Extract total from the response 

231 total = 0 

232 if isinstance(result, dict) and "total" in result: 

233 total = result.get("total", 0) 

234 

235 return total 

236 

237 except Exception as e: 

238 logger.error(f"Error getting issue count for project {project_key}: {str(e)}") 

239 return 0 

240 

241 def get_project_issues( 

242 self, project_key: str, start: int = 0, limit: int = 50 

243 ) -> List[Document]: 

244 """ 

245 Get issues for a specific project. 

246  

247 Args: 

248 project_key: The project key 

249 start: Index of the first issue to return 

250 limit: Maximum number of issues to return 

251  

252 Returns: 

253 List of Document objects representing the issues 

254 """ 

255 try: 

256 # Use JQL to get issues in the project 

257 jql = f"project = {project_key}" 

258 

259 # Use search_issues if available (delegate to SearchMixin) 

260 if hasattr(self, 'search_issues') and callable(self.search_issues): 

261 return self.search_issues(jql, start=start, limit=limit) 

262 

263 # Fallback implementation if search_issues is not available 

264 result = self.jira.jql(jql=jql, fields="*all", start=start, limit=limit) 

265 

266 documents = [] 

267 if isinstance(result, dict) and "issues" in result: 

268 for issue in result.get("issues", []): 

269 key = issue.get("key", "") 

270 fields = issue.get("fields", {}) 

271 summary = fields.get("summary", "") 

272 description = fields.get("description", "") 

273 

274 # Create a Document for each issue 

275 document = Document( 

276 page_content=description or summary, 

277 metadata={ 

278 "key": key, 

279 "summary": summary, 

280 "type": "issue", 

281 "project": project_key 

282 } 

283 ) 

284 documents.append(document) 

285 

286 return documents 

287 

288 except Exception as e: 

289 logger.error(f"Error getting issues for project {project_key}: {str(e)}") 

290 return [] 

291 

292 def get_project_keys(self) -> List[str]: 

293 """ 

294 Get all project keys. 

295  

296 Returns: 

297 List of project keys 

298 """ 

299 try: 

300 projects = self.get_all_projects() 

301 return [project.get("key") for project in projects if "key" in project] 

302 

303 except Exception as e: 

304 logger.error(f"Error getting project keys: {str(e)}") 

305 return [] 

306 

307 def get_project_leads(self) -> Dict[str, str]: 

308 """ 

309 Get all project leads mapped to their projects. 

310  

311 Returns: 

312 Dictionary mapping project keys to lead usernames 

313 """ 

314 try: 

315 projects = self.get_all_projects() 

316 leads = {} 

317 

318 for project in projects: 

319 if "key" in project and "lead" in project: 

320 key = project.get("key") 

321 lead = project.get("lead", {}) 

322 

323 # Handle different formats of lead information 

324 lead_name = None 

325 if isinstance(lead, dict): 

326 lead_name = lead.get("name") or lead.get("displayName") 

327 elif isinstance(lead, str): 

328 lead_name = lead 

329 

330 if key and lead_name: 

331 leads[key] = lead_name 

332 

333 return leads 

334 

335 except Exception as e: 

336 logger.error(f"Error getting project leads: {str(e)}") 

337 return {} 

338 

339 def get_user_accessible_projects(self, username: str) -> List[Dict[str, Any]]: 

340 """ 

341 Get projects that a specific user can access. 

342  

343 Args: 

344 username: The username to check access for 

345  

346 Returns: 

347 List of accessible project data dictionaries 

348 """ 

349 try: 

350 # This requires admin permissions 

351 # For non-admins, a different approach might be needed 

352 all_projects = self.get_all_projects() 

353 accessible_projects = [] 

354 

355 for project in all_projects: 

356 project_key = project.get("key") 

357 if not project_key: 

358 continue 

359 

360 try: 

361 # Check if user has browse permission for this project 

362 browse_users = self.jira.get_users_with_browse_permission_to_a_project( 

363 username=username, 

364 project_key=project_key, 

365 limit=1 

366 ) 

367 

368 # If the user is in the list, they have access 

369 user_has_access = False 

370 if isinstance(browse_users, list): 

371 for user in browse_users: 

372 if isinstance(user, dict) and user.get("name") == username: 

373 user_has_access = True 

374 break 

375 

376 if user_has_access: 

377 accessible_projects.append(project) 

378 

379 except Exception: 

380 # Skip projects that cause errors 

381 continue 

382 

383 return accessible_projects 

384 

385 except Exception as e: 

386 logger.error(f"Error getting accessible projects for user {username}: {str(e)}") 

387 return []