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
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-10 03:26 +0900
1"""Module for Jira project operations."""
3import logging
4from typing import Any, Dict, List, Optional, Union, cast
6from ..document_types import Document
7from .client import JiraClient
9logger = logging.getLogger("mcp-jira")
12class ProjectsMixin(JiraClient):
13 """Mixin for Jira project operations.
15 This mixin provides methods for retrieving and working with Jira projects,
16 including project details, components, versions, and other project-related operations.
17 """
19 def get_all_projects(self, include_archived: bool = False) -> List[Dict[str, Any]]:
20 """
21 Get all projects visible to the current user.
23 Args:
24 include_archived: Whether to include archived projects
26 Returns:
27 List of project data dictionaries
28 """
29 try:
30 params = {}
31 if include_archived:
32 params["includeArchived"] = "true"
34 projects = self.jira.projects(included_archived=include_archived)
35 return projects if isinstance(projects, list) else []
37 except Exception as e:
38 logger.error(f"Error getting all projects: {str(e)}")
39 return []
41 def get_project(self, project_key: str) -> Optional[Dict[str, Any]]:
42 """
43 Get detailed information about a specific project.
45 Args:
46 project_key: The project key (e.g., 'PROJ')
48 Returns:
49 Project data dictionary if found, None otherwise
50 """
51 try:
52 project = self.jira.project(key=project_key)
53 return project
55 except Exception as e:
56 logger.error(f"Error getting project {project_key}: {str(e)}")
57 return None
59 def project_exists(self, project_key: str) -> bool:
60 """
61 Check if a project exists.
63 Args:
64 project_key: The project key to check
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
73 except Exception:
74 return False
76 def get_project_components(self, project_key: str) -> List[Dict[str, Any]]:
77 """
78 Get all components for a project.
80 Args:
81 project_key: The project key
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 []
90 except Exception as e:
91 logger.error(f"Error getting components for project {project_key}: {str(e)}")
92 return []
94 def get_project_versions(self, project_key: str) -> List[Dict[str, Any]]:
95 """
96 Get all versions for a project.
98 Args:
99 project_key: The project key
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 []
108 except Exception as e:
109 logger.error(f"Error getting versions for project {project_key}: {str(e)}")
110 return []
112 def get_project_roles(self, project_key: str) -> Dict[str, Any]:
113 """
114 Get all roles for a project.
116 Args:
117 project_key: The project key
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 {}
126 except Exception as e:
127 logger.error(f"Error getting roles for project {project_key}: {str(e)}")
128 return {}
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.
134 Args:
135 project_key: The project key
136 role_id: The role ID
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
149 except Exception as e:
150 logger.error(f"Error getting role members for project {project_key}, role {role_id}: {str(e)}")
151 return []
153 def get_project_permission_scheme(self, project_key: str) -> Optional[Dict[str, Any]]:
154 """
155 Get the permission scheme for a project.
157 Args:
158 project_key: The project key
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
167 except Exception as e:
168 logger.error(f"Error getting permission scheme for project {project_key}: {str(e)}")
169 return None
171 def get_project_notification_scheme(self, project_key: str) -> Optional[Dict[str, Any]]:
172 """
173 Get the notification scheme for a project.
175 Args:
176 project_key: The project key
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
185 except Exception as e:
186 logger.error(f"Error getting notification scheme for project {project_key}: {str(e)}")
187 return None
189 def get_project_issue_types(self, project_key: str) -> List[Dict[str, Any]]:
190 """
191 Get all issue types available for a project.
193 Args:
194 project_key: The project key
196 Returns:
197 List of issue type data dictionaries
198 """
199 try:
200 meta = self.jira.issue_createmeta(project=project_key)
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"]
209 return issue_types
211 except Exception as e:
212 logger.error(f"Error getting issue types for project {project_key}: {str(e)}")
213 return []
215 def get_project_issues_count(self, project_key: str) -> int:
216 """
217 Get the total number of issues in a project.
219 Args:
220 project_key: The project key
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)
230 # Extract total from the response
231 total = 0
232 if isinstance(result, dict) and "total" in result:
233 total = result.get("total", 0)
235 return total
237 except Exception as e:
238 logger.error(f"Error getting issue count for project {project_key}: {str(e)}")
239 return 0
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.
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
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}"
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)
263 # Fallback implementation if search_issues is not available
264 result = self.jira.jql(jql=jql, fields="*all", start=start, limit=limit)
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", "")
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)
286 return documents
288 except Exception as e:
289 logger.error(f"Error getting issues for project {project_key}: {str(e)}")
290 return []
292 def get_project_keys(self) -> List[str]:
293 """
294 Get all project keys.
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]
303 except Exception as e:
304 logger.error(f"Error getting project keys: {str(e)}")
305 return []
307 def get_project_leads(self) -> Dict[str, str]:
308 """
309 Get all project leads mapped to their projects.
311 Returns:
312 Dictionary mapping project keys to lead usernames
313 """
314 try:
315 projects = self.get_all_projects()
316 leads = {}
318 for project in projects:
319 if "key" in project and "lead" in project:
320 key = project.get("key")
321 lead = project.get("lead", {})
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
330 if key and lead_name:
331 leads[key] = lead_name
333 return leads
335 except Exception as e:
336 logger.error(f"Error getting project leads: {str(e)}")
337 return {}
339 def get_user_accessible_projects(self, username: str) -> List[Dict[str, Any]]:
340 """
341 Get projects that a specific user can access.
343 Args:
344 username: The username to check access for
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 = []
355 for project in all_projects:
356 project_key = project.get("key")
357 if not project_key:
358 continue
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 )
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
376 if user_has_access:
377 accessible_projects.append(project)
379 except Exception:
380 # Skip projects that cause errors
381 continue
383 return accessible_projects
385 except Exception as e:
386 logger.error(f"Error getting accessible projects for user {username}: {str(e)}")
387 return []