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

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

2

3 import logging

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

5

6 from ..document_types import Document

7 from .client import JiraClient

8

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

10

11

12 class 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 []