xrayclient.xray_client
1import os 2import re 3import time 4import json 5import base64 6import requests 7import mimetypes 8from jira import JIRA 9from jsonpath_nz import log as logger, jprint 10from typing import Optional, Dict, Any, List, Union, Tuple 11 12class JiraHandler(): 13 """A handler class for interacting with JIRA's REST API. 14 This class provides methods to interact with JIRA's REST API, handling authentication, 15 issue creation, retrieval, and various JIRA operations. It uses environment variables for 16 configuration and provides robust error handling with comprehensive logging. 17 The class supports creating issues with attachments, linking issues, and retrieving 18 detailed issue information with customizable field selection. It handles various 19 JIRA field types including user information, attachments, comments, and issue links. 20 Attributes 21 ---------- 22 client : JIRA 23 The JIRA client instance used for API interactions 24 Environment Variables 25 -------------------- 26 JIRA_SERVER : str 27 The JIRA server URL (default: 'https://arusa.atlassian.net') 28 JIRA_USER : str 29 The JIRA user email (default: 'yakub@arusatech.com') 30 JIRA_API_KEY : str 31 The JIRA API key for authentication (required) 32 Methods 33 ------- 34 create_issue(project_key, summary, description, **kwargs) 35 Create a new JIRA issue with optional attachments, linking, and custom fields 36 get_issue(issue_key, fields=None) 37 Retrieve a JIRA issue with specified fields or all available fields 38 Examples 39 -------- 40 >>> handler = JiraHandler() 41 >>> # Create a new issue 42 >>> issue_key, issue_id = handler.create_issue( 43 ... project_key="PROJ", 44 ... summary="New feature implementation", 45 ... description="Implement new login flow", 46 ... issue_type="Story", 47 ... priority="High", 48 ... labels=["feature", "login"], 49 ... attachments=["/path/to/screenshot.png"] 50 ... ) 51 >>> print(f"Created issue {issue_key} with ID {issue_id}") 52 >>> # Retrieve issue details 53 >>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"]) 54 >>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}") 55 Notes 56 ----- 57 - Requires valid JIRA credentials stored in environment variables 58 - Automatically loads configuration from .env file if present 59 - Provides comprehensive error handling and logging 60 - Supports various JIRA field types and custom fields 61 - Handles file attachments with automatic MIME type detection 62 - Creates issue links with configurable link types 63 - Returns None for failed operations instead of raising exceptions 64 """ 65 66 def __init__(self): 67 """Initialize the JIRA client with configuration from environment variables. 68 This constructor sets up the JIRA client by reading configuration from 69 environment variables. It automatically loads variables from a .env file 70 if present in the project root. 71 Environment Variables 72 ------------------- 73 JIRA_SERVER : str 74 The JIRA server URL (default: 'https://arusatech.atlassian.net') 75 JIRA_USER : str 76 The JIRA user email (default: 'yakub@arusatech.com') 77 JIRA_API_KEY : str 78 The JIRA API key for authentication 79 Raises 80 ------ 81 Exception 82 If the JIRA client initialization fails 83 """ 84 try: 85 # Load environment variables from .env file 86 jira_server = os.getenv('JIRA_SERVER', 'https://arusatech.atlassian.net') 87 jira_user = os.getenv('JIRA_USER', 'yakub@arusatech.com') 88 jira_api_key = os.getenv('JIRA_API_KEY', "") 89 # Validate required environment variables 90 if not jira_api_key or jira_api_key == '<JIRA_API_KEY>': 91 raise ValueError("JIRA_API_KEY environment variable is required and must be set to a valid API key") 92 self.client = JIRA( 93 server=jira_server, 94 basic_auth=(jira_user, jira_api_key) 95 ) 96 logger.info("JIRA client initialized successfully") 97 except Exception as e: 98 logger.error(f"Failed to initialize JIRA client: {str(e)}") 99 logger.traceback(e) 100 raise 101 102 def create_issue( 103 self, 104 project_key: str, 105 summary: str, 106 description: str, 107 issue_type: str = None, 108 priority: str = None, 109 assignee: str = None, 110 labels: List[str] = None, 111 components: List[str] = None, 112 attachments: List[str] = None, 113 parent_issue_key: str = None, 114 linked_issues: List[Dict[str, str]] = None, 115 custom_fields: Dict[str, Any] = None 116 ) -> Optional[tuple[str, str]]: 117 """Create a new issue in JIRA with the specified details. 118 This method creates a new JIRA issue with the provided details and handles 119 optional features like attachments and issue linking. 120 Parameters 121 ---------- 122 project_key : str 123 The key of the project where the issue should be created 124 summary : str 125 The summary/title of the issue 126 description : str 127 The detailed description of the issue 128 issue_type : str, optional 129 The type of issue (default: 'Bug') 130 priority : str, optional 131 The priority of the issue 132 assignee : str, optional 133 The username of the assignee 134 labels : List[str], optional 135 List of labels to add to the issue 136 components : List[str], optional 137 List of component names to add to the issue 138 attachments : List[str], optional 139 List of file paths to attach to the issue 140 linked_issues : List[Dict[str, str]], optional 141 List of issues to link to the new issue. Each dict should contain: 142 - 'key': The issue key to link to 143 - 'type': The type of link (default: 'Relates') 144 custom_fields : Dict[str, Any], optional 145 Dictionary of custom fields to set on the issue 146 Returns 147 ------- 148 Optional[tuple[str, str]] 149 A tuple containing (issue_key, issue_id) if successful, 150 (None, None) if creation fails 151 Examples 152 -------- 153 >>> handler = JiraHandler() 154 >>> result = handler.create_issue( 155 ... project_key="PROJ", 156 ... summary="Bug in login", 157 ... description="User cannot login with valid credentials", 158 ... issue_type="Bug", 159 ... priority="High", 160 ... labels=["login", "bug"], 161 ... components=["Authentication"], 162 ... attachments=["/path/to/screenshot.png"], 163 ... linked_issues=[{"key": "PROJ-123", "type": "Blocks"}] 164 ... ) 165 >>> print(f"Created issue {result[0]} with ID {result[1]}") 166 """ 167 try: 168 # Build basic issue fields 169 issue_dict = { 170 'project': {'key': project_key}, 171 'summary': summary, 172 'description': description, 173 'issuetype': {'name': issue_type or 'Bug'}, 174 'parent': {'key': parent_issue_key} if parent_issue_key else None 175 } 176 # Add optional fields 177 if priority: 178 issue_dict['priority'] = {'name': priority} 179 if assignee: 180 issue_dict['assignee'] = {'name': assignee} 181 if labels: 182 issue_dict['labels'] = labels 183 if components: 184 issue_dict['components'] = [{'name': c} for c in components] 185 # Add any custom fields 186 if custom_fields: 187 issue_dict.update(custom_fields) 188 # Create the issue 189 issue = self.client.create_issue(fields=issue_dict) 190 logger.info(f"Created JIRA issue : {issue.key} [ID: {issue.id}]") 191 # Add attachments if provided 192 if attachments: 193 for file_path in attachments: 194 if os.path.exists(file_path): 195 self.client.add_attachment( 196 issue=issue.key, 197 attachment=file_path 198 ) 199 logger.info(f"Added attachment: {file_path}") 200 else: 201 logger.warning(f"Attachment not found: {file_path}") 202 # Create issue links if provided 203 if linked_issues: 204 for link in linked_issues: 205 try: 206 self.client.create_issue_link( 207 link.get('type', 'Relates'), 208 issue.key, 209 link['key'] 210 ) 211 logger.info(f"Created link between {issue.key} and {link['key']}") 212 except Exception as e: 213 logger.error(f"Failed to create link to {link['key']}: {str(e)}") 214 return (issue.key, issue.id) 215 except Exception as e: 216 logger.error(f"Failed to create JIRA issue for project {project_key}: {str(e)}") 217 logger.traceback(e) 218 return (None, None) 219 220 def get_issue(self, issue_key: str, fields: List[str] = None) -> Optional[Dict[str, Any]]: 221 """Get an issue by its key with specified fields. 222 This method retrieves a JIRA issue using its key and returns the issue details 223 with the specified fields. If no fields are specified, it returns all available fields. 224 Parameters 225 ---------- 226 issue_key : str 227 The JIRA issue key to retrieve (e.g., "PROJ-123") 228 fields : List[str], optional 229 List of specific fields to retrieve. If None, all fields are returned. 230 Common fields include: "summary", "description", "status", "assignee", 231 "reporter", "created", "updated", "priority", "labels", "components", 232 "attachments", "comments", "issuetype", "project" 233 Returns 234 ------- 235 Optional[Dict[str, Any]] 236 A dictionary containing the issue details if successful, None if the issue 237 is not found or an error occurs. 238 Examples 239 -------- 240 >>> handler = JiraHandler() 241 >>> # Get all fields 242 >>> issue = handler.get_issue("PROJ-123") 243 >>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}") 244 >>> # Get specific fields only 245 >>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"]) 246 >>> print(f"Assignee: {issue['assignee']['displayName'] if issue['assignee'] else 'Unassigned'}") 247 Notes 248 ----- 249 - The issue key must be valid and accessible with current authentication 250 - If fields parameter is None, all fields are returned 251 - Some fields may be None if the issue doesn't have values for them 252 - Failed operations are logged as errors with relevant details 253 - The method handles missing issues gracefully by returning None 254 """ 255 try: 256 if not issue_key: 257 logger.error("Issue key is required") 258 return None 259 # Define field mappings for JIRA API 260 field_mappings = { 261 'summary': 'summary', 262 'description': 'description', 263 'status': 'status', 264 'assignee': 'assignee', 265 'reporter': 'reporter', 266 'priority': 'priority', 267 'labels': 'labels', 268 'components': 'components', 269 'issuetype': 'issuetype', 270 'project': 'project', 271 'created': 'created', 272 'updated': 'updated', 273 'resolutiondate': 'resolutiondate', 274 'duedate': 'duedate', 275 'attachments': 'attachments', 276 'comment': 'comment', 277 'issuelinks': 'issuelinks' 278 } 279 # Determine requested fields 280 if fields is None: 281 requested_fields = None 282 else: 283 # Map requested fields to JIRA field names 284 jira_fields = [field_mappings.get(field, field) for field in fields] 285 # Always include key and id as they're required 286 if 'key' not in fields: 287 jira_fields.append('key') 288 if 'id' not in fields: 289 jira_fields.append('id') 290 requested_fields = ','.join(jira_fields) 291 # Get the issue using the JIRA client 292 issue = self.client.issue(issue_key, fields=requested_fields) 293 if not issue: 294 logger.warning(f"Issue not found: {issue_key}") 295 return None 296 # Helper function to safely get user attributes 297 def get_user_dict(user_obj): 298 if not user_obj: 299 return None 300 try: 301 return { 302 'name': getattr(user_obj, 'name', None), 303 'displayName': getattr(user_obj, 'displayName', None), 304 'emailAddress': getattr(user_obj, 'emailAddress', None) 305 } 306 except Exception: 307 return None 308 # Helper function to safely get field value 309 def safe_get_field(field_name, default=None): 310 try: 311 return getattr(issue.fields, field_name, default) 312 except AttributeError: 313 return default 314 # Helper function to get object attributes safely 315 def get_object_attrs(obj, attrs): 316 if not obj: 317 return None 318 return {attr: getattr(obj, attr, None) for attr in attrs} 319 # Helper function to process attachments 320 def process_attachments(attachments): 321 if not attachments: 322 return [] 323 return [ 324 { 325 'id': getattr(att, 'id', None), 326 'filename': getattr(att, 'filename', None), 327 'size': getattr(att, 'size', None), 328 'created': getattr(att, 'created', None), 329 'mimeType': getattr(att, 'mimeType', None) 330 } for att in attachments 331 ] 332 # Helper function to process comments 333 def process_comments(comments): 334 if not comments or not hasattr(comments, 'comments'): 335 return [] 336 return [ 337 { 338 'id': getattr(comment, 'id', None), 339 'body': getattr(comment, 'body', None), 340 'author': get_user_dict(comment.author), 341 'created': getattr(comment, 'created', None), 342 'updated': getattr(comment, 'updated', None) 343 } for comment in comments.comments 344 ] 345 # Helper function to process issue links 346 def process_issue_links(issue_links): 347 if not issue_links: 348 return [] 349 def process_issue_reference(issue_ref, direction): 350 if not hasattr(issue_ref, direction) or not getattr(issue_ref, direction): 351 return None 352 ref_issue = getattr(issue_ref, direction) 353 return { 354 'key': getattr(ref_issue, 'key', None), 355 'id': getattr(ref_issue, 'id', None), 356 'fields': { 357 'summary': getattr(ref_issue.fields, 'summary', None), 358 'status': get_object_attrs(ref_issue.fields.status, ['name']) if ref_issue.fields.status else None 359 } 360 } 361 return [ 362 { 363 'id': getattr(link, 'id', None), 364 'type': get_object_attrs(link.type, ['id', 'name', 'inward', 'outward']) if link.type else None, 365 'inwardIssue': process_issue_reference(link, 'inwardIssue'), 366 'outwardIssue': process_issue_reference(link, 'outwardIssue') 367 } for link in issue_links 368 ] 369 # Build response dictionary 370 issue_dict = { 371 'key': issue.key, 372 'id': issue.id 373 } 374 # Determine which fields to process 375 fields_to_process = fields if fields is not None else list(field_mappings.keys()) 376 # Process each field 377 for field in fields_to_process: 378 if field in ['key', 'id']: 379 continue # Already handled 380 field_value = safe_get_field(field_mappings.get(field, field)) 381 match field: 382 case 'summary' | 'description' | 'created' | 'updated' | 'resolutiondate' | 'duedate': 383 issue_dict[field] = field_value 384 case 'status' | 'issuetype' | 'priority': 385 issue_dict[field] = get_object_attrs(field_value, ['id', 'name', 'description']) 386 case 'project': 387 issue_dict[field] = get_object_attrs(field_value, ['key', 'name', 'id']) 388 case 'assignee' | 'reporter': 389 issue_dict[field] = get_user_dict(field_value) 390 case 'labels': 391 issue_dict[field] = list(field_value) if field_value else [] 392 case 'components': 393 issue_dict[field] = [ 394 get_object_attrs(comp, ['id', 'name', 'description']) 395 for comp in (field_value or []) 396 ] 397 case 'attachments': 398 issue_dict[field] = process_attachments(field_value) 399 case 'comments': 400 issue_dict[field] = process_comments(field_value) 401 case 'issuelinks': 402 issue_dict[field] = process_issue_links(field_value) 403 case _: 404 # Handle unknown fields or custom fields 405 issue_dict[field] = field_value 406 # logger.info(f"Retrieved JIRA issue: {issue_key}") 407 return issue_dict 408 except Exception as e: 409 logger.error(f"Failed to get JIRA issue {issue_key}: {str(e)}") 410 logger.traceback(e) 411 return None 412 413 def update_issue_summary(self, issue_key: str, new_summary: str) -> bool: 414 """Update the summary of a JIRA issue. 415 This method updates the summary field of an existing JIRA issue using the JIRA REST API. 416 It validates the input parameters and handles errors gracefully with comprehensive logging. 417 Parameters 418 ---------- 419 issue_key : str 420 The JIRA issue key to update (e.g., "PROJ-123") 421 new_summary : str 422 The new summary text to set for the issue 423 Returns 424 ------- 425 bool 426 True if the summary was successfully updated, False if the operation failed. 427 Returns None if an error occurs during the API request. 428 Examples 429 -------- 430 >>> handler = JiraHandler() 431 >>> success = handler.update_issue_summary("PROJ-123", "Updated bug description") 432 >>> print(success) 433 True 434 Notes 435 ----- 436 - The issue key must be valid and accessible with current authentication 437 - The new summary cannot be empty or None 438 - Failed operations are logged as errors with relevant details 439 - The method uses the JIRA client's update method for efficient API calls 440 """ 441 try: 442 # Validate input parameters 443 if not issue_key or not issue_key.strip(): 444 logger.error("Issue key is required and cannot be empty") 445 return False 446 if not new_summary or not new_summary.strip(): 447 logger.error("New summary is required and cannot be empty") 448 return False 449 # Strip whitespace from inputs 450 issue_key = issue_key.strip() 451 new_summary = new_summary.strip() 452 # logger.info(f"Updating summary for issue {issue_key}") 453 # Get the issue object 454 issue = self.client.issue(issue_key) 455 if not issue: 456 logger.error(f"Issue not found: {issue_key}") 457 return False 458 # Update the summary field 459 issue.update(summary=new_summary) 460 logger.info(f"Successfully updated summary for issue {issue_key}") 461 return True 462 except Exception as e: 463 logger.error(f"Failed to update summary for issue {issue_key}: {str(e)}") 464 logger.traceback(e) 465 return False 466 467 def _build_auth_headers(self, api_key: str = None, user: str = None, cookie: str = None) -> Dict[str, str]: 468 """ 469 Build authentication headers for JIRA API requests. 470 471 This method converts an API key to base64 format and creates the proper 472 Authorization header, similar to how Postman generates it. 473 474 Parameters 475 ---------- 476 api_key : str, optional 477 The JIRA API key. If not provided, uses the one from environment variables. 478 user : str, optional 479 The JIRA user email. If not provided, uses the one from environment variables. 480 cookie : str, optional 481 Additional cookie value to include in headers. 482 483 Returns 484 ------- 485 Dict[str, str] 486 Dictionary containing the Authorization and Cookie headers. 487 488 Examples 489 -------- 490 >>> handler = JiraHandler() 491 >>> headers = handler._build_auth_headers() 492 >>> print(headers) 493 { 494 'Authorization': 'Basic eWFrdWIubW9oYW1tYWRAd25jby5jb206QVRBVFQzeEZmR0YwN29tcFRCcU9FVUxlXzJjWlFDbkJXb2ZTYS1xMW92YmYxYnBURC1URmppY3VFczVBUzFJMkdjaXcybHlNMEFaRjl1T19OSU0yR0tIMlZ6SkQtQ0JtLTV2T05RNHhnMEFKbzVoaWhtQjIxaHc3Zk54MUFicjFtTWx1R0M4cVJoVDIzUkZlQUlaMVk3UUd0UnBLQlFLOV9iV0hyWnhPOWlucURRVjh4ZC0wd2tNPTIyQTdDMjg1', 495 'Cookie': 'atlassian.xsrf.token=9dd7b0ae95b82b138b9fd93e27a45a6fd01c548e_lin' 496 } 497 """ 498 try: 499 # Use provided values or fall back to environment variables 500 api_key = api_key or os.getenv('JIRA_API_KEY') 501 user = user or os.getenv('JIRA_USER', 'yakub@arusatech.com') 502 503 if not api_key: 504 raise ValueError("API key is required") 505 if not user: 506 raise ValueError("User email is required") 507 508 # Create the credentials string in format "user:api_key" 509 credentials = f"{user}:{api_key}" 510 511 # Encode to base64 512 encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') 513 514 # Build headers 515 headers = { 516 'Authorization': f'Basic {encoded_credentials}' 517 } 518 519 # Add cookie if provided 520 if cookie: 521 headers['Cookie'] = cookie 522 523 return headers 524 525 except Exception as e: 526 logger.error(f"Failed to build auth headers: {str(e)}") 527 logger.traceback(e) 528 raise 529 530 def make_jira_request(self, jira_key: str, url: str, method: str = "GET", payload: Dict = None, 531 api_key: str = None, user: str = None, cookie: str = None) -> Optional[Dict]: 532 """ 533 Make a JIRA API request with proper authentication headers. 534 535 This method builds the authentication headers (similar to Postman) and 536 makes the request to the JIRA API. 537 538 Parameters 539 ---------- 540 jira_key : str 541 The JIRA issue key 542 method : str, optional 543 HTTP method (GET, POST, PUT, DELETE). Defaults to "GET" 544 payload : Dict, optional 545 Request payload for POST/PUT requests 546 api_key : str, optional 547 The JIRA API key. If not provided, uses environment variable 548 user : str, optional 549 The JIRA user email. If not provided, uses environment variable 550 cookie : str, optional 551 Additional cookie value 552 553 Returns 554 ------- 555 Optional[Dict] 556 The JSON response from the API, or None if the request fails 557 558 Examples 559 -------- 560 >>> handler = JiraHandler() 561 >>> response = handler.make_jira_request( 562 ... url="https://arusa.atlassian.net/rest/api/2/issue/XSP1-543213456" 563 ... ) 564 >>> print(response) 565 {'id': '12345', 'key': 'XSP1-3456', ...} 566 """ 567 try: 568 if not jira_key: 569 logger.error("JIRA issue key is required") 570 return None 571 572 url = f"{os.getenv('JIRA_SERVER')}/rest/api/2/issue/{jira_key}" 573 if not url: 574 logger.error("JIRA API endpoint URL is required") 575 return None 576 # Build authentication headers 577 headers = self._build_auth_headers(api_key, user, cookie) 578 579 # Make the request 580 response = requests.request(method, url, headers=headers, data=payload) 581 response.raise_for_status() 582 583 # Return JSON response 584 return response.json() 585 586 except requests.exceptions.RequestException as e: 587 logger.error(f"JIRA API request failed: {str(e)}") 588 logger.traceback(e) 589 return None 590 except Exception as e: 591 logger.error(f"Unexpected error in JIRA request: {str(e)}") 592 logger.traceback(e) 593 return None 594 595 def download_jira_attachment_by_id(self, attachment_id: str, mime_type: str) -> Optional[Dict]: 596 ''' 597 Download a JIRA attachment by its ID. 598 ''' 599 try: 600 # ATTACHMENT_URL = f"{os.getenv('JIRA_SERVER')}/rest/api/2/attachment/{attachment_id}" 601 CONTENT_URL = f"{os.getenv('JIRA_SERVER')}/rest/api/2/attachment/content/{attachment_id}" 602 if not CONTENT_URL: 603 logger.error(f"No content URL found for attachment '{attachment_id}'") 604 return None 605 headers = self._build_auth_headers() 606 download_response = requests.get(CONTENT_URL, headers=headers) 607 download_response.raise_for_status() 608 content = download_response.content 609 #Process content based on type 610 result = { 611 'content': content, 612 'mime_type': mime_type, 613 'text_content': None, 614 'json_content': None 615 } 616 617 # Handle text-based files 618 if mime_type.startswith(('text/', 'application/json', 'application/xml' , 'json')): 619 try: 620 text_content = content.decode('utf-8') 621 result['text_content'] = text_content 622 623 # Try to parse as JSON 624 if mime_type == 'application/json': 625 try: 626 result['json_content'] = json.loads(text_content) 627 except json.JSONDecodeError: 628 pass 629 except UnicodeDecodeError: 630 logger.error(f"Warning: Could not decode text content for {attachment_id}") 631 logger.traceback(e) 632 633 return result 634 except Exception as e: 635 logger.error(f"Error downloading JIRA attachment: {str(e)}") 636 logger.traceback(e) 637 return None 638 639 640class XrayGraphQL(JiraHandler): 641 """A comprehensive client for interacting with Xray Cloud's GraphQL API. 642 This class extends JiraHandler to provide specialized methods for interacting with 643 Xray Cloud's GraphQL API for test management. It handles authentication, test plans, 644 test executions, test runs, defects, evidence, and other Xray-related operations 645 through GraphQL queries and mutations. 646 Inherits 647 -------- 648 JiraHandler 649 Base class providing JIRA client functionality and issue management 650 Attributes 651 ---------- 652 client_id : str 653 The client ID for Xray authentication 654 client_secret : str 655 The client secret for Xray authentication 656 xray_base_url : str 657 Base URL for Xray Cloud API (defaults to 'https://us.xray.cloud.getxray.app') 658 logger : Logger 659 Logger instance for debugging and error tracking 660 token : str 661 Authentication token obtained from Xray 662 Methods 663 ------- 664 Authentication & Setup 665 --------------------- 666 __init__() 667 Initialize XrayGraphQL client with authentication and configuration settings. 668 _get_auth_token() -> Optional[str] 669 Authenticate with Xray Cloud API and obtain an authentication token. 670 _make_graphql_request(query: str, variables: Dict) -> Optional[Dict] 671 Makes a GraphQL request to the Xray API with proper authentication. 672 _parse_table(table_str: str) -> Dict[str, Union[List[int], List[float]]] 673 Parse a string representation of a table into a dictionary of numeric values. 674 Issue ID Management 675 ------------------ 676 get_issue_id_from_jira_id(issue_key: str, issue_type: str) -> Optional[str] 677 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 678 Test Plan Operations 679 ------------------- 680 get_tests_from_test_plan(test_plan: str) -> Optional[Dict[str, str]] 681 Retrieves all tests associated with a given test plan. 682 get_test_plan_data(test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]] 683 Retrieves and parses tabular data from a test plan's description. 684 Test Set Operations 685 ------------------ 686 get_tests_from_test_set(test_set: str) -> Optional[Dict[str, str]] 687 Retrieves all tests associated with a given test set. 688 filter_test_set_by_test_case(test_key: str) -> Optional[Dict[str, str]] 689 Retrieves all test sets containing a specific test case. 690 filter_tags_by_test_case(test_key: str) -> Optional[List[str]] 691 Extracts and filters tags from test sets associated with a test case. 692 Test Execution Operations 693 ------------------------ 694 get_tests_from_test_execution(test_execution: str) -> Optional[Dict[str, str]] 695 Retrieves all tests associated with a given test execution. 696 get_test_execution(test_execution: str) -> Optional[Dict] 697 Retrieve detailed information about a test execution from Xray. 698 create_test_execution(test_issue_keys: List[str], project_key: Optional[str], 699 summary: Optional[str], description: Optional[str]) -> Optional[Dict] 700 Creates a new test execution with specified test cases. 701 create_test_execution_from_test_plan(test_plan: str) -> Optional[Dict[str, Dict[str, str]]] 702 Creates a test execution from a given test plan with all associated tests. 703 add_test_execution_to_test_plan(test_plan: str, test_execution: str) -> Optional[Dict] 704 Add a test execution to an existing test plan in Xray. 705 Test Run Operations 706 ------------------ 707 get_test_runstatus(test_case: str, test_execution: str) -> Optional[Tuple[str, str]] 708 Retrieves the status of a test run for a specific test case. 709 get_test_run_by_id(test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]] 710 Retrieves the test run ID and status using internal Xray IDs. 711 update_test_run_status(test_run_id: str, test_run_status: str) -> bool 712 Updates the status of a specific test run. 713 update_test_run_comment(test_run_id: str, test_run_comment: str) -> bool 714 Updates the comment of a specific test run. 715 get_test_run_comment(test_run_id: str) -> Optional[str] 716 Retrieve the comment of a specific test run from Xray. 717 append_test_run_comment(test_run_id: str, test_run_comment: str) -> bool 718 Append a comment to an existing test run comment. 719 Evidence & Defect Management 720 --------------------------- 721 add_evidence_to_test_run(test_run_id: str, evidence_path: str) -> bool 722 Add evidence (attachments) to a test run in Xray. 723 create_defect_from_test_run(test_run_id: str, project_key: str, parent_issue_key: str, 724 defect_summary: str, defect_description: str) -> Optional[Dict] 725 Create a defect from a test run and link it to the test run in Xray. 726 Examples 727 -------- 728 >>> client = XrayGraphQL() 729 >>> test_plan_tests = client.get_tests_from_test_plan("TEST-123") 730 >>> print(test_plan_tests) 731 {'TEST-124': '10001', 'TEST-125': '10002'} 732 >>> test_execution = client.create_test_execution_from_test_plan("TEST-123") 733 >>> print(test_execution) 734 { 735 'TEST-124': { 736 'test_run_id': '5f7c3', 737 'test_execution_key': 'TEST-456', 738 'test_plan_key': 'TEST-123' 739 } 740 } 741 >>> # Update test run status 742 >>> success = client.update_test_run_status("test_run_id", "PASS") 743 >>> print(success) 744 True 745 >>> # Add evidence to test run 746 >>> evidence_added = client.add_evidence_to_test_run("test_run_id", "/path/to/screenshot.png") 747 >>> print(evidence_added) 748 True 749 Notes 750 ----- 751 - Requires valid Xray Cloud credentials (client_id and client_secret) 752 - Uses GraphQL for all API interactions 753 - Implements automatic token refresh 754 - Handles rate limiting and retries 755 - All methods include comprehensive error handling and logging 756 - Returns None for failed operations instead of raising exceptions 757 - Supports various file types for evidence attachments 758 - Integrates with JIRA for defect creation and issue management 759 - Provides both synchronous and asynchronous operation patterns 760 - Includes retry logic for transient failures 761 """ 762 763 def __init__(self): 764 """Initialize XrayGraphQL client with authentication and configuration settings. 765 This constructor sets up the XrayGraphQL client by: 766 1. Loading environment variables from .env file 767 2. Reading required environment variables for authentication 768 3. Configuring the base URL for Xray Cloud 769 4. Obtaining an authentication token 770 Required Environment Variables 771 ---------------------------- 772 XRAY_CLIENT_ID : str 773 Client ID for Xray authentication 774 XRAY_CLIENT_SECRET : str 775 Client secret for Xray authentication 776 XRAY_BASE_URL : str, optional 777 Base URL for Xray Cloud API. Defaults to 'https://us.xray.cloud.getxray.app' 778 Attributes 779 ---------- 780 client_id : str 781 Xray client ID from environment 782 client_secret : str 783 Xray client secret from environment 784 xray_base_url : str 785 Base URL for Xray Cloud API 786 logger : Logger 787 Logger instance for debugging and error tracking 788 token : str 789 Authentication token obtained from Xray 790 Raises 791 ------ 792 Exception 793 If authentication fails or required environment variables are missing 794 """ 795 super().__init__() 796 try: 797 # Load environment variables from .env file 798 self.client_id = os.getenv('XRAY_CLIENT_ID') 799 self.client_secret = os.getenv('XRAY_CLIENT_SECRET') 800 self.xray_base_url = os.getenv('XRAY_BASE_URL', 'https://us.xray.cloud.getxray.app') 801 self.logger = logger 802 # Validate required environment variables 803 if not self.client_id or self.client_id == '<CLIENT_ID>': 804 raise ValueError("XRAY_CLIENT_ID environment variable is required") 805 if not self.client_secret or self.client_secret == '<CLIENT_SECRET>': 806 raise ValueError("XRAY_CLIENT_SECRET environment variable is required") 807 # Get authentication token 808 self.token = self._get_auth_token() 809 if not self.token: 810 logger.error("Failed to authenticate with Xray GraphQL") 811 raise Exception("Failed to initialize XrayGraphQL: No authentication token") 812 except Exception as e: 813 logger.error(f"Error initializing XrayGraphQL: {e}") 814 logger.traceback(e) 815 raise e 816 817 def _get_auth_token(self) -> Optional[str]: 818 """Authenticate with Xray Cloud API and obtain an authentication token. 819 Makes a POST request to the Xray authentication endpoint using the client credentials 820 to obtain a JWT token for subsequent API calls. 821 Returns 822 ------- 823 Optional[str] 824 The authentication token if successful, None if authentication fails. 825 The token is stripped of surrounding quotes before being returned. 826 Raises 827 ------ 828 requests.exceptions.RequestException 829 If the HTTP request fails 830 requests.exceptions.HTTPError 831 If the server returns an error status code 832 Notes 833 ----- 834 - The token is obtained from the Xray Cloud API endpoint /api/v2/authenticate 835 - The method uses client credentials stored in self.client_id and self.client_secret 836 - Failed authentication attempts are logged as errors 837 - Successful authentication is logged at debug level 838 """ 839 try: 840 auth_url = f"{self.xray_base_url}/api/v2/authenticate" 841 auth_headers = {"Content-Type": "application/json"} 842 auth_payload = { 843 "client_id": self.client_id, 844 "client_secret": self.client_secret 845 } 846 # logger.debug("Attempting Xray authentication", "auth_start") 847 logger.debug("Attempting Xray authentication... auth start") 848 response = requests.post(auth_url, headers=auth_headers, json=auth_payload) 849 response.raise_for_status() 850 token = response.text.strip('"') 851 # logger.info("Successfully authenticated with Xray", "auth_success") 852 logger.debug("Successfully authenticated with Xray... auth success again") 853 return token 854 except Exception as e: 855 # logger.error("Xray authentication failed", "auth_failed", error=e) 856 logger.error("Xray authentication failed. auth failed") 857 logger.traceback(e) 858 return None 859 860 def _parse_table(self, table_str: str) -> Dict[str, Union[List[int], List[float]]]: 861 """Parse a string representation of a table into a dictionary of numeric values. 862 Parameters 863 ---------- 864 table_str : str 865 A string containing the table data in markdown-like format. 866 Example format:: 867 header1 || header2 || header3 868 |row1 |value1 |[1, 2, 3]| 869 |row2 |value2 |42 | 870 Returns 871 ------- 872 Dict[str, Union[List[int], List[float]]] 873 A dictionary where: 874 * Keys are strings derived from the first column (lowercase, underscores) 875 * Values are either: 876 * Lists of integers/floats (for array-like values in brackets) 877 * Lists of individual numbers (for single numeric values) 878 For non-array columns, duplicate values are removed and sorted. 879 Returns None if parsing fails. 880 Examples 881 -------- 882 >>> table_str = '''header1 || header2 883 ... |temp |[1, 2, 3]| 884 ... |value |42 |''' 885 >>> result = client._parse_table(table_str) 886 >>> print(result) 887 { 888 'temp': [1, 2, 3], 889 'value': [42] 890 } 891 """ 892 try: 893 # Split the table into lines 894 lines = table_str.strip().split('\n') 895 # Process each data row 896 result = {} 897 for line in lines[1:]: 898 if not line.startswith('|'): 899 continue 900 # Split the row into columns 901 columns = [col.strip() for col in line.split('|')[1:-1]] 902 if not columns: 903 continue 904 key = columns[0].replace(' ', '_').lower() 905 values = [] 906 # Process each value column 907 for col in columns[1:]: 908 # Handle list values 909 if col.startswith('[') and col.endswith(']'): 910 try: 911 # Clean and parse the list 912 list_str = col[1:-1].replace(',', ' ') 913 list_items = [item.strip() for item in list_str.split() if item.strip()] 914 num_list = [float(item) if '.' in item else int(item) for item in list_items] 915 values.append(num_list) 916 except (ValueError, SyntaxError): 917 pass 918 elif col.strip(): # Handle simple numeric values 919 try: 920 num = float(col) if '.' in col else int(col) 921 values.append(num) 922 except ValueError: 923 pass 924 # Store in result 925 if key: 926 if key in result: 927 result[key].extend(values) 928 else: 929 result[key] = values 930 # For temperature (simple values), remove duplicates and sort 931 for key in result: 932 if all(not isinstance(v, list) for v in result[key]): 933 result[key] = sorted(list(set(result[key]))) 934 return result 935 except Exception as e: 936 logger.error(f"Error parsing table: {e}") 937 logger.traceback(e) 938 return None 939 940 def _make_graphql_request(self, query: str, variables: Dict) -> Optional[Dict]: 941 """ 942 Makes a GraphQL request to the Xray API with the provided query and variables. 943 This internal method handles the execution of GraphQL queries against the Xray API, 944 including proper authentication and error handling. 945 Args: 946 query (str): The GraphQL query or mutation to execute 947 variables (Dict): Variables to be used in the GraphQL query/mutation 948 Returns: 949 Optional[Dict]: The 'data' field from the GraphQL response if successful, 950 None if the request fails or contains GraphQL errors 951 Raises: 952 No exceptions are raised - all errors are caught, logged, and return None 953 Example: 954 query = ''' 955 query GetTestPlan($id: String!) { 956 getTestPlan(issueId: $id) { 957 issueId 958 } 959 } 960 ''' 961 variables = {"id": "12345"} 962 result = self._make_graphql_request(query, variables) 963 Note: 964 - Automatically includes authentication token in request headers 965 - Logs errors if the request fails or if GraphQL errors are present 966 - Returns None instead of raising exceptions to allow for graceful error handling 967 - Only returns the 'data' portion of the GraphQL response 968 """ 969 try: 970 graphql_url = f"{self.xray_base_url}/api/v2/graphql" 971 headers = { 972 "Authorization": f"Bearer {self.token}", 973 "Content-Type": "application/json" 974 } 975 payload = {"query": query, "variables": variables} 976 # logger.debug(f'Making GraphQL request "query": {query}, "variables": {variables} ') 977 response = requests.post(graphql_url, headers=headers, json=payload) 978 jprint(response.json()) 979 980 response.raise_for_status() 981 try: 982 data = response.json() 983 jprint(data) 984 except: 985 data = response.text 986 logger.debug(f"Response text: {data}") 987 if 'errors' in data: 988 logger.error(f'GraphQL request failed: {data["errors"]}') 989 return None 990 return data['data'] 991 except Exception as e: 992 logger.error(f"GraphQL request failed due to {e} ") 993 logger.traceback(e) 994 return None 995 996 def get_issue_id_from_jira_id(self, issue_key: str, issue_type: str) -> Optional[str]: 997 """ 998 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 999 This method queries the Xray GraphQL API to find the internal issue ID corresponding 1000 to a JIRA issue key. It supports different types of Xray artifacts including test plans, 1001 test executions, test sets, and tests. 1002 Args: 1003 issue_key (str): The JIRA issue key (e.g., "PROJECT-123") 1004 issue_type (str): The type of Xray artifact. Supported values are: 1005 - "plan" or contains "plan": For Test Plans 1006 - "exec" or contains "exec": For Test Executions 1007 - "set" or contains "set": For Test Sets 1008 - "test" or contains "test": For Tests 1009 If not provided, defaults to "plan" 1010 Returns: 1011 Optional[str]: The internal Xray issue ID if found, None if: 1012 - The issue key doesn't exist 1013 - The GraphQL request fails 1014 - Any other error occurs during processing 1015 Examples: 1016 >>> client.get_issue_id_from_jira_id("TEST-123", "plan") 1017 '10000' 1018 >>> client.get_issue_id_from_jira_id("TEST-456", "test") 1019 '10001' 1020 >>> client.get_issue_id_from_jira_id("INVALID-789", "plan") 1021 None 1022 Note: 1023 The method performs a case-insensitive comparison when matching issue keys. 1024 The project key is extracted from the issue_key (text before the hyphen) 1025 to filter results by project. 1026 """ 1027 try: 1028 parse_project = issue_key.split("-")[0] 1029 function_name = "getTestPlans" 1030 if not issue_type: 1031 issue_type = "plan" 1032 if "plan" in issue_type.lower(): 1033 function_name = "getTestPlans" 1034 query = """ 1035 query GetIds($limit: Int!, $jql: String!) { 1036 getTestPlans(limit: $limit, jql:$jql) { 1037 results { 1038 issueId 1039 jira(fields: ["key"]) 1040 } 1041 } 1042 } 1043 """ 1044 if "exec" in issue_type.lower(): 1045 function_name = "getTestExecutions" 1046 query = """ 1047 query GetIds($limit: Int!, $jql: String!) { 1048 getTestExecutions(limit: $limit, jql:$jql) { 1049 results { 1050 issueId 1051 jira(fields: ["key"]) 1052 } 1053 } 1054 } 1055 """ 1056 if "set" in issue_type.lower(): 1057 function_name = "getTestSets" 1058 query = """ 1059 query GetIds($limit: Int!, $jql: String!) { 1060 getTestSets(limit: $limit, jql:$jql) { 1061 results { 1062 issueId 1063 jira(fields: ["key"]) 1064 } 1065 } 1066 } 1067 """ 1068 if "test" in issue_type.lower(): 1069 function_name = "getTests" 1070 query = """ 1071 query GetIds($limit: Int!, $jql: String!) { 1072 getTests(limit: $limit, jql:$jql) { 1073 results { 1074 issueId 1075 jira(fields: ["key"]) 1076 } 1077 } 1078 } 1079 """ 1080 variables = { 1081 "limit": 10, 1082 "jql": f"project = '{parse_project}' AND key = '{issue_key}'" 1083 } 1084 data = self._make_graphql_request(query, variables) 1085 if not data: 1086 logger.error(f"Failed to get issue ID for {issue_key}") 1087 return None 1088 for issue in data[function_name]['results']: 1089 if str(issue['jira']['key']).lower() == issue_key.lower(): 1090 return issue['issueId'] 1091 return None 1092 except Exception as e: 1093 logger.error(f"Failed to get issue ID for {issue_key}") 1094 logger.traceback(e) 1095 return None 1096 1097 def get_test_details(self, issue_key: str, issue_type: str) -> Optional[str]: 1098 """ 1099 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 1100 This method queries the Xray GraphQL API to find the internal issue ID corresponding 1101 to a JIRA issue key. It supports different types of Xray artifacts including test plans, 1102 test executions, test sets, and tests. 1103 Args: 1104 issue_key (str): The JIRA issue key (e.g., "PROJECT-123") 1105 issue_type (str): The type of Xray artifact. Supported values are: 1106 - "plan" or contains "plan": For Test Plans 1107 - "exec" or contains "exec": For Test Executions 1108 - "set" or contains "set": For Test Sets 1109 - "test" or contains "test": For Tests 1110 If not provided, defaults to "plan" 1111 Returns: 1112 Optional[str]: The internal Xray issue ID if found, None if: 1113 - The issue key doesn't exist 1114 - The GraphQL request fails 1115 - Any other error occurs during processing 1116 Examples: 1117 >>> client.get_issue_id_from_jira_id("TEST-123", "plan") 1118 '10000' 1119 >>> client.get_issue_id_from_jira_id("TEST-456", "test") 1120 '10001' 1121 >>> client.get_issue_id_from_jira_id("INVALID-789", "plan") 1122 None 1123 Note: 1124 The method performs a case-insensitive comparison when matching issue keys. 1125 The project key is extracted from the issue_key (text before the hyphen) 1126 to filter results by project. 1127 """ 1128 try: 1129 parse_project = issue_key.split("-")[0] 1130 function_name = "getTestPlans" 1131 if not issue_type: 1132 issue_type = "plan" 1133 if "plan" in issue_type.lower(): 1134 function_name = "getTestPlans" 1135 jira_fields = [ 1136 "key", "summary", "description", "assignee", 1137 "status", "priority", "labels", "created", 1138 "updated", "dueDate", "components", "versions", 1139 "attachments", "comments" 1140 ] 1141 query = """ 1142 query GetDetails($limit: Int!, $jql: String!) { 1143 getTestPlans(limit: $limit, jql:$jql) { 1144 results { 1145 issueId 1146 jira(fields: ["key"]) 1147 } 1148 } 1149 } 1150 """ 1151 if "exec" in issue_type.lower(): 1152 function_name = "getTestExecutions" 1153 jira_fields = [ 1154 "key", "summary", "description", "assignee", 1155 "status", "priority", "labels", "created", 1156 "updated", "dueDate", "components", "versions", 1157 "attachments", "comments" 1158 ] 1159 query = """ 1160 query GetDetails($limit: Int!, $jql: String!) { 1161 getTestExecutions(limit: $limit, jql:$jql) { 1162 results { 1163 issueId 1164 jira(fields: ["key"]) 1165 } 1166 } 1167 } 1168 """ 1169 if "set" in issue_type.lower(): 1170 function_name = "getTestSets" 1171 jira_fields = [ 1172 "key", "summary", "description", "assignee", 1173 "status", "priority", "labels", "created", 1174 "updated", "dueDate", "components", "versions", 1175 "attachments", "comments" 1176 ] 1177 query = """ 1178 query GetDetails($limit: Int!, $jql: String!) { 1179 getTestSets(limit: $limit, jql:$jql) { 1180 results { 1181 issueId 1182 jira(fields: ["key"]) 1183 } 1184 } 1185 } 1186 """ 1187 if "test" in issue_type.lower(): 1188 function_name = "getTests" 1189 jira_fields = [ 1190 "key", "summary", "description", "assignee", 1191 "status", "priority", "labels", "created", 1192 "updated", "dueDate", "components", "versions", 1193 "attachments", "comments" 1194 ] 1195 query = """ 1196 query GetDetails($limit: Int!, $jql: String!, $jiraFields: [String!]!) { 1197 getTests(limit: $limit, jql:$jql) { 1198 results { 1199 issueId 1200 jira(fields: $jiraFields) 1201 steps { 1202 id 1203 action 1204 result 1205 attachments { 1206 id 1207 filename 1208 storedInJira 1209 downloadLink 1210 } 1211 } 1212 1213 } 1214 } 1215 } 1216 """ 1217 variables = { 1218 "limit": 10, 1219 "jql": f"project = '{parse_project}' AND key = '{issue_key}'", 1220 "jiraFields": jira_fields 1221 } 1222 data = self._make_graphql_request(query, variables) 1223 if not data: 1224 logger.error(f"Failed to get issue ID for {issue_key}") 1225 return None 1226 for issue in data[function_name]['results']: 1227 if str(issue['jira']['key']).lower() == issue_key.lower(): 1228 return issue # This now includes all metadata 1229 return None 1230 except Exception as e: 1231 logger.error(f"Failed to get issue ID for {issue_key}") 1232 logger.traceback(e) 1233 return None 1234 1235 def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]: 1236 """ 1237 Retrieves all tests associated with a given test plan from Xray. 1238 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1239 test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that 1240 ID to fetch the associated tests. 1241 Args: 1242 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1243 Returns: 1244 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1245 or None if the operation fails. For example: 1246 { 1247 "PROJECT-124": "10001", 1248 "PROJECT-125": "10002" 1249 } 1250 Returns None in the following cases: 1251 - Test plan ID cannot be found 1252 - GraphQL request fails 1253 - Any other error occurs during processing 1254 Example: 1255 >>> client = XrayGraphQL() 1256 >>> tests = client.get_tests_from_test_plan("PROJECT-123") 1257 >>> print(tests) 1258 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1259 Note: 1260 - The method is limited to retrieving 99999 tests per test plan 1261 - Test plan must exist in Xray and be accessible with current authentication 1262 """ 1263 try: 1264 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1265 if not test_plan_id: 1266 logger.error(f"Failed to get test plan ID for {test_plan}") 1267 return None 1268 query = """ 1269 query GetTestPlanTests($testPlanId: String!) { 1270 getTestPlan(issueId: $testPlanId) { 1271 tests(limit: 99999) { 1272 results { 1273 issueId 1274 jira(fields: ["key"]) 1275 } 1276 } 1277 } 1278 } 1279 """ 1280 variables = {"testPlanId": test_plan_id} 1281 data = self._make_graphql_request(query, variables) 1282 if not data: 1283 logger.error(f"Failed to get tests for plan {test_plan_id}") 1284 return None 1285 tests = {} 1286 for test in data['getTestPlan']['tests']['results']: 1287 tests[test['jira']['key']] = test['issueId'] 1288 return tests 1289 except Exception as e: 1290 logger.error(f"Failed to get tests for plan {test_plan_id}") 1291 logger.traceback(e) 1292 return None 1293 1294 def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]: 1295 """ 1296 Retrieves all tests associated with a given test set from Xray. 1297 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1298 test set. It first converts the JIRA test set key to an internal Xray ID, then uses that 1299 ID to fetch the associated tests. 1300 Args: 1301 test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") 1302 Returns: 1303 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1304 or None if the operation fails. For example: 1305 { 1306 "PROJECT-124": "10001", 1307 "PROJECT-125": "10002" 1308 } 1309 Returns None in the following cases: 1310 - Test set ID cannot be found 1311 - GraphQL request fails 1312 - Any other error occurs during processing 1313 Example: 1314 >>> client = XrayGraphQL() 1315 >>> tests = client.get_tests_from_test_set("PROJECT-123") 1316 >>> print(tests) 1317 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1318 Note: 1319 - The method is limited to retrieving 99999 tests per test set 1320 - Test set must exist in Xray and be accessible with current authentication 1321 """ 1322 try: 1323 test_set_id = self.get_issue_id_from_jira_id(test_set, "set") 1324 if not test_set_id: 1325 logger.error(f"Failed to get test set ID for {test_set}") 1326 return None 1327 query = """ 1328 query GetTestSetTests($testSetId: String!) { 1329 getTestSet(issueId: $testSetId) { 1330 tests(limit: 99999) { 1331 results { 1332 issueId 1333 jira(fields: ["key"]) 1334 } 1335 } 1336 } 1337 } 1338 """ 1339 variables = {"testSetId": test_set_id} 1340 data = self._make_graphql_request(query, variables) 1341 if not data: 1342 logger.error(f"Failed to get tests for set {test_set_id}") 1343 return None 1344 tests = {} 1345 for test in data['getTestSet']['tests']['results']: 1346 tests[test['jira']['key']] = test['issueId'] 1347 return tests 1348 except Exception as e: 1349 logger.error(f"Failed to get tests for set {test_set_id}") 1350 logger.traceback(e) 1351 return None 1352 1353 def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]: 1354 """ 1355 Retrieves all tests associated with a given test execution from Xray. 1356 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1357 test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that 1358 ID to fetch the associated tests. 1359 Args: 1360 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1361 Returns: 1362 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1363 or None if the operation fails. For example: 1364 { 1365 "PROJECT-124": "10001", 1366 "PROJECT-125": "10002" 1367 } 1368 Returns None in the following cases: 1369 - Test execution ID cannot be found 1370 - GraphQL request fails 1371 - Any other error occurs during processing 1372 Example: 1373 >>> client = XrayGraphQL() 1374 >>> tests = client.get_tests_from_test_execution("PROJECT-123") 1375 >>> print(tests) 1376 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1377 Note: 1378 - The method is limited to retrieving 99999 tests per test execution 1379 - Test execution must exist in Xray and be accessible with current authentication 1380 """ 1381 try: 1382 test_execution_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1383 if not test_execution_id: 1384 logger.error(f"Failed to get test execution ID for {test_execution}") 1385 return None 1386 query = """ 1387 query GetTestExecutionTests($testExecutionId: String!) { 1388 getTestExecution(issueId: $testExecutionId) { 1389 tests(limit: 100) { 1390 results { 1391 issueId 1392 jira(fields: ["key"]) 1393 } 1394 } 1395 } 1396 } 1397 """ 1398 variables = {"testExecutionId": test_execution_id} 1399 data = self._make_graphql_request(query, variables) 1400 if not data: 1401 logger.error(f"Failed to get tests for execution {test_execution_id}") 1402 return None 1403 tests = {} 1404 for test in data['getTestExecution']['tests']['results']: 1405 tests[test['jira']['key']] = test['issueId'] 1406 return tests 1407 except Exception as e: 1408 logger.error(f"Failed to get tests for execution {test_execution_id}") 1409 logger.traceback(e) 1410 return None 1411 1412 def get_test_plan_data(self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]: 1413 """ 1414 Retrieve and parse tabular data from a test plan's description field in Xray. 1415 This method fetches a test plan's description from Xray and parses any tables found within it. 1416 The tables in the description are expected to be in a specific format that can be parsed by 1417 the _parse_table method. The parsed data is returned as a dictionary containing numeric values 1418 and lists extracted from the table. 1419 Args: 1420 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1421 Returns: 1422 Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data 1423 where keys are derived from the first column of the table and values are lists of numeric 1424 values. Returns None if: 1425 - The test plan ID cannot be found 1426 - The GraphQL request fails 1427 - The description cannot be parsed 1428 - Any other error occurs during processing 1429 Example: 1430 >>> client = XrayGraphQL() 1431 >>> data = client.get_test_plan_data("TEST-123") 1432 >>> print(data) 1433 { 1434 'temperature': [20, 25, 30], 1435 'pressure': [1.0, 1.5, 2.0], 1436 'measurements': [[1, 2, 3], [4, 5, 6]] 1437 } 1438 Note: 1439 - The test plan must exist in Xray and be accessible with current authentication 1440 - The description must contain properly formatted tables for parsing 1441 - Table values are converted to numeric types (int or float) where possible 1442 - Lists in table cells should be formatted as [value1, value2, ...] 1443 - Failed operations are logged as errors with relevant details 1444 """ 1445 try: 1446 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1447 if not test_plan_id: 1448 logger.error(f"Failed to get test plan ID for {test_plan}") 1449 return None 1450 query = """ 1451 query GetTestPlanTests($testPlanId: String!) { 1452 getTestPlan(issueId: $testPlanId) { 1453 issueId 1454 jira(fields: ["key","description"]) 1455 } 1456 } 1457 """ 1458 variables = {"testPlanId": test_plan_id} 1459 data = self._make_graphql_request(query, variables) 1460 if not data: 1461 logger.error(f"Failed to get tests for plan {test_plan_id}") 1462 return None 1463 description = data['getTestPlan']['jira']['description'] 1464 test_plan_data = self._parse_table(description) 1465 return test_plan_data 1466 except Exception as e: 1467 logger.error(f"Failed to get tests for plan {test_plan_id}") 1468 logger.traceback(e) 1469 return None 1470 1471 def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]: 1472 """ 1473 Retrieves all test sets that contain a specific test case from Xray. 1474 This method queries the Xray GraphQL API to find all test sets that include the specified 1475 test case. It first converts the JIRA test case key to an internal Xray ID, then uses that 1476 ID to fetch all associated test sets. 1477 Args: 1478 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1479 Returns: 1480 Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, 1481 or None if the operation fails. For example: 1482 { 1483 "PROJECT-124": "Test Set for Feature A", 1484 "PROJECT-125": "Regression Test Set" 1485 } 1486 Returns None in the following cases: 1487 - Test case ID cannot be found 1488 - GraphQL request fails 1489 - Any other error occurs during processing 1490 Example: 1491 >>> client = XrayGraphQL() 1492 >>> test_sets = client.filter_test_set_by_test_case("PROJECT-123") 1493 >>> print(test_sets) 1494 {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} 1495 Note: 1496 - The method is limited to retrieving 99999 test sets per test case 1497 - Test case must exist in Xray and be accessible with current authentication 1498 """ 1499 try: 1500 test_id = self.get_issue_id_from_jira_id(test_key, "test") 1501 if not test_id: 1502 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1503 return None 1504 query = """ 1505 query GetTestDetails($testId: String!) { 1506 getTest(issueId: $testId) { 1507 testSets(limit: 100) { 1508 results { 1509 issueId 1510 jira(fields: ["key","summary"]) 1511 } 1512 } 1513 } 1514 } 1515 """ 1516 variables = { 1517 "testId": test_id 1518 } 1519 data = self._make_graphql_request(query, variables) 1520 if not data: 1521 logger.error(f"Failed to get tests for plan {test_id}") 1522 return None 1523 retDict = {} 1524 for test in data['getTest']['testSets']['results']: 1525 retDict[test['jira']['key']] = test['jira']['summary'] 1526 return retDict 1527 except Exception as e: 1528 logger.error(f"Error in getting test set by test id: {e}") 1529 logger.traceback(e) 1530 return None 1531 1532 def filter_tags_by_test_case(self, test_key: str) -> Optional[List[str]]: 1533 """ 1534 Extract and filter tags from test sets associated with a specific test case in Xray. 1535 This method queries the Xray GraphQL API to find all test sets associated with the given 1536 test case and extracts tags from their summaries. Tags are identified from test set summaries 1537 that start with either 'tag' or 'benchtype' prefixes. 1538 Args: 1539 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1540 Returns: 1541 Optional[List[str]]: A list of unique tags extracted from test set summaries, 1542 or None if no tags are found or an error occurs. Tags are: 1543 - Extracted from summaries starting with 'tag:' or 'benchtype:' 1544 - Split on commas, semicolons, double pipes, or whitespace 1545 - Converted to lowercase and stripped of whitespace 1546 Example: 1547 >>> client = XrayGraphQL() 1548 >>> tags = client.filter_tags_by_test_case("TEST-123") 1549 >>> print(tags) 1550 ['regression', 'smoke', 'performance'] 1551 Note: 1552 - Test sets must have summaries in the format "tag: tag1, tag2" or "benchtype: type1, type2" 1553 - Tags are extracted only from summaries with the correct prefix 1554 - All tags are converted to lowercase for consistency 1555 - Duplicate tags are automatically removed via set conversion 1556 - Returns None if no valid tags are found or if an error occurs 1557 """ 1558 try: 1559 test_id = self.get_issue_id_from_jira_id(test_key, "test") 1560 if not test_id: 1561 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1562 return None 1563 query = """ 1564 query GetTestDetails($testId: String!) { 1565 getTest(issueId: $testId) { 1566 testSets(limit: 100) { 1567 results { 1568 issueId 1569 jira(fields: ["key","summary"]) 1570 } 1571 } 1572 } 1573 } 1574 """ 1575 variables = { 1576 "testId": test_id 1577 } 1578 data = self._make_graphql_request(query, variables) 1579 if not data: 1580 logger.error(f"Failed to get tests for plan {test_id}") 1581 return None 1582 tags = set() 1583 for test in data['getTest']['testSets']['results']: 1584 summary = str(test['jira']['summary']).strip().lower() 1585 if summary.startswith(('tag', 'benchtype')): 1586 summary = summary.split(':', 1)[-1].strip() # Split only once and take last part 1587 tags.update(tag for tag in re.split(r'[,|;|\|\|]\s*|\s+', summary) if tag) 1588 if tags: 1589 return list(tags) 1590 else: 1591 return None 1592 except Exception as e: 1593 logger.error(f"Error in getting test set by test id: {e}") 1594 logger.traceback(e) 1595 return None 1596 1597 def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]: 1598 """ 1599 Retrieve the status of a test run for a specific test case within a test execution. 1600 This method queries the Xray GraphQL API to get the current status of a test run, 1601 which represents the execution status of a specific test case within a test execution. 1602 It first converts both the test case and test execution JIRA keys to their internal 1603 Xray IDs, then uses these to fetch the test run status. 1604 Args: 1605 test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1606 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") 1607 Returns: 1608 Tuple[Optional[str], Optional[str]]: A tuple containing: 1609 - test_run_id: The unique identifier of the test run (or None if not found) 1610 - test_run_status: The current status of the test run (or None if not found) 1611 Returns (None, None) if any error occurs during the process. 1612 Example: 1613 >>> client = XrayGraphQL() 1614 >>> run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") 1615 >>> print(f"Test Run ID: {run_id}, Status: {status}") 1616 Test Run ID: 10001, Status: PASS 1617 Note: 1618 - Both the test case and test execution must exist in Xray and be accessible 1619 - The test case must be associated with the test execution 1620 - The method performs two ID lookups before querying the test run status 1621 - Failed operations are logged as errors with relevant details 1622 """ 1623 try: 1624 test_case_id = self.get_issue_id_from_jira_id(test_case, "test") 1625 if not test_case_id: 1626 logger.error(f"Failed to get test ID for Test Case ({test_case})") 1627 return None 1628 test_exec_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1629 if not test_exec_id: 1630 logger.error(f"Failed to get test execution ID for Test Execution ({test_execution})") 1631 return None 1632 query = """ 1633 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1634 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1635 id 1636 status { 1637 name 1638 } 1639 } 1640 } 1641 """ 1642 variables = { 1643 "testId": test_case_id, 1644 "testExecutionId": test_exec_id, 1645 } 1646 # Add debug loggerging 1647 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_exec_id}") 1648 data = self._make_graphql_request(query, variables) 1649 if not data: 1650 logger.error(f"Failed to get test run status for test {test_case_id}") 1651 return None 1652 # jprint(data) 1653 test_run_id = data['getTestRun']['id'] 1654 test_run_status = data['getTestRun']['status']['name'] 1655 return (test_run_id, test_run_status) 1656 except Exception as e: 1657 logger.error(f"Error getting test run status: {str(e)}") 1658 logger.traceback(e) 1659 return (None, None) 1660 1661 def get_test_run_by_id(self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]: 1662 """ 1663 Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. 1664 Args: 1665 test_case_id (str): The ID of the test case to query 1666 test_execution_id (str): The ID of the test execution containing the test run 1667 Returns: 1668 tuple[Optional[str], Optional[str]]: A tuple containing: 1669 - test_run_id: The ID of the test run if found, None if not found or on error 1670 - test_run_status: The status name of the test run if found, None if not found or on error 1671 Note: 1672 The function makes a GraphQL request to fetch the test run information. If the request fails 1673 or encounters any errors, it will log the error and return (None, None). 1674 """ 1675 try: 1676 query = """ 1677 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1678 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1679 id 1680 status { 1681 name 1682 } 1683 } 1684 } 1685 """ 1686 variables = { 1687 "testId": test_case_id, 1688 "testExecutionId": test_execution_id, 1689 } 1690 # Add debug loggerging 1691 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_execution_id}") 1692 data = self._make_graphql_request(query, variables) 1693 if not data: 1694 logger.error(f"Failed to get test run status for test {test_case_id}") 1695 return None 1696 test_run_id = data['getTestRun']['id'] 1697 test_run_status = data['getTestRun']['status']['name'] 1698 return (test_run_id, test_run_status) 1699 except Exception as e: 1700 logger.error(f"Error getting test run status: {str(e)}") 1701 logger.traceback(e) 1702 return (None, None) 1703 1704 def get_test_execution(self, test_execution: str) -> Optional[Dict]: 1705 """ 1706 Retrieve detailed information about a test execution from Xray. 1707 This method queries the Xray GraphQL API to fetch information about a specific test execution, 1708 including its ID and associated tests. It first converts the JIRA test execution key to an 1709 internal Xray ID, then uses that ID to fetch the execution details. 1710 Args: 1711 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1712 Returns: 1713 Optional[Dict]: A dictionary containing test execution details if successful, None if failed. 1714 The dictionary has the following structure: 1715 { 1716 'id': str, # The internal Xray ID of the test execution 1717 'tests': { # Dictionary mapping test keys to their IDs 1718 'TEST-124': '10001', 1719 'TEST-125': '10002', 1720 ... 1721 } 1722 } 1723 Returns None in the following cases: 1724 - Test execution ID cannot be found 1725 - GraphQL request fails 1726 - No test execution found with the given ID 1727 - No tests found in the test execution 1728 - Any other error occurs during processing 1729 Example: 1730 >>> client = XrayGraphQL() 1731 >>> execution = client.get_test_execution("TEST-123") 1732 >>> print(execution) 1733 { 1734 'id': '10000', 1735 'tests': { 1736 'TEST-124': '10001', 1737 'TEST-125': '10002' 1738 } 1739 } 1740 Note: 1741 - The method is limited to retrieving 99999 tests per test execution 1742 - Test execution must exist in Xray and be accessible with current authentication 1743 - Failed operations are logged with appropriate error or warning messages 1744 """ 1745 try: 1746 test_execution_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1747 if not test_execution_id: 1748 logger.error(f"Failed to get test execution ID for {test_execution}") 1749 return None 1750 query = """ 1751 query GetTestExecution($testExecutionId: String!) { 1752 getTestExecution(issueId: $testExecutionId) { 1753 issueId 1754 projectId 1755 jira(fields: ["key", "summary", "description", "status"]) 1756 tests(limit: 100) { 1757 total 1758 start 1759 limit 1760 results { 1761 issueId 1762 jira(fields: ["key"]) 1763 } 1764 } 1765 } 1766 } 1767 """ 1768 variables = { 1769 "testExecutionId": test_execution_id 1770 } 1771 # Add debug loggerging 1772 logger.debug(f"Getting test execution details for {test_execution_id}") 1773 data = self._make_graphql_request(query, variables) 1774 # jprint(data) 1775 if not data: 1776 logger.error(f"Failed to get test execution details for {test_execution_id}") 1777 return None 1778 test_execution = data.get('getTestExecution',{}) 1779 if not test_execution: 1780 logger.warning(f"No test execution found with ID {test_execution_id}") 1781 return None 1782 tests = test_execution.get('tests',{}) 1783 if not tests: 1784 logger.warning(f"No tests found for test execution {test_execution_id}") 1785 return None 1786 tests_details = dict() 1787 for test in tests['results']: 1788 tests_details[test['jira']['key']] = test['issueId'] 1789 formatted_response = { 1790 'id': test_execution['issueId'], 1791 'tests': tests_details 1792 } 1793 return formatted_response 1794 except Exception as e: 1795 logger.error(f"Error getting test execution details: {str(e)}") 1796 logger.traceback(e) 1797 return None 1798 1799 def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]: 1800 """ 1801 Add a test execution to an existing test plan in Xray. 1802 This method associates a test execution with a test plan using the Xray GraphQL API. 1803 It first converts both the test plan and test execution JIRA keys to their internal 1804 Xray IDs, then creates the association between them. 1805 Args: 1806 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1807 test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") 1808 Returns: 1809 Optional[Dict]: A dictionary containing the response data if successful, None if failed. 1810 The dictionary has the following structure: 1811 { 1812 'addTestExecutionsToTestPlan': { 1813 'addedTestExecutions': [str], # List of added test execution IDs 1814 'warning': str # Any warnings from the operation 1815 } 1816 } 1817 Returns None in the following cases: 1818 - Test plan ID cannot be found 1819 - Test execution ID cannot be found 1820 - GraphQL request fails 1821 - Any other error occurs during processing 1822 Example: 1823 >>> client = XrayGraphQL() 1824 >>> result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") 1825 >>> print(result) 1826 { 1827 'addTestExecutionsToTestPlan': { 1828 'addedTestExecutions': ['10001'], 1829 'warning': None 1830 } 1831 } 1832 Note: 1833 - Both the test plan and test execution must exist in Xray and be accessible 1834 - The method performs two ID lookups before creating the association 1835 - Failed operations are logged as errors with relevant details 1836 """ 1837 try: 1838 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1839 if not test_plan_id: 1840 logger.error(f"Test plan ID is required") 1841 return None 1842 test_exec_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1843 if not test_exec_id: 1844 logger.error(f"Test execution ID is required") 1845 return None 1846 query = """ 1847 mutation AddTestExecutionToTestPlan($testPlanId: String!, $testExecutionIds: [String!]!) { 1848 addTestExecutionsToTestPlan(issueId: $testPlanId, testExecIssueIds: $testExecutionIds) { 1849 addedTestExecutions 1850 warning 1851 } 1852 } 1853 """ 1854 variables = { 1855 "testPlanId": test_plan_id, 1856 "testExecutionIds": [test_exec_id] 1857 } 1858 data = self._make_graphql_request(query, variables) 1859 return data 1860 except Exception as e: 1861 logger.error(f"Error adding test execution to test plan: {str(e)}") 1862 logger.traceback(e) 1863 return None 1864 1865 def create_test_execution(self, 1866 test_issue_keys: List[str], 1867 project_key: Optional[str] = None, 1868 summary: Optional[str] = None, 1869 description: Optional[str] = None) -> Optional[Dict]: 1870 """ 1871 Create a new test execution in Xray with specified test cases. 1872 This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. 1873 It handles validation of test issue keys, automatically derives project information if not provided, 1874 and creates appropriate default values for summary and description if not specified. 1875 Args: 1876 test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution 1877 (e.g., ["TEST-123", "TEST-124"]) 1878 project_key (Optional[str]): The JIRA project key where the test execution should be created. 1879 If not provided, it will be derived from the first test issue key. 1880 summary (Optional[str]): The summary/title for the test execution ticket. 1881 If not provided, a default summary will be generated using the test issue keys. 1882 description (Optional[str]): The description for the test execution ticket. 1883 If not provided, a default description will be generated using the test issue keys. 1884 Returns: 1885 Optional[Dict]: A dictionary containing the created test execution details if successful, 1886 None if the creation fails. The dictionary has the following structure: 1887 { 1888 'issueId': str, # The internal Xray ID of the created test execution 1889 'jira': { 1890 'key': str # The JIRA issue key of the created test execution 1891 } 1892 } 1893 Example: 1894 >>> client = XrayGraphQL() 1895 >>> test_execution = client.create_test_execution( 1896 ... test_issue_keys=["TEST-123", "TEST-124"], 1897 ... project_key="TEST", 1898 ... summary="Sprint 1 Regression Tests" 1899 ... ) 1900 >>> print(test_execution) 1901 {'issueId': '10001', 'jira': {'key': 'TEST-125'}} 1902 Note: 1903 - Invalid test issue keys are logged as warnings but don't prevent execution creation 1904 - At least one valid test issue key is required 1905 - The method validates each test issue key before creating the execution 1906 - Project key is automatically derived from the first test issue key if not provided 1907 """ 1908 try: 1909 invalid_keys = [] 1910 test_issue_ids = [] 1911 for key in test_issue_keys: 1912 test_issue_id = self.get_issue_id_from_jira_id(key, "test") 1913 if test_issue_id: 1914 test_issue_ids.append(test_issue_id) 1915 else: 1916 invalid_keys.append(key) 1917 if len(test_issue_ids) == 0: 1918 logger.error(f"No valid test issue keys provided: {invalid_keys}") 1919 return None 1920 if len(invalid_keys) > 0: 1921 logger.warning(f"Invalid test issue keys: {invalid_keys}") 1922 if not project_key: 1923 project_key = test_issue_keys[0].split("-")[0] 1924 if not summary: 1925 summary = f"Test Execution for Test Plan {test_issue_keys}" 1926 if not description: 1927 description = f"Test Execution for Test Plan {test_issue_keys}" 1928 mutation = """ 1929 mutation CreateTestExecutionForTestPlan( 1930 $testIssueId_list: [String!]!, 1931 $projectKey: String!, 1932 $summary: String!, 1933 $description: String 1934 ) { 1935 createTestExecution( 1936 testIssueIds: $testIssueId_list, 1937 jira: { 1938 fields: { 1939 project: { key: $projectKey }, 1940 summary: $summary, 1941 description: $description, 1942 issuetype: { name: "Test Execution" } 1943 } 1944 } 1945 ) { 1946 testExecution { 1947 issueId 1948 jira(fields: ["key"]) 1949 } 1950 warnings 1951 } 1952 } 1953 """ 1954 variables = { 1955 "testIssueId_list": test_issue_ids, 1956 "projectKey": project_key, 1957 "summary": summary, 1958 "description": description 1959 } 1960 data = self._make_graphql_request(mutation, variables) 1961 if not data: 1962 return None 1963 execution_details = data['createTestExecution']['testExecution'] 1964 # logger.info(f"Created test execution {execution_details['jira']['key']}") 1965 return execution_details 1966 except Exception as e: 1967 logger.error("Failed to create test execution : {e}") 1968 logger.traceback(e) 1969 return None 1970 1971 def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]: 1972 """Creates a test execution from a given test plan and associates all tests from the plan with the execution. 1973 This method performs several operations in sequence: 1974 1. Retrieves all tests from the specified test plan 1975 2. Creates a new test execution with those tests 1976 3. Associates the new test execution with the original test plan 1977 4. Creates test runs for each test in the execution 1978 Parameters 1979 ---------- 1980 test_plan : str 1981 The JIRA issue key of the test plan (e.g., "PROJECT-123") 1982 Returns 1983 ------- 1984 Optional[Dict[str, Dict[str, str]]] 1985 A dictionary mapping test case keys to their execution details, or None if the operation fails. 1986 The dictionary structure is:: 1987 { 1988 "TEST-123": { # Test case JIRA key 1989 "test_run_id": "12345", # Unique ID for this test run 1990 "test_execution_key": "TEST-456", # JIRA key of the created test execution 1991 "test_plan_key": "TEST-789" # Original test plan JIRA key 1992 }, 1993 "TEST-124": { 1994 ... 1995 } 1996 } 1997 Returns None in the following cases: 1998 * Test plan parameter is empty or invalid 1999 * No tests found in the test plan 2000 * Test execution creation fails 2001 * API request fails 2002 Examples 2003 -------- 2004 >>> client = XrayGraphQL() 2005 >>> result = client.create_test_execution_from_test_plan("TEST-123") 2006 >>> print(result) 2007 { 2008 "TEST-124": { 2009 "test_run_id": "5f7c3", 2010 "test_execution_key": "TEST-456", 2011 "test_plan_key": "TEST-123" 2012 }, 2013 "TEST-125": { 2014 "test_run_id": "5f7c4", 2015 "test_execution_key": "TEST-456", 2016 "test_plan_key": "TEST-123" 2017 } 2018 } 2019 Notes 2020 ----- 2021 - The test plan must exist and be accessible in Xray 2022 - All tests in the test plan must be valid and accessible 2023 - The method automatically generates a summary and description for the test execution 2024 - The created test execution is automatically linked back to the original test plan 2025 """ 2026 try: 2027 if not test_plan: 2028 logger.error("Test plan is required [ jira key]") 2029 return None 2030 project_key = test_plan.split("-")[0] 2031 summary = f"Test Execution for Test Plan {test_plan}" 2032 retDict = dict() 2033 #Get tests from test plan 2034 tests = self.get_tests_from_test_plan(test_plan) 2035 retDict["tests"] = tests 2036 testIssueId_list = list(tests.values()) 2037 # logger.info(f"Tests: {tests}") 2038 if not testIssueId_list: 2039 logger.error(f"No tests found for {test_plan}") 2040 return None 2041 description = f"Test Execution for {len(tests)} Test cases" 2042 # GraphQL mutation to create test execution 2043 query = """ 2044 mutation CreateTestExecutionForTestPlan( 2045 $testIssueId_list: [String!]!, 2046 $projectKey: String!, 2047 $summary: String!, 2048 $description: String 2049 ) { 2050 createTestExecution( 2051 testIssueIds: $testIssueId_list, 2052 jira: { 2053 fields: { 2054 project: { key: $projectKey }, 2055 summary: $summary, 2056 description: $description, 2057 issuetype: { name: "Test Execution" } 2058 } 2059 } 2060 ) { 2061 testExecution { 2062 issueId 2063 jira(fields: ["key"]) 2064 testRuns(limit: 100) { 2065 results { 2066 id 2067 test { 2068 issueId 2069 jira(fields: ["key"]) 2070 } 2071 } 2072 } 2073 } 2074 warnings 2075 } 2076 } 2077 """ 2078 variables = { 2079 "testIssueId_list": testIssueId_list, 2080 "projectKey": project_key, 2081 "summary": summary, 2082 "description": description or f"Test execution for total of {len(testIssueId_list)} test cases" 2083 } 2084 data = self._make_graphql_request(query, variables) 2085 if not data: 2086 return None 2087 test_exec_key = data['createTestExecution']['testExecution']['jira']['key'] 2088 test_exec_id = data['createTestExecution']['testExecution']['issueId'] 2089 #Add Test execution to test plan 2090 test_exec_dict= self.add_test_execution_to_test_plan(test_plan, test_exec_key) 2091 #Get test runs for test execution 2092 test_runs = data['createTestExecution']['testExecution']['testRuns']['results'] 2093 test_run_dict = dict() 2094 for test_run in test_runs: 2095 test_run_dict[test_run['test']['jira']['key']] = dict() 2096 test_run_dict[test_run['test']['jira']['key']]['test_run_id'] = test_run['id'] 2097 # test_run_dict[test_run['test']['jira']['key']]['test_issue_id'] = test_run['test']['issueId'] 2098 test_run_dict[test_run['test']['jira']['key']]['test_execution_key'] = test_exec_key 2099 # test_run_dict[test_run['test']['jira']['key']]['test_execution_id'] = test_exec_id 2100 test_run_dict[test_run['test']['jira']['key']]['test_plan_key'] = test_plan 2101 return test_run_dict 2102 except requests.exceptions.RequestException as e: 2103 logger.error(f"Error creating test execution: {e}") 2104 logger.traceback(e) 2105 return None 2106 2107 def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool: 2108 """ 2109 Update the status of a specific test run in Xray using the GraphQL API. 2110 This method allows updating the execution status of a test run identified by its ID. 2111 The status can be changed to reflect the current state of the test execution 2112 (e.g., "PASS", "FAIL", "TODO", etc.). 2113 Args: 2114 test_run_id (str): The unique identifier of the test run to update. 2115 This is the internal Xray ID for the test run, not the Jira issue key. 2116 test_run_status (str): The new status to set for the test run. 2117 Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. 2118 Returns: 2119 bool: True if the status update was successful, False otherwise. 2120 Returns None if an error occurs during the API request. 2121 Example: 2122 >>> client = XrayGraphQL() 2123 >>> test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") 2124 >>> success = client.update_test_run_status(test_run_id, "PASS") 2125 >>> print(success) 2126 True 2127 Note: 2128 - The test run ID must be valid and accessible with current authentication 2129 - The status value should be one of the valid status values configured in your Xray instance 2130 - Failed updates are logged as errors with details about the failure 2131 Raises: 2132 Exception: If there is an error making the GraphQL request or processing the response. 2133 The exception is caught and logged, and the method returns None. 2134 """ 2135 try: 2136 query = """ 2137 mutation UpdateTestRunStatus($testRunId: String!, $status: String!) { 2138 updateTestRunStatus( 2139 id: $testRunId, 2140 status: $status 2141 ) 2142 } 2143 """ 2144 variables = { 2145 "testRunId": test_run_id, 2146 "status": test_run_status 2147 } 2148 data = self._make_graphql_request(query, variables) 2149 if not data: 2150 logger.error(f"Failed to get test run status for test {data}") 2151 return None 2152 # logger.info(f"Test run status updated: {data}") 2153 return data['updateTestRunStatus'] 2154 except Exception as e: 2155 logger.error(f"Error updating test run status: {str(e)}") 2156 logger.traceback(e) 2157 return None 2158 2159 def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2160 """ 2161 Update the comment of a specific test run in Xray using the GraphQL API. 2162 This method allows adding or updating the comment associated with a test run 2163 identified by its ID. The comment can provide additional context, test results, 2164 or any other relevant information about the test execution. 2165 Args: 2166 test_run_id (str): The unique identifier of the test run to update. 2167 This is the internal Xray ID for the test run, not the Jira issue key. 2168 test_run_comment (str): The new comment text to set for the test run. 2169 This will replace any existing comment on the test run. 2170 Returns: 2171 bool: True if the comment update was successful, False otherwise. 2172 Returns None if an error occurs during the API request. 2173 Example: 2174 >>> client = XrayGraphQL() 2175 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2176 >>> success = client.update_test_run_comment( 2177 ... test_run_id, 2178 ... "Test passed with performance within expected range" 2179 ... ) 2180 >>> print(success) 2181 True 2182 Note: 2183 - The test run ID must be valid and accessible with current authentication 2184 - The comment can include any text content, including newlines and special characters 2185 - Failed updates are logged as errors with details about the failure 2186 - This method will overwrite any existing comment on the test run 2187 Raises: 2188 Exception: If there is an error making the GraphQL request or processing the response. 2189 The exception is caught and logged, and the method returns None. 2190 """ 2191 try: 2192 query = """ 2193 mutation UpdateTestRunStatus($testRunId: String!, $comment: String!) { 2194 updateTestRunComment( 2195 id: $testRunId, 2196 comment: $comment 2197 ) 2198 } 2199 """ 2200 variables = { 2201 "testRunId": test_run_id, 2202 "comment": test_run_comment 2203 } 2204 data = self._make_graphql_request(query, variables) 2205 if not data: 2206 logger.error(f"Failed to get test run comment for test {data}") 2207 return None 2208 # jprint(data) 2209 return data['updateTestRunComment'] 2210 except Exception as e: 2211 logger.error(f"Error updating test run comment: {str(e)}") 2212 logger.traceback(e) 2213 return None 2214 2215 def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool: 2216 """Add evidence (attachments) to a test run in Xray. 2217 This method allows attaching files as evidence to a specific test run. The file is 2218 read, converted to base64, and uploaded to Xray with appropriate MIME type detection. 2219 Parameters 2220 ---------- 2221 test_run_id : str 2222 The unique identifier of the test run to add evidence to 2223 evidence_path : str 2224 The local file system path to the evidence file to be attached 2225 Returns 2226 ------- 2227 bool 2228 True if the evidence was successfully added, None if the operation failed. 2229 Returns None in the following cases: 2230 - Test run ID is not provided 2231 - Evidence path is not provided 2232 - Evidence file does not exist 2233 - GraphQL request fails 2234 - Any other error occurs during processing 2235 Examples 2236 -------- 2237 >>> client = XrayGraphQL() 2238 >>> success = client.add_evidence_to_test_run( 2239 ... test_run_id="10001", 2240 ... evidence_path="/path/to/screenshot.png" 2241 ... ) 2242 >>> print(success) 2243 True 2244 Notes 2245 ----- 2246 - The evidence file must exist and be accessible 2247 - The file is automatically converted to base64 for upload 2248 - MIME type is automatically detected, defaults to "text/plain" if detection fails 2249 - The method supports various file types (images, documents, logs, etc.) 2250 - Failed operations are logged with appropriate error messages 2251 """ 2252 try: 2253 if not test_run_id: 2254 logger.error("Test run ID is required") 2255 return None 2256 if not evidence_path: 2257 logger.error("Evidence path is required") 2258 return None 2259 if not os.path.exists(evidence_path): 2260 logger.error(f"Evidence file not found: {evidence_path}") 2261 return None 2262 #if file exists then read the file in base64 2263 evidence_base64 = None 2264 mime_type = None 2265 filename = os.path.basename(evidence_path) 2266 with open(evidence_path, "rb") as file: 2267 evidence_data = file.read() 2268 evidence_base64 = base64.b64encode(evidence_data).decode('utf-8') 2269 mime_type = mimetypes.guess_type(evidence_path)[0] 2270 logger.info(f"For loop -- Mime type: {mime_type}") 2271 if not mime_type: 2272 mime_type = "text/plain" 2273 query = """ 2274 mutation AddEvidenceToTestRun($testRunId: String!, $filename: String!, $mimeType: String!, $evidenceBase64: String!) { 2275 addEvidenceToTestRun( 2276 id: $testRunId, 2277 evidence: [ 2278 { 2279 filename : $filename, 2280 mimeType : $mimeType, 2281 data : $evidenceBase64 2282 } 2283 ] 2284 ) { 2285 addedEvidence 2286 warnings 2287 } 2288 } 2289 """ 2290 variables = { 2291 "testRunId": test_run_id, 2292 "filename": filename, 2293 "mimeType": mime_type, 2294 "evidenceBase64": evidence_base64 2295 } 2296 data = self._make_graphql_request(query, variables) 2297 if not data: 2298 logger.error(f"Failed to add evidence to test run: {data}") 2299 return None 2300 return data['addEvidenceToTestRun'] 2301 except Exception as e: 2302 logger.error(f"Error adding evidence to test run: {str(e)}") 2303 logger.traceback(e) 2304 return None 2305 2306 def create_defect_from_test_run(self, test_run_id: str, project_key: str, parent_issue_key: str, defect_summary: str, defect_description: str) -> Optional[Dict]: 2307 """Create a defect from a test run and link it to the test run in Xray. 2308 This method performs two main operations: 2309 1. Creates a new defect in JIRA with the specified summary and description 2310 2. Links the created defect to the specified test run in Xray 2311 Parameters 2312 ---------- 2313 test_run_id : str 2314 The ID of the test run to create defect from 2315 project_key : str 2316 The JIRA project key where the defect should be created. 2317 If not provided, defaults to "EAGVAL" 2318 parent_issue_key : str 2319 The JIRA key of the parent issue to link the defect to 2320 defect_summary : str 2321 Summary/title of the defect. 2322 If not provided, defaults to "Please provide a summary for the defect" 2323 defect_description : str 2324 Description of the defect. 2325 If not provided, defaults to "Please provide a description for the defect" 2326 Returns 2327 ------- 2328 Optional[Dict] 2329 Response data from the GraphQL API if successful, None if failed. 2330 The response includes: 2331 - addedDefects: List of added defects 2332 - warnings: Any warnings from the operation 2333 Examples 2334 -------- 2335 >>> client = XrayGraphQL() 2336 >>> result = client.create_defect_from_test_run( 2337 ... test_run_id="10001", 2338 ... project_key="PROJ", 2339 ... parent_issue_key="PROJ-456", 2340 ... defect_summary="Test failure in login flow", 2341 ... defect_description="The login button is not responding to clicks" 2342 ... ) 2343 >>> print(result) 2344 { 2345 'addedDefects': ['PROJ-123'], 2346 'warnings': [] 2347 } 2348 Notes 2349 ----- 2350 - The project_key will be split on '-' and only the first part will be used 2351 - The defect will be created with issue type 'Bug' 2352 - The method handles missing parameters with default values 2353 - The parent issue must exist and be accessible to create the defect 2354 """ 2355 try: 2356 if not project_key: 2357 project_key = "EAGVAL" 2358 if not defect_summary: 2359 defect_summary = "Please provide a summary for the defect" 2360 if not defect_description: 2361 defect_description = "Please provide a description for the defect" 2362 project_key = project_key.split("-")[0] 2363 # Fix: Correct parameter order for create_issue 2364 defect_key, defect_id = self.create_issue( 2365 project_key=project_key, 2366 parent_issue_key=parent_issue_key, 2367 summary=defect_summary, 2368 description=defect_description, 2369 issue_type='Bug' 2370 ) 2371 if not defect_key: 2372 logger.error("Failed to create defect issue") 2373 return None 2374 # Then add the defect to the test run 2375 add_defect_mutation = """ 2376 mutation AddDefectsToTestRun( 2377 $testRunId: String!, 2378 $defectKey: String! 2379 ) { 2380 addDefectsToTestRun( id: $testRunId, issues: [$defectKey]) { 2381 addedDefects 2382 warnings 2383 } 2384 } 2385 """ 2386 variables = { 2387 "testRunId": test_run_id, 2388 "defectKey": defect_key 2389 } 2390 data = None 2391 retry_count = 0 2392 while retry_count < 3: 2393 data = self._make_graphql_request(add_defect_mutation, variables) 2394 if not data: 2395 logger.error(f"Failed to add defect [{defect_key}] to test run - {test_run_id}.. retrying... {retry_count}") 2396 retry_count += 1 2397 time.sleep(1) 2398 else: 2399 break 2400 return data 2401 except Exception as e: 2402 logger.error(f"Error creating defect from test run: {str(e)}") 2403 logger.traceback(e) 2404 return None 2405 2406 def get_test_run_comment(self, test_run_id: str) -> Optional[str]: 2407 """ 2408 Retrieve the comment of a specific test run from Xray using the GraphQL API. 2409 This method allows retrieving the comment associated with a test run 2410 identified by its ID. The comment can provide additional context, test results, 2411 or any other relevant information about the test execution. 2412 Args: 2413 test_run_id (str): The unique identifier of the test run to retrieve comment from. 2414 This is the internal Xray ID for the test run, not the Jira issue key. 2415 Returns: 2416 Optional[str]: The comment text of the test run if successful, None if: 2417 - The test run ID is not found 2418 - The GraphQL request fails 2419 - No comment exists for the test run 2420 - Any other error occurs during the API request 2421 Example: 2422 >>> client = XrayGraphQL() 2423 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2424 >>> comment = client.get_test_run_comment(test_run_id) 2425 >>> print(comment) 2426 "Test passed with performance within expected range" 2427 Note: 2428 - The test run ID must be valid and accessible with current authentication 2429 - If no comment exists for the test run, the method will return None 2430 - Failed requests are logged as errors with details about the failure 2431 - The method returns the raw comment text as stored in Xray 2432 Raises: 2433 Exception: If there is an error making the GraphQL request or processing the response. 2434 The exception is caught and logged, and the method returns None. 2435 """ 2436 try: 2437 # Try the direct ID approach first 2438 query = """ 2439 query GetTestRunComment($testRunId: String!) { 2440 getTestRunById(id: $testRunId) { 2441 id 2442 comment 2443 status { 2444 name 2445 } 2446 } 2447 } 2448 """ 2449 variables = { 2450 "testRunId": test_run_id 2451 } 2452 data = self._make_graphql_request(query, variables) 2453 jprint(data) 2454 if not data: 2455 logger.error(f"Failed to get test run comment for test run {test_run_id}") 2456 return None 2457 test_run = data.get('getTestRunById', {}) 2458 if not test_run: 2459 logger.warning(f"No test run found with ID {test_run_id}") 2460 return None 2461 comment = test_run.get('comment') 2462 return comment 2463 except Exception as e: 2464 logger.error(f"Error getting test run comment: {str(e)}") 2465 logger.traceback(e) 2466 return None 2467 2468 def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2469 """ 2470 Append the comment of a specific test run in Xray using the GraphQL API. 2471 This method allows appending the comment associated with a test run 2472 identified by its ID. The comment can provide additional context, test results, 2473 or any other relevant information about the test execution. 2474 Args: 2475 test_run_id (str): The unique identifier of the test run to update. 2476 This is the internal Xray ID for the test run, not the Jira issue key. 2477 test_run_comment (str): The comment text to append to the test run. 2478 This will be added to any existing comment on the test run with proper formatting. 2479 Returns: 2480 bool: True if the comment update was successful, False otherwise. 2481 Returns None if an error occurs during the API request. 2482 Example: 2483 >>> client = XrayGraphQL() 2484 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2485 >>> success = client.append_test_run_comment( 2486 ... test_run_id, 2487 ... "Test passed with performance within expected range" 2488 ... ) 2489 >>> print(success) 2490 True 2491 Note: 2492 - The test run ID must be valid and accessible with current authentication 2493 - The comment can include any text content, including newlines and special characters 2494 - Failed updates are logged as errors with details about the failure 2495 - This method will append to existing comments with proper line breaks 2496 - If no existing comment exists, the new comment will be set as the initial comment 2497 Raises: 2498 Exception: If there is an error making the GraphQL request or processing the response. 2499 The exception is caught and logged, and the method returns None. 2500 """ 2501 try: 2502 # Get existing comment 2503 existing_comment = self.get_test_run_comment(test_run_id) 2504 # Prepare the combined comment with proper formatting 2505 if existing_comment: 2506 # If there's an existing comment, append with double newline for proper separation 2507 combined_comment = f"{existing_comment}\n{test_run_comment}" 2508 else: 2509 # If no existing comment, use the new comment as is 2510 combined_comment = test_run_comment 2511 logger.debug(f"No existing comment found for test run {test_run_id}, setting initial comment") 2512 query = """ 2513 mutation UpdateTestRunComment($testRunId: String!, $comment: String!) { 2514 updateTestRunComment( 2515 id: $testRunId, 2516 comment: $comment 2517 ) 2518 } 2519 """ 2520 variables = { 2521 "testRunId": test_run_id, 2522 "comment": combined_comment 2523 } 2524 data = self._make_graphql_request(query, variables) 2525 if not data: 2526 logger.error(f"Failed to update test run comment for test run {test_run_id}") 2527 return None 2528 return data['updateTestRunComment'] 2529 except Exception as e: 2530 logger.error(f"Error updating test run comment: {str(e)}") 2531 logger.traceback(e) 2532 return None 2533 2534 def download_attachment(self, jira_key: str, file_extension: str) -> Optional[Dict]: 2535 ''' 2536 Download a JIRA attachment by its ID. 2537 ''' 2538 try: 2539 response = self.make_jira_request(jira_key, 'GET') 2540 2541 if not response or 'fields' not in response: 2542 logger.error(f"Error: Could not retrieve issue {jira_key}") 2543 return None 2544 2545 # Find attachment by filename 2546 attachments = response.get('fields', {}).get('attachment', []) 2547 target_attachment = [att for att in attachments if att.get('filename').endswith(file_extension)] 2548 if not target_attachment: 2549 logger.error(f"No attachment found for {jira_key} with extension {file_extension}") 2550 return None 2551 2552 combined_attachment = [] 2553 for attachment in target_attachment: 2554 attachment_id = attachment.get('id') 2555 mime_type = attachment.get('mimeType', '') 2556 combined_attachment.append(self.download_jira_attachment_by_id(attachment_id, mime_type)) 2557 2558 return combined_attachment 2559 except Exception as e: 2560 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2561 logger.traceback(e) 2562 return None 2563
13class JiraHandler(): 14 """A handler class for interacting with JIRA's REST API. 15 This class provides methods to interact with JIRA's REST API, handling authentication, 16 issue creation, retrieval, and various JIRA operations. It uses environment variables for 17 configuration and provides robust error handling with comprehensive logging. 18 The class supports creating issues with attachments, linking issues, and retrieving 19 detailed issue information with customizable field selection. It handles various 20 JIRA field types including user information, attachments, comments, and issue links. 21 Attributes 22 ---------- 23 client : JIRA 24 The JIRA client instance used for API interactions 25 Environment Variables 26 -------------------- 27 JIRA_SERVER : str 28 The JIRA server URL (default: 'https://arusa.atlassian.net') 29 JIRA_USER : str 30 The JIRA user email (default: 'yakub@arusatech.com') 31 JIRA_API_KEY : str 32 The JIRA API key for authentication (required) 33 Methods 34 ------- 35 create_issue(project_key, summary, description, **kwargs) 36 Create a new JIRA issue with optional attachments, linking, and custom fields 37 get_issue(issue_key, fields=None) 38 Retrieve a JIRA issue with specified fields or all available fields 39 Examples 40 -------- 41 >>> handler = JiraHandler() 42 >>> # Create a new issue 43 >>> issue_key, issue_id = handler.create_issue( 44 ... project_key="PROJ", 45 ... summary="New feature implementation", 46 ... description="Implement new login flow", 47 ... issue_type="Story", 48 ... priority="High", 49 ... labels=["feature", "login"], 50 ... attachments=["/path/to/screenshot.png"] 51 ... ) 52 >>> print(f"Created issue {issue_key} with ID {issue_id}") 53 >>> # Retrieve issue details 54 >>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"]) 55 >>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}") 56 Notes 57 ----- 58 - Requires valid JIRA credentials stored in environment variables 59 - Automatically loads configuration from .env file if present 60 - Provides comprehensive error handling and logging 61 - Supports various JIRA field types and custom fields 62 - Handles file attachments with automatic MIME type detection 63 - Creates issue links with configurable link types 64 - Returns None for failed operations instead of raising exceptions 65 """ 66 67 def __init__(self): 68 """Initialize the JIRA client with configuration from environment variables. 69 This constructor sets up the JIRA client by reading configuration from 70 environment variables. It automatically loads variables from a .env file 71 if present in the project root. 72 Environment Variables 73 ------------------- 74 JIRA_SERVER : str 75 The JIRA server URL (default: 'https://arusatech.atlassian.net') 76 JIRA_USER : str 77 The JIRA user email (default: 'yakub@arusatech.com') 78 JIRA_API_KEY : str 79 The JIRA API key for authentication 80 Raises 81 ------ 82 Exception 83 If the JIRA client initialization fails 84 """ 85 try: 86 # Load environment variables from .env file 87 jira_server = os.getenv('JIRA_SERVER', 'https://arusatech.atlassian.net') 88 jira_user = os.getenv('JIRA_USER', 'yakub@arusatech.com') 89 jira_api_key = os.getenv('JIRA_API_KEY', "") 90 # Validate required environment variables 91 if not jira_api_key or jira_api_key == '<JIRA_API_KEY>': 92 raise ValueError("JIRA_API_KEY environment variable is required and must be set to a valid API key") 93 self.client = JIRA( 94 server=jira_server, 95 basic_auth=(jira_user, jira_api_key) 96 ) 97 logger.info("JIRA client initialized successfully") 98 except Exception as e: 99 logger.error(f"Failed to initialize JIRA client: {str(e)}") 100 logger.traceback(e) 101 raise 102 103 def create_issue( 104 self, 105 project_key: str, 106 summary: str, 107 description: str, 108 issue_type: str = None, 109 priority: str = None, 110 assignee: str = None, 111 labels: List[str] = None, 112 components: List[str] = None, 113 attachments: List[str] = None, 114 parent_issue_key: str = None, 115 linked_issues: List[Dict[str, str]] = None, 116 custom_fields: Dict[str, Any] = None 117 ) -> Optional[tuple[str, str]]: 118 """Create a new issue in JIRA with the specified details. 119 This method creates a new JIRA issue with the provided details and handles 120 optional features like attachments and issue linking. 121 Parameters 122 ---------- 123 project_key : str 124 The key of the project where the issue should be created 125 summary : str 126 The summary/title of the issue 127 description : str 128 The detailed description of the issue 129 issue_type : str, optional 130 The type of issue (default: 'Bug') 131 priority : str, optional 132 The priority of the issue 133 assignee : str, optional 134 The username of the assignee 135 labels : List[str], optional 136 List of labels to add to the issue 137 components : List[str], optional 138 List of component names to add to the issue 139 attachments : List[str], optional 140 List of file paths to attach to the issue 141 linked_issues : List[Dict[str, str]], optional 142 List of issues to link to the new issue. Each dict should contain: 143 - 'key': The issue key to link to 144 - 'type': The type of link (default: 'Relates') 145 custom_fields : Dict[str, Any], optional 146 Dictionary of custom fields to set on the issue 147 Returns 148 ------- 149 Optional[tuple[str, str]] 150 A tuple containing (issue_key, issue_id) if successful, 151 (None, None) if creation fails 152 Examples 153 -------- 154 >>> handler = JiraHandler() 155 >>> result = handler.create_issue( 156 ... project_key="PROJ", 157 ... summary="Bug in login", 158 ... description="User cannot login with valid credentials", 159 ... issue_type="Bug", 160 ... priority="High", 161 ... labels=["login", "bug"], 162 ... components=["Authentication"], 163 ... attachments=["/path/to/screenshot.png"], 164 ... linked_issues=[{"key": "PROJ-123", "type": "Blocks"}] 165 ... ) 166 >>> print(f"Created issue {result[0]} with ID {result[1]}") 167 """ 168 try: 169 # Build basic issue fields 170 issue_dict = { 171 'project': {'key': project_key}, 172 'summary': summary, 173 'description': description, 174 'issuetype': {'name': issue_type or 'Bug'}, 175 'parent': {'key': parent_issue_key} if parent_issue_key else None 176 } 177 # Add optional fields 178 if priority: 179 issue_dict['priority'] = {'name': priority} 180 if assignee: 181 issue_dict['assignee'] = {'name': assignee} 182 if labels: 183 issue_dict['labels'] = labels 184 if components: 185 issue_dict['components'] = [{'name': c} for c in components] 186 # Add any custom fields 187 if custom_fields: 188 issue_dict.update(custom_fields) 189 # Create the issue 190 issue = self.client.create_issue(fields=issue_dict) 191 logger.info(f"Created JIRA issue : {issue.key} [ID: {issue.id}]") 192 # Add attachments if provided 193 if attachments: 194 for file_path in attachments: 195 if os.path.exists(file_path): 196 self.client.add_attachment( 197 issue=issue.key, 198 attachment=file_path 199 ) 200 logger.info(f"Added attachment: {file_path}") 201 else: 202 logger.warning(f"Attachment not found: {file_path}") 203 # Create issue links if provided 204 if linked_issues: 205 for link in linked_issues: 206 try: 207 self.client.create_issue_link( 208 link.get('type', 'Relates'), 209 issue.key, 210 link['key'] 211 ) 212 logger.info(f"Created link between {issue.key} and {link['key']}") 213 except Exception as e: 214 logger.error(f"Failed to create link to {link['key']}: {str(e)}") 215 return (issue.key, issue.id) 216 except Exception as e: 217 logger.error(f"Failed to create JIRA issue for project {project_key}: {str(e)}") 218 logger.traceback(e) 219 return (None, None) 220 221 def get_issue(self, issue_key: str, fields: List[str] = None) -> Optional[Dict[str, Any]]: 222 """Get an issue by its key with specified fields. 223 This method retrieves a JIRA issue using its key and returns the issue details 224 with the specified fields. If no fields are specified, it returns all available fields. 225 Parameters 226 ---------- 227 issue_key : str 228 The JIRA issue key to retrieve (e.g., "PROJ-123") 229 fields : List[str], optional 230 List of specific fields to retrieve. If None, all fields are returned. 231 Common fields include: "summary", "description", "status", "assignee", 232 "reporter", "created", "updated", "priority", "labels", "components", 233 "attachments", "comments", "issuetype", "project" 234 Returns 235 ------- 236 Optional[Dict[str, Any]] 237 A dictionary containing the issue details if successful, None if the issue 238 is not found or an error occurs. 239 Examples 240 -------- 241 >>> handler = JiraHandler() 242 >>> # Get all fields 243 >>> issue = handler.get_issue("PROJ-123") 244 >>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}") 245 >>> # Get specific fields only 246 >>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"]) 247 >>> print(f"Assignee: {issue['assignee']['displayName'] if issue['assignee'] else 'Unassigned'}") 248 Notes 249 ----- 250 - The issue key must be valid and accessible with current authentication 251 - If fields parameter is None, all fields are returned 252 - Some fields may be None if the issue doesn't have values for them 253 - Failed operations are logged as errors with relevant details 254 - The method handles missing issues gracefully by returning None 255 """ 256 try: 257 if not issue_key: 258 logger.error("Issue key is required") 259 return None 260 # Define field mappings for JIRA API 261 field_mappings = { 262 'summary': 'summary', 263 'description': 'description', 264 'status': 'status', 265 'assignee': 'assignee', 266 'reporter': 'reporter', 267 'priority': 'priority', 268 'labels': 'labels', 269 'components': 'components', 270 'issuetype': 'issuetype', 271 'project': 'project', 272 'created': 'created', 273 'updated': 'updated', 274 'resolutiondate': 'resolutiondate', 275 'duedate': 'duedate', 276 'attachments': 'attachments', 277 'comment': 'comment', 278 'issuelinks': 'issuelinks' 279 } 280 # Determine requested fields 281 if fields is None: 282 requested_fields = None 283 else: 284 # Map requested fields to JIRA field names 285 jira_fields = [field_mappings.get(field, field) for field in fields] 286 # Always include key and id as they're required 287 if 'key' not in fields: 288 jira_fields.append('key') 289 if 'id' not in fields: 290 jira_fields.append('id') 291 requested_fields = ','.join(jira_fields) 292 # Get the issue using the JIRA client 293 issue = self.client.issue(issue_key, fields=requested_fields) 294 if not issue: 295 logger.warning(f"Issue not found: {issue_key}") 296 return None 297 # Helper function to safely get user attributes 298 def get_user_dict(user_obj): 299 if not user_obj: 300 return None 301 try: 302 return { 303 'name': getattr(user_obj, 'name', None), 304 'displayName': getattr(user_obj, 'displayName', None), 305 'emailAddress': getattr(user_obj, 'emailAddress', None) 306 } 307 except Exception: 308 return None 309 # Helper function to safely get field value 310 def safe_get_field(field_name, default=None): 311 try: 312 return getattr(issue.fields, field_name, default) 313 except AttributeError: 314 return default 315 # Helper function to get object attributes safely 316 def get_object_attrs(obj, attrs): 317 if not obj: 318 return None 319 return {attr: getattr(obj, attr, None) for attr in attrs} 320 # Helper function to process attachments 321 def process_attachments(attachments): 322 if not attachments: 323 return [] 324 return [ 325 { 326 'id': getattr(att, 'id', None), 327 'filename': getattr(att, 'filename', None), 328 'size': getattr(att, 'size', None), 329 'created': getattr(att, 'created', None), 330 'mimeType': getattr(att, 'mimeType', None) 331 } for att in attachments 332 ] 333 # Helper function to process comments 334 def process_comments(comments): 335 if not comments or not hasattr(comments, 'comments'): 336 return [] 337 return [ 338 { 339 'id': getattr(comment, 'id', None), 340 'body': getattr(comment, 'body', None), 341 'author': get_user_dict(comment.author), 342 'created': getattr(comment, 'created', None), 343 'updated': getattr(comment, 'updated', None) 344 } for comment in comments.comments 345 ] 346 # Helper function to process issue links 347 def process_issue_links(issue_links): 348 if not issue_links: 349 return [] 350 def process_issue_reference(issue_ref, direction): 351 if not hasattr(issue_ref, direction) or not getattr(issue_ref, direction): 352 return None 353 ref_issue = getattr(issue_ref, direction) 354 return { 355 'key': getattr(ref_issue, 'key', None), 356 'id': getattr(ref_issue, 'id', None), 357 'fields': { 358 'summary': getattr(ref_issue.fields, 'summary', None), 359 'status': get_object_attrs(ref_issue.fields.status, ['name']) if ref_issue.fields.status else None 360 } 361 } 362 return [ 363 { 364 'id': getattr(link, 'id', None), 365 'type': get_object_attrs(link.type, ['id', 'name', 'inward', 'outward']) if link.type else None, 366 'inwardIssue': process_issue_reference(link, 'inwardIssue'), 367 'outwardIssue': process_issue_reference(link, 'outwardIssue') 368 } for link in issue_links 369 ] 370 # Build response dictionary 371 issue_dict = { 372 'key': issue.key, 373 'id': issue.id 374 } 375 # Determine which fields to process 376 fields_to_process = fields if fields is not None else list(field_mappings.keys()) 377 # Process each field 378 for field in fields_to_process: 379 if field in ['key', 'id']: 380 continue # Already handled 381 field_value = safe_get_field(field_mappings.get(field, field)) 382 match field: 383 case 'summary' | 'description' | 'created' | 'updated' | 'resolutiondate' | 'duedate': 384 issue_dict[field] = field_value 385 case 'status' | 'issuetype' | 'priority': 386 issue_dict[field] = get_object_attrs(field_value, ['id', 'name', 'description']) 387 case 'project': 388 issue_dict[field] = get_object_attrs(field_value, ['key', 'name', 'id']) 389 case 'assignee' | 'reporter': 390 issue_dict[field] = get_user_dict(field_value) 391 case 'labels': 392 issue_dict[field] = list(field_value) if field_value else [] 393 case 'components': 394 issue_dict[field] = [ 395 get_object_attrs(comp, ['id', 'name', 'description']) 396 for comp in (field_value or []) 397 ] 398 case 'attachments': 399 issue_dict[field] = process_attachments(field_value) 400 case 'comments': 401 issue_dict[field] = process_comments(field_value) 402 case 'issuelinks': 403 issue_dict[field] = process_issue_links(field_value) 404 case _: 405 # Handle unknown fields or custom fields 406 issue_dict[field] = field_value 407 # logger.info(f"Retrieved JIRA issue: {issue_key}") 408 return issue_dict 409 except Exception as e: 410 logger.error(f"Failed to get JIRA issue {issue_key}: {str(e)}") 411 logger.traceback(e) 412 return None 413 414 def update_issue_summary(self, issue_key: str, new_summary: str) -> bool: 415 """Update the summary of a JIRA issue. 416 This method updates the summary field of an existing JIRA issue using the JIRA REST API. 417 It validates the input parameters and handles errors gracefully with comprehensive logging. 418 Parameters 419 ---------- 420 issue_key : str 421 The JIRA issue key to update (e.g., "PROJ-123") 422 new_summary : str 423 The new summary text to set for the issue 424 Returns 425 ------- 426 bool 427 True if the summary was successfully updated, False if the operation failed. 428 Returns None if an error occurs during the API request. 429 Examples 430 -------- 431 >>> handler = JiraHandler() 432 >>> success = handler.update_issue_summary("PROJ-123", "Updated bug description") 433 >>> print(success) 434 True 435 Notes 436 ----- 437 - The issue key must be valid and accessible with current authentication 438 - The new summary cannot be empty or None 439 - Failed operations are logged as errors with relevant details 440 - The method uses the JIRA client's update method for efficient API calls 441 """ 442 try: 443 # Validate input parameters 444 if not issue_key or not issue_key.strip(): 445 logger.error("Issue key is required and cannot be empty") 446 return False 447 if not new_summary or not new_summary.strip(): 448 logger.error("New summary is required and cannot be empty") 449 return False 450 # Strip whitespace from inputs 451 issue_key = issue_key.strip() 452 new_summary = new_summary.strip() 453 # logger.info(f"Updating summary for issue {issue_key}") 454 # Get the issue object 455 issue = self.client.issue(issue_key) 456 if not issue: 457 logger.error(f"Issue not found: {issue_key}") 458 return False 459 # Update the summary field 460 issue.update(summary=new_summary) 461 logger.info(f"Successfully updated summary for issue {issue_key}") 462 return True 463 except Exception as e: 464 logger.error(f"Failed to update summary for issue {issue_key}: {str(e)}") 465 logger.traceback(e) 466 return False 467 468 def _build_auth_headers(self, api_key: str = None, user: str = None, cookie: str = None) -> Dict[str, str]: 469 """ 470 Build authentication headers for JIRA API requests. 471 472 This method converts an API key to base64 format and creates the proper 473 Authorization header, similar to how Postman generates it. 474 475 Parameters 476 ---------- 477 api_key : str, optional 478 The JIRA API key. If not provided, uses the one from environment variables. 479 user : str, optional 480 The JIRA user email. If not provided, uses the one from environment variables. 481 cookie : str, optional 482 Additional cookie value to include in headers. 483 484 Returns 485 ------- 486 Dict[str, str] 487 Dictionary containing the Authorization and Cookie headers. 488 489 Examples 490 -------- 491 >>> handler = JiraHandler() 492 >>> headers = handler._build_auth_headers() 493 >>> print(headers) 494 { 495 'Authorization': 'Basic eWFrdWIubW9oYW1tYWRAd25jby5jb206QVRBVFQzeEZmR0YwN29tcFRCcU9FVUxlXzJjWlFDbkJXb2ZTYS1xMW92YmYxYnBURC1URmppY3VFczVBUzFJMkdjaXcybHlNMEFaRjl1T19OSU0yR0tIMlZ6SkQtQ0JtLTV2T05RNHhnMEFKbzVoaWhtQjIxaHc3Zk54MUFicjFtTWx1R0M4cVJoVDIzUkZlQUlaMVk3UUd0UnBLQlFLOV9iV0hyWnhPOWlucURRVjh4ZC0wd2tNPTIyQTdDMjg1', 496 'Cookie': 'atlassian.xsrf.token=9dd7b0ae95b82b138b9fd93e27a45a6fd01c548e_lin' 497 } 498 """ 499 try: 500 # Use provided values or fall back to environment variables 501 api_key = api_key or os.getenv('JIRA_API_KEY') 502 user = user or os.getenv('JIRA_USER', 'yakub@arusatech.com') 503 504 if not api_key: 505 raise ValueError("API key is required") 506 if not user: 507 raise ValueError("User email is required") 508 509 # Create the credentials string in format "user:api_key" 510 credentials = f"{user}:{api_key}" 511 512 # Encode to base64 513 encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') 514 515 # Build headers 516 headers = { 517 'Authorization': f'Basic {encoded_credentials}' 518 } 519 520 # Add cookie if provided 521 if cookie: 522 headers['Cookie'] = cookie 523 524 return headers 525 526 except Exception as e: 527 logger.error(f"Failed to build auth headers: {str(e)}") 528 logger.traceback(e) 529 raise 530 531 def make_jira_request(self, jira_key: str, url: str, method: str = "GET", payload: Dict = None, 532 api_key: str = None, user: str = None, cookie: str = None) -> Optional[Dict]: 533 """ 534 Make a JIRA API request with proper authentication headers. 535 536 This method builds the authentication headers (similar to Postman) and 537 makes the request to the JIRA API. 538 539 Parameters 540 ---------- 541 jira_key : str 542 The JIRA issue key 543 method : str, optional 544 HTTP method (GET, POST, PUT, DELETE). Defaults to "GET" 545 payload : Dict, optional 546 Request payload for POST/PUT requests 547 api_key : str, optional 548 The JIRA API key. If not provided, uses environment variable 549 user : str, optional 550 The JIRA user email. If not provided, uses environment variable 551 cookie : str, optional 552 Additional cookie value 553 554 Returns 555 ------- 556 Optional[Dict] 557 The JSON response from the API, or None if the request fails 558 559 Examples 560 -------- 561 >>> handler = JiraHandler() 562 >>> response = handler.make_jira_request( 563 ... url="https://arusa.atlassian.net/rest/api/2/issue/XSP1-543213456" 564 ... ) 565 >>> print(response) 566 {'id': '12345', 'key': 'XSP1-3456', ...} 567 """ 568 try: 569 if not jira_key: 570 logger.error("JIRA issue key is required") 571 return None 572 573 url = f"{os.getenv('JIRA_SERVER')}/rest/api/2/issue/{jira_key}" 574 if not url: 575 logger.error("JIRA API endpoint URL is required") 576 return None 577 # Build authentication headers 578 headers = self._build_auth_headers(api_key, user, cookie) 579 580 # Make the request 581 response = requests.request(method, url, headers=headers, data=payload) 582 response.raise_for_status() 583 584 # Return JSON response 585 return response.json() 586 587 except requests.exceptions.RequestException as e: 588 logger.error(f"JIRA API request failed: {str(e)}") 589 logger.traceback(e) 590 return None 591 except Exception as e: 592 logger.error(f"Unexpected error in JIRA request: {str(e)}") 593 logger.traceback(e) 594 return None 595 596 def download_jira_attachment_by_id(self, attachment_id: str, mime_type: str) -> Optional[Dict]: 597 ''' 598 Download a JIRA attachment by its ID. 599 ''' 600 try: 601 # ATTACHMENT_URL = f"{os.getenv('JIRA_SERVER')}/rest/api/2/attachment/{attachment_id}" 602 CONTENT_URL = f"{os.getenv('JIRA_SERVER')}/rest/api/2/attachment/content/{attachment_id}" 603 if not CONTENT_URL: 604 logger.error(f"No content URL found for attachment '{attachment_id}'") 605 return None 606 headers = self._build_auth_headers() 607 download_response = requests.get(CONTENT_URL, headers=headers) 608 download_response.raise_for_status() 609 content = download_response.content 610 #Process content based on type 611 result = { 612 'content': content, 613 'mime_type': mime_type, 614 'text_content': None, 615 'json_content': None 616 } 617 618 # Handle text-based files 619 if mime_type.startswith(('text/', 'application/json', 'application/xml' , 'json')): 620 try: 621 text_content = content.decode('utf-8') 622 result['text_content'] = text_content 623 624 # Try to parse as JSON 625 if mime_type == 'application/json': 626 try: 627 result['json_content'] = json.loads(text_content) 628 except json.JSONDecodeError: 629 pass 630 except UnicodeDecodeError: 631 logger.error(f"Warning: Could not decode text content for {attachment_id}") 632 logger.traceback(e) 633 634 return result 635 except Exception as e: 636 logger.error(f"Error downloading JIRA attachment: {str(e)}") 637 logger.traceback(e) 638 return None
A handler class for interacting with JIRA's REST API. This class provides methods to interact with JIRA's REST API, handling authentication, issue creation, retrieval, and various JIRA operations. It uses environment variables for configuration and provides robust error handling with comprehensive logging. The class supports creating issues with attachments, linking issues, and retrieving detailed issue information with customizable field selection. It handles various JIRA field types including user information, attachments, comments, and issue links.
Attributes
client : JIRA The JIRA client instance used for API interactions
Environment Variables
JIRA_SERVER : str The JIRA server URL (default: 'https://arusa.atlassian.net') JIRA_USER : str The JIRA user email (default: 'yakub@arusatech.com') JIRA_API_KEY : str The JIRA API key for authentication (required)
Methods
create_issue(project_key, summary, description, **kwargs) Create a new JIRA issue with optional attachments, linking, and custom fields get_issue(issue_key, fields=None) Retrieve a JIRA issue with specified fields or all available fields
Examples
>>> handler = JiraHandler()
>>> # Create a new issue
>>> issue_key, issue_id = handler.create_issue(
... project_key="PROJ",
... summary="New feature implementation",
... description="Implement new login flow",
... issue_type="Story",
... priority="High",
... labels=["feature", "login"],
... attachments=["/path/to/screenshot.png"]
... )
>>> print(f"Created issue {issue_key} with ID {issue_id}")
>>> # Retrieve issue details
>>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"])
>>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}")
<h2 id="notes">Notes</h2>
- Requires valid JIRA credentials stored in environment variables
- Automatically loads configuration from .env file if present
- Provides comprehensive error handling and logging
- Supports various JIRA field types and custom fields
- Handles file attachments with automatic MIME type detection
- Creates issue links with configurable link types
- Returns None for failed operations instead of raising exceptions
67 def __init__(self): 68 """Initialize the JIRA client with configuration from environment variables. 69 This constructor sets up the JIRA client by reading configuration from 70 environment variables. It automatically loads variables from a .env file 71 if present in the project root. 72 Environment Variables 73 ------------------- 74 JIRA_SERVER : str 75 The JIRA server URL (default: 'https://arusatech.atlassian.net') 76 JIRA_USER : str 77 The JIRA user email (default: 'yakub@arusatech.com') 78 JIRA_API_KEY : str 79 The JIRA API key for authentication 80 Raises 81 ------ 82 Exception 83 If the JIRA client initialization fails 84 """ 85 try: 86 # Load environment variables from .env file 87 jira_server = os.getenv('JIRA_SERVER', 'https://arusatech.atlassian.net') 88 jira_user = os.getenv('JIRA_USER', 'yakub@arusatech.com') 89 jira_api_key = os.getenv('JIRA_API_KEY', "") 90 # Validate required environment variables 91 if not jira_api_key or jira_api_key == '<JIRA_API_KEY>': 92 raise ValueError("JIRA_API_KEY environment variable is required and must be set to a valid API key") 93 self.client = JIRA( 94 server=jira_server, 95 basic_auth=(jira_user, jira_api_key) 96 ) 97 logger.info("JIRA client initialized successfully") 98 except Exception as e: 99 logger.error(f"Failed to initialize JIRA client: {str(e)}") 100 logger.traceback(e) 101 raise
Initialize the JIRA client with configuration from environment variables. This constructor sets up the JIRA client by reading configuration from environment variables. It automatically loads variables from a .env file if present in the project root.
Environment Variables
JIRA_SERVER : str The JIRA server URL (default: 'https://arusatech.atlassian.net') JIRA_USER : str The JIRA user email (default: 'yakub@arusatech.com') JIRA_API_KEY : str The JIRA API key for authentication
Raises
Exception If the JIRA client initialization fails
103 def create_issue( 104 self, 105 project_key: str, 106 summary: str, 107 description: str, 108 issue_type: str = None, 109 priority: str = None, 110 assignee: str = None, 111 labels: List[str] = None, 112 components: List[str] = None, 113 attachments: List[str] = None, 114 parent_issue_key: str = None, 115 linked_issues: List[Dict[str, str]] = None, 116 custom_fields: Dict[str, Any] = None 117 ) -> Optional[tuple[str, str]]: 118 """Create a new issue in JIRA with the specified details. 119 This method creates a new JIRA issue with the provided details and handles 120 optional features like attachments and issue linking. 121 Parameters 122 ---------- 123 project_key : str 124 The key of the project where the issue should be created 125 summary : str 126 The summary/title of the issue 127 description : str 128 The detailed description of the issue 129 issue_type : str, optional 130 The type of issue (default: 'Bug') 131 priority : str, optional 132 The priority of the issue 133 assignee : str, optional 134 The username of the assignee 135 labels : List[str], optional 136 List of labels to add to the issue 137 components : List[str], optional 138 List of component names to add to the issue 139 attachments : List[str], optional 140 List of file paths to attach to the issue 141 linked_issues : List[Dict[str, str]], optional 142 List of issues to link to the new issue. Each dict should contain: 143 - 'key': The issue key to link to 144 - 'type': The type of link (default: 'Relates') 145 custom_fields : Dict[str, Any], optional 146 Dictionary of custom fields to set on the issue 147 Returns 148 ------- 149 Optional[tuple[str, str]] 150 A tuple containing (issue_key, issue_id) if successful, 151 (None, None) if creation fails 152 Examples 153 -------- 154 >>> handler = JiraHandler() 155 >>> result = handler.create_issue( 156 ... project_key="PROJ", 157 ... summary="Bug in login", 158 ... description="User cannot login with valid credentials", 159 ... issue_type="Bug", 160 ... priority="High", 161 ... labels=["login", "bug"], 162 ... components=["Authentication"], 163 ... attachments=["/path/to/screenshot.png"], 164 ... linked_issues=[{"key": "PROJ-123", "type": "Blocks"}] 165 ... ) 166 >>> print(f"Created issue {result[0]} with ID {result[1]}") 167 """ 168 try: 169 # Build basic issue fields 170 issue_dict = { 171 'project': {'key': project_key}, 172 'summary': summary, 173 'description': description, 174 'issuetype': {'name': issue_type or 'Bug'}, 175 'parent': {'key': parent_issue_key} if parent_issue_key else None 176 } 177 # Add optional fields 178 if priority: 179 issue_dict['priority'] = {'name': priority} 180 if assignee: 181 issue_dict['assignee'] = {'name': assignee} 182 if labels: 183 issue_dict['labels'] = labels 184 if components: 185 issue_dict['components'] = [{'name': c} for c in components] 186 # Add any custom fields 187 if custom_fields: 188 issue_dict.update(custom_fields) 189 # Create the issue 190 issue = self.client.create_issue(fields=issue_dict) 191 logger.info(f"Created JIRA issue : {issue.key} [ID: {issue.id}]") 192 # Add attachments if provided 193 if attachments: 194 for file_path in attachments: 195 if os.path.exists(file_path): 196 self.client.add_attachment( 197 issue=issue.key, 198 attachment=file_path 199 ) 200 logger.info(f"Added attachment: {file_path}") 201 else: 202 logger.warning(f"Attachment not found: {file_path}") 203 # Create issue links if provided 204 if linked_issues: 205 for link in linked_issues: 206 try: 207 self.client.create_issue_link( 208 link.get('type', 'Relates'), 209 issue.key, 210 link['key'] 211 ) 212 logger.info(f"Created link between {issue.key} and {link['key']}") 213 except Exception as e: 214 logger.error(f"Failed to create link to {link['key']}: {str(e)}") 215 return (issue.key, issue.id) 216 except Exception as e: 217 logger.error(f"Failed to create JIRA issue for project {project_key}: {str(e)}") 218 logger.traceback(e) 219 return (None, None)
Create a new issue in JIRA with the specified details. This method creates a new JIRA issue with the provided details and handles optional features like attachments and issue linking.
Parameters
project_key : str The key of the project where the issue should be created summary : str The summary/title of the issue description : str The detailed description of the issue issue_type : str, optional The type of issue (default: 'Bug') priority : str, optional The priority of the issue assignee : str, optional The username of the assignee labels : List[str], optional List of labels to add to the issue components : List[str], optional List of component names to add to the issue attachments : List[str], optional List of file paths to attach to the issue linked_issues : List[Dict[str, str]], optional List of issues to link to the new issue. Each dict should contain: - 'key': The issue key to link to - 'type': The type of link (default: 'Relates') custom_fields : Dict[str, Any], optional Dictionary of custom fields to set on the issue
Returns
Optional[tuple[str, str]] A tuple containing (issue_key, issue_id) if successful, (None, None) if creation fails
Examples
>>> handler = JiraHandler()
>>> result = handler.create_issue(
... project_key="PROJ",
... summary="Bug in login",
... description="User cannot login with valid credentials",
... issue_type="Bug",
... priority="High",
... labels=["login", "bug"],
... components=["Authentication"],
... attachments=["/path/to/screenshot.png"],
... linked_issues=[{"key": "PROJ-123", "type": "Blocks"}]
... )
>>> print(f"Created issue {result[0]} with ID {result[1]}")
221 def get_issue(self, issue_key: str, fields: List[str] = None) -> Optional[Dict[str, Any]]: 222 """Get an issue by its key with specified fields. 223 This method retrieves a JIRA issue using its key and returns the issue details 224 with the specified fields. If no fields are specified, it returns all available fields. 225 Parameters 226 ---------- 227 issue_key : str 228 The JIRA issue key to retrieve (e.g., "PROJ-123") 229 fields : List[str], optional 230 List of specific fields to retrieve. If None, all fields are returned. 231 Common fields include: "summary", "description", "status", "assignee", 232 "reporter", "created", "updated", "priority", "labels", "components", 233 "attachments", "comments", "issuetype", "project" 234 Returns 235 ------- 236 Optional[Dict[str, Any]] 237 A dictionary containing the issue details if successful, None if the issue 238 is not found or an error occurs. 239 Examples 240 -------- 241 >>> handler = JiraHandler() 242 >>> # Get all fields 243 >>> issue = handler.get_issue("PROJ-123") 244 >>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}") 245 >>> # Get specific fields only 246 >>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"]) 247 >>> print(f"Assignee: {issue['assignee']['displayName'] if issue['assignee'] else 'Unassigned'}") 248 Notes 249 ----- 250 - The issue key must be valid and accessible with current authentication 251 - If fields parameter is None, all fields are returned 252 - Some fields may be None if the issue doesn't have values for them 253 - Failed operations are logged as errors with relevant details 254 - The method handles missing issues gracefully by returning None 255 """ 256 try: 257 if not issue_key: 258 logger.error("Issue key is required") 259 return None 260 # Define field mappings for JIRA API 261 field_mappings = { 262 'summary': 'summary', 263 'description': 'description', 264 'status': 'status', 265 'assignee': 'assignee', 266 'reporter': 'reporter', 267 'priority': 'priority', 268 'labels': 'labels', 269 'components': 'components', 270 'issuetype': 'issuetype', 271 'project': 'project', 272 'created': 'created', 273 'updated': 'updated', 274 'resolutiondate': 'resolutiondate', 275 'duedate': 'duedate', 276 'attachments': 'attachments', 277 'comment': 'comment', 278 'issuelinks': 'issuelinks' 279 } 280 # Determine requested fields 281 if fields is None: 282 requested_fields = None 283 else: 284 # Map requested fields to JIRA field names 285 jira_fields = [field_mappings.get(field, field) for field in fields] 286 # Always include key and id as they're required 287 if 'key' not in fields: 288 jira_fields.append('key') 289 if 'id' not in fields: 290 jira_fields.append('id') 291 requested_fields = ','.join(jira_fields) 292 # Get the issue using the JIRA client 293 issue = self.client.issue(issue_key, fields=requested_fields) 294 if not issue: 295 logger.warning(f"Issue not found: {issue_key}") 296 return None 297 # Helper function to safely get user attributes 298 def get_user_dict(user_obj): 299 if not user_obj: 300 return None 301 try: 302 return { 303 'name': getattr(user_obj, 'name', None), 304 'displayName': getattr(user_obj, 'displayName', None), 305 'emailAddress': getattr(user_obj, 'emailAddress', None) 306 } 307 except Exception: 308 return None 309 # Helper function to safely get field value 310 def safe_get_field(field_name, default=None): 311 try: 312 return getattr(issue.fields, field_name, default) 313 except AttributeError: 314 return default 315 # Helper function to get object attributes safely 316 def get_object_attrs(obj, attrs): 317 if not obj: 318 return None 319 return {attr: getattr(obj, attr, None) for attr in attrs} 320 # Helper function to process attachments 321 def process_attachments(attachments): 322 if not attachments: 323 return [] 324 return [ 325 { 326 'id': getattr(att, 'id', None), 327 'filename': getattr(att, 'filename', None), 328 'size': getattr(att, 'size', None), 329 'created': getattr(att, 'created', None), 330 'mimeType': getattr(att, 'mimeType', None) 331 } for att in attachments 332 ] 333 # Helper function to process comments 334 def process_comments(comments): 335 if not comments or not hasattr(comments, 'comments'): 336 return [] 337 return [ 338 { 339 'id': getattr(comment, 'id', None), 340 'body': getattr(comment, 'body', None), 341 'author': get_user_dict(comment.author), 342 'created': getattr(comment, 'created', None), 343 'updated': getattr(comment, 'updated', None) 344 } for comment in comments.comments 345 ] 346 # Helper function to process issue links 347 def process_issue_links(issue_links): 348 if not issue_links: 349 return [] 350 def process_issue_reference(issue_ref, direction): 351 if not hasattr(issue_ref, direction) or not getattr(issue_ref, direction): 352 return None 353 ref_issue = getattr(issue_ref, direction) 354 return { 355 'key': getattr(ref_issue, 'key', None), 356 'id': getattr(ref_issue, 'id', None), 357 'fields': { 358 'summary': getattr(ref_issue.fields, 'summary', None), 359 'status': get_object_attrs(ref_issue.fields.status, ['name']) if ref_issue.fields.status else None 360 } 361 } 362 return [ 363 { 364 'id': getattr(link, 'id', None), 365 'type': get_object_attrs(link.type, ['id', 'name', 'inward', 'outward']) if link.type else None, 366 'inwardIssue': process_issue_reference(link, 'inwardIssue'), 367 'outwardIssue': process_issue_reference(link, 'outwardIssue') 368 } for link in issue_links 369 ] 370 # Build response dictionary 371 issue_dict = { 372 'key': issue.key, 373 'id': issue.id 374 } 375 # Determine which fields to process 376 fields_to_process = fields if fields is not None else list(field_mappings.keys()) 377 # Process each field 378 for field in fields_to_process: 379 if field in ['key', 'id']: 380 continue # Already handled 381 field_value = safe_get_field(field_mappings.get(field, field)) 382 match field: 383 case 'summary' | 'description' | 'created' | 'updated' | 'resolutiondate' | 'duedate': 384 issue_dict[field] = field_value 385 case 'status' | 'issuetype' | 'priority': 386 issue_dict[field] = get_object_attrs(field_value, ['id', 'name', 'description']) 387 case 'project': 388 issue_dict[field] = get_object_attrs(field_value, ['key', 'name', 'id']) 389 case 'assignee' | 'reporter': 390 issue_dict[field] = get_user_dict(field_value) 391 case 'labels': 392 issue_dict[field] = list(field_value) if field_value else [] 393 case 'components': 394 issue_dict[field] = [ 395 get_object_attrs(comp, ['id', 'name', 'description']) 396 for comp in (field_value or []) 397 ] 398 case 'attachments': 399 issue_dict[field] = process_attachments(field_value) 400 case 'comments': 401 issue_dict[field] = process_comments(field_value) 402 case 'issuelinks': 403 issue_dict[field] = process_issue_links(field_value) 404 case _: 405 # Handle unknown fields or custom fields 406 issue_dict[field] = field_value 407 # logger.info(f"Retrieved JIRA issue: {issue_key}") 408 return issue_dict 409 except Exception as e: 410 logger.error(f"Failed to get JIRA issue {issue_key}: {str(e)}") 411 logger.traceback(e) 412 return None
Get an issue by its key with specified fields. This method retrieves a JIRA issue using its key and returns the issue details with the specified fields. If no fields are specified, it returns all available fields.
Parameters
issue_key : str The JIRA issue key to retrieve (e.g., "PROJ-123") fields : List[str], optional List of specific fields to retrieve. If None, all fields are returned. Common fields include: "summary", "description", "status", "assignee", "reporter", "created", "updated", "priority", "labels", "components", "attachments", "comments", "issuetype", "project"
Returns
Optional[Dict[str, Any]] A dictionary containing the issue details if successful, None if the issue is not found or an error occurs.
Examples
>>> handler = JiraHandler()
>>> # Get all fields
>>> issue = handler.get_issue("PROJ-123")
>>> print(f"Issue: {issue['summary']} - Status: {issue['status']['name']}")
>>> # Get specific fields only
>>> issue = handler.get_issue("PROJ-123", fields=["summary", "status", "assignee"])
>>> print(f"Assignee: {issue['assignee']['displayName'] if issue['assignee'] else 'Unassigned'}")
<h2 id="notes">Notes</h2>
- The issue key must be valid and accessible with current authentication
- If fields parameter is None, all fields are returned
- Some fields may be None if the issue doesn't have values for them
- Failed operations are logged as errors with relevant details
- The method handles missing issues gracefully by returning None
414 def update_issue_summary(self, issue_key: str, new_summary: str) -> bool: 415 """Update the summary of a JIRA issue. 416 This method updates the summary field of an existing JIRA issue using the JIRA REST API. 417 It validates the input parameters and handles errors gracefully with comprehensive logging. 418 Parameters 419 ---------- 420 issue_key : str 421 The JIRA issue key to update (e.g., "PROJ-123") 422 new_summary : str 423 The new summary text to set for the issue 424 Returns 425 ------- 426 bool 427 True if the summary was successfully updated, False if the operation failed. 428 Returns None if an error occurs during the API request. 429 Examples 430 -------- 431 >>> handler = JiraHandler() 432 >>> success = handler.update_issue_summary("PROJ-123", "Updated bug description") 433 >>> print(success) 434 True 435 Notes 436 ----- 437 - The issue key must be valid and accessible with current authentication 438 - The new summary cannot be empty or None 439 - Failed operations are logged as errors with relevant details 440 - The method uses the JIRA client's update method for efficient API calls 441 """ 442 try: 443 # Validate input parameters 444 if not issue_key or not issue_key.strip(): 445 logger.error("Issue key is required and cannot be empty") 446 return False 447 if not new_summary or not new_summary.strip(): 448 logger.error("New summary is required and cannot be empty") 449 return False 450 # Strip whitespace from inputs 451 issue_key = issue_key.strip() 452 new_summary = new_summary.strip() 453 # logger.info(f"Updating summary for issue {issue_key}") 454 # Get the issue object 455 issue = self.client.issue(issue_key) 456 if not issue: 457 logger.error(f"Issue not found: {issue_key}") 458 return False 459 # Update the summary field 460 issue.update(summary=new_summary) 461 logger.info(f"Successfully updated summary for issue {issue_key}") 462 return True 463 except Exception as e: 464 logger.error(f"Failed to update summary for issue {issue_key}: {str(e)}") 465 logger.traceback(e) 466 return False
Update the summary of a JIRA issue. This method updates the summary field of an existing JIRA issue using the JIRA REST API. It validates the input parameters and handles errors gracefully with comprehensive logging.
Parameters
issue_key : str The JIRA issue key to update (e.g., "PROJ-123") new_summary : str The new summary text to set for the issue
Returns
bool True if the summary was successfully updated, False if the operation failed. Returns None if an error occurs during the API request.
Examples
>>> handler = JiraHandler()
>>> success = handler.update_issue_summary("PROJ-123", "Updated bug description")
>>> print(success)
True
<h2 id="notes">Notes</h2>
- The issue key must be valid and accessible with current authentication
- The new summary cannot be empty or None
- Failed operations are logged as errors with relevant details
- The method uses the JIRA client's update method for efficient API calls
531 def make_jira_request(self, jira_key: str, url: str, method: str = "GET", payload: Dict = None, 532 api_key: str = None, user: str = None, cookie: str = None) -> Optional[Dict]: 533 """ 534 Make a JIRA API request with proper authentication headers. 535 536 This method builds the authentication headers (similar to Postman) and 537 makes the request to the JIRA API. 538 539 Parameters 540 ---------- 541 jira_key : str 542 The JIRA issue key 543 method : str, optional 544 HTTP method (GET, POST, PUT, DELETE). Defaults to "GET" 545 payload : Dict, optional 546 Request payload for POST/PUT requests 547 api_key : str, optional 548 The JIRA API key. If not provided, uses environment variable 549 user : str, optional 550 The JIRA user email. If not provided, uses environment variable 551 cookie : str, optional 552 Additional cookie value 553 554 Returns 555 ------- 556 Optional[Dict] 557 The JSON response from the API, or None if the request fails 558 559 Examples 560 -------- 561 >>> handler = JiraHandler() 562 >>> response = handler.make_jira_request( 563 ... url="https://arusa.atlassian.net/rest/api/2/issue/XSP1-543213456" 564 ... ) 565 >>> print(response) 566 {'id': '12345', 'key': 'XSP1-3456', ...} 567 """ 568 try: 569 if not jira_key: 570 logger.error("JIRA issue key is required") 571 return None 572 573 url = f"{os.getenv('JIRA_SERVER')}/rest/api/2/issue/{jira_key}" 574 if not url: 575 logger.error("JIRA API endpoint URL is required") 576 return None 577 # Build authentication headers 578 headers = self._build_auth_headers(api_key, user, cookie) 579 580 # Make the request 581 response = requests.request(method, url, headers=headers, data=payload) 582 response.raise_for_status() 583 584 # Return JSON response 585 return response.json() 586 587 except requests.exceptions.RequestException as e: 588 logger.error(f"JIRA API request failed: {str(e)}") 589 logger.traceback(e) 590 return None 591 except Exception as e: 592 logger.error(f"Unexpected error in JIRA request: {str(e)}") 593 logger.traceback(e) 594 return None
Make a JIRA API request with proper authentication headers.
This method builds the authentication headers (similar to Postman) and makes the request to the JIRA API.
Parameters
jira_key : str The JIRA issue key method : str, optional HTTP method (GET, POST, PUT, DELETE). Defaults to "GET" payload : Dict, optional Request payload for POST/PUT requests api_key : str, optional The JIRA API key. If not provided, uses environment variable user : str, optional The JIRA user email. If not provided, uses environment variable cookie : str, optional Additional cookie value
Returns
Optional[Dict] The JSON response from the API, or None if the request fails
Examples
>>> handler = JiraHandler()
>>> response = handler.make_jira_request(
... url="https://arusa.atlassian.net/rest/api/2/issue/XSP1-543213456"
... )
>>> print(response)
{'id': '12345', 'key': 'XSP1-3456', ...}
596 def download_jira_attachment_by_id(self, attachment_id: str, mime_type: str) -> Optional[Dict]: 597 ''' 598 Download a JIRA attachment by its ID. 599 ''' 600 try: 601 # ATTACHMENT_URL = f"{os.getenv('JIRA_SERVER')}/rest/api/2/attachment/{attachment_id}" 602 CONTENT_URL = f"{os.getenv('JIRA_SERVER')}/rest/api/2/attachment/content/{attachment_id}" 603 if not CONTENT_URL: 604 logger.error(f"No content URL found for attachment '{attachment_id}'") 605 return None 606 headers = self._build_auth_headers() 607 download_response = requests.get(CONTENT_URL, headers=headers) 608 download_response.raise_for_status() 609 content = download_response.content 610 #Process content based on type 611 result = { 612 'content': content, 613 'mime_type': mime_type, 614 'text_content': None, 615 'json_content': None 616 } 617 618 # Handle text-based files 619 if mime_type.startswith(('text/', 'application/json', 'application/xml' , 'json')): 620 try: 621 text_content = content.decode('utf-8') 622 result['text_content'] = text_content 623 624 # Try to parse as JSON 625 if mime_type == 'application/json': 626 try: 627 result['json_content'] = json.loads(text_content) 628 except json.JSONDecodeError: 629 pass 630 except UnicodeDecodeError: 631 logger.error(f"Warning: Could not decode text content for {attachment_id}") 632 logger.traceback(e) 633 634 return result 635 except Exception as e: 636 logger.error(f"Error downloading JIRA attachment: {str(e)}") 637 logger.traceback(e) 638 return None
Download a JIRA attachment by its ID.
641class XrayGraphQL(JiraHandler): 642 """A comprehensive client for interacting with Xray Cloud's GraphQL API. 643 This class extends JiraHandler to provide specialized methods for interacting with 644 Xray Cloud's GraphQL API for test management. It handles authentication, test plans, 645 test executions, test runs, defects, evidence, and other Xray-related operations 646 through GraphQL queries and mutations. 647 Inherits 648 -------- 649 JiraHandler 650 Base class providing JIRA client functionality and issue management 651 Attributes 652 ---------- 653 client_id : str 654 The client ID for Xray authentication 655 client_secret : str 656 The client secret for Xray authentication 657 xray_base_url : str 658 Base URL for Xray Cloud API (defaults to 'https://us.xray.cloud.getxray.app') 659 logger : Logger 660 Logger instance for debugging and error tracking 661 token : str 662 Authentication token obtained from Xray 663 Methods 664 ------- 665 Authentication & Setup 666 --------------------- 667 __init__() 668 Initialize XrayGraphQL client with authentication and configuration settings. 669 _get_auth_token() -> Optional[str] 670 Authenticate with Xray Cloud API and obtain an authentication token. 671 _make_graphql_request(query: str, variables: Dict) -> Optional[Dict] 672 Makes a GraphQL request to the Xray API with proper authentication. 673 _parse_table(table_str: str) -> Dict[str, Union[List[int], List[float]]] 674 Parse a string representation of a table into a dictionary of numeric values. 675 Issue ID Management 676 ------------------ 677 get_issue_id_from_jira_id(issue_key: str, issue_type: str) -> Optional[str] 678 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 679 Test Plan Operations 680 ------------------- 681 get_tests_from_test_plan(test_plan: str) -> Optional[Dict[str, str]] 682 Retrieves all tests associated with a given test plan. 683 get_test_plan_data(test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]] 684 Retrieves and parses tabular data from a test plan's description. 685 Test Set Operations 686 ------------------ 687 get_tests_from_test_set(test_set: str) -> Optional[Dict[str, str]] 688 Retrieves all tests associated with a given test set. 689 filter_test_set_by_test_case(test_key: str) -> Optional[Dict[str, str]] 690 Retrieves all test sets containing a specific test case. 691 filter_tags_by_test_case(test_key: str) -> Optional[List[str]] 692 Extracts and filters tags from test sets associated with a test case. 693 Test Execution Operations 694 ------------------------ 695 get_tests_from_test_execution(test_execution: str) -> Optional[Dict[str, str]] 696 Retrieves all tests associated with a given test execution. 697 get_test_execution(test_execution: str) -> Optional[Dict] 698 Retrieve detailed information about a test execution from Xray. 699 create_test_execution(test_issue_keys: List[str], project_key: Optional[str], 700 summary: Optional[str], description: Optional[str]) -> Optional[Dict] 701 Creates a new test execution with specified test cases. 702 create_test_execution_from_test_plan(test_plan: str) -> Optional[Dict[str, Dict[str, str]]] 703 Creates a test execution from a given test plan with all associated tests. 704 add_test_execution_to_test_plan(test_plan: str, test_execution: str) -> Optional[Dict] 705 Add a test execution to an existing test plan in Xray. 706 Test Run Operations 707 ------------------ 708 get_test_runstatus(test_case: str, test_execution: str) -> Optional[Tuple[str, str]] 709 Retrieves the status of a test run for a specific test case. 710 get_test_run_by_id(test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]] 711 Retrieves the test run ID and status using internal Xray IDs. 712 update_test_run_status(test_run_id: str, test_run_status: str) -> bool 713 Updates the status of a specific test run. 714 update_test_run_comment(test_run_id: str, test_run_comment: str) -> bool 715 Updates the comment of a specific test run. 716 get_test_run_comment(test_run_id: str) -> Optional[str] 717 Retrieve the comment of a specific test run from Xray. 718 append_test_run_comment(test_run_id: str, test_run_comment: str) -> bool 719 Append a comment to an existing test run comment. 720 Evidence & Defect Management 721 --------------------------- 722 add_evidence_to_test_run(test_run_id: str, evidence_path: str) -> bool 723 Add evidence (attachments) to a test run in Xray. 724 create_defect_from_test_run(test_run_id: str, project_key: str, parent_issue_key: str, 725 defect_summary: str, defect_description: str) -> Optional[Dict] 726 Create a defect from a test run and link it to the test run in Xray. 727 Examples 728 -------- 729 >>> client = XrayGraphQL() 730 >>> test_plan_tests = client.get_tests_from_test_plan("TEST-123") 731 >>> print(test_plan_tests) 732 {'TEST-124': '10001', 'TEST-125': '10002'} 733 >>> test_execution = client.create_test_execution_from_test_plan("TEST-123") 734 >>> print(test_execution) 735 { 736 'TEST-124': { 737 'test_run_id': '5f7c3', 738 'test_execution_key': 'TEST-456', 739 'test_plan_key': 'TEST-123' 740 } 741 } 742 >>> # Update test run status 743 >>> success = client.update_test_run_status("test_run_id", "PASS") 744 >>> print(success) 745 True 746 >>> # Add evidence to test run 747 >>> evidence_added = client.add_evidence_to_test_run("test_run_id", "/path/to/screenshot.png") 748 >>> print(evidence_added) 749 True 750 Notes 751 ----- 752 - Requires valid Xray Cloud credentials (client_id and client_secret) 753 - Uses GraphQL for all API interactions 754 - Implements automatic token refresh 755 - Handles rate limiting and retries 756 - All methods include comprehensive error handling and logging 757 - Returns None for failed operations instead of raising exceptions 758 - Supports various file types for evidence attachments 759 - Integrates with JIRA for defect creation and issue management 760 - Provides both synchronous and asynchronous operation patterns 761 - Includes retry logic for transient failures 762 """ 763 764 def __init__(self): 765 """Initialize XrayGraphQL client with authentication and configuration settings. 766 This constructor sets up the XrayGraphQL client by: 767 1. Loading environment variables from .env file 768 2. Reading required environment variables for authentication 769 3. Configuring the base URL for Xray Cloud 770 4. Obtaining an authentication token 771 Required Environment Variables 772 ---------------------------- 773 XRAY_CLIENT_ID : str 774 Client ID for Xray authentication 775 XRAY_CLIENT_SECRET : str 776 Client secret for Xray authentication 777 XRAY_BASE_URL : str, optional 778 Base URL for Xray Cloud API. Defaults to 'https://us.xray.cloud.getxray.app' 779 Attributes 780 ---------- 781 client_id : str 782 Xray client ID from environment 783 client_secret : str 784 Xray client secret from environment 785 xray_base_url : str 786 Base URL for Xray Cloud API 787 logger : Logger 788 Logger instance for debugging and error tracking 789 token : str 790 Authentication token obtained from Xray 791 Raises 792 ------ 793 Exception 794 If authentication fails or required environment variables are missing 795 """ 796 super().__init__() 797 try: 798 # Load environment variables from .env file 799 self.client_id = os.getenv('XRAY_CLIENT_ID') 800 self.client_secret = os.getenv('XRAY_CLIENT_SECRET') 801 self.xray_base_url = os.getenv('XRAY_BASE_URL', 'https://us.xray.cloud.getxray.app') 802 self.logger = logger 803 # Validate required environment variables 804 if not self.client_id or self.client_id == '<CLIENT_ID>': 805 raise ValueError("XRAY_CLIENT_ID environment variable is required") 806 if not self.client_secret or self.client_secret == '<CLIENT_SECRET>': 807 raise ValueError("XRAY_CLIENT_SECRET environment variable is required") 808 # Get authentication token 809 self.token = self._get_auth_token() 810 if not self.token: 811 logger.error("Failed to authenticate with Xray GraphQL") 812 raise Exception("Failed to initialize XrayGraphQL: No authentication token") 813 except Exception as e: 814 logger.error(f"Error initializing XrayGraphQL: {e}") 815 logger.traceback(e) 816 raise e 817 818 def _get_auth_token(self) -> Optional[str]: 819 """Authenticate with Xray Cloud API and obtain an authentication token. 820 Makes a POST request to the Xray authentication endpoint using the client credentials 821 to obtain a JWT token for subsequent API calls. 822 Returns 823 ------- 824 Optional[str] 825 The authentication token if successful, None if authentication fails. 826 The token is stripped of surrounding quotes before being returned. 827 Raises 828 ------ 829 requests.exceptions.RequestException 830 If the HTTP request fails 831 requests.exceptions.HTTPError 832 If the server returns an error status code 833 Notes 834 ----- 835 - The token is obtained from the Xray Cloud API endpoint /api/v2/authenticate 836 - The method uses client credentials stored in self.client_id and self.client_secret 837 - Failed authentication attempts are logged as errors 838 - Successful authentication is logged at debug level 839 """ 840 try: 841 auth_url = f"{self.xray_base_url}/api/v2/authenticate" 842 auth_headers = {"Content-Type": "application/json"} 843 auth_payload = { 844 "client_id": self.client_id, 845 "client_secret": self.client_secret 846 } 847 # logger.debug("Attempting Xray authentication", "auth_start") 848 logger.debug("Attempting Xray authentication... auth start") 849 response = requests.post(auth_url, headers=auth_headers, json=auth_payload) 850 response.raise_for_status() 851 token = response.text.strip('"') 852 # logger.info("Successfully authenticated with Xray", "auth_success") 853 logger.debug("Successfully authenticated with Xray... auth success again") 854 return token 855 except Exception as e: 856 # logger.error("Xray authentication failed", "auth_failed", error=e) 857 logger.error("Xray authentication failed. auth failed") 858 logger.traceback(e) 859 return None 860 861 def _parse_table(self, table_str: str) -> Dict[str, Union[List[int], List[float]]]: 862 """Parse a string representation of a table into a dictionary of numeric values. 863 Parameters 864 ---------- 865 table_str : str 866 A string containing the table data in markdown-like format. 867 Example format:: 868 header1 || header2 || header3 869 |row1 |value1 |[1, 2, 3]| 870 |row2 |value2 |42 | 871 Returns 872 ------- 873 Dict[str, Union[List[int], List[float]]] 874 A dictionary where: 875 * Keys are strings derived from the first column (lowercase, underscores) 876 * Values are either: 877 * Lists of integers/floats (for array-like values in brackets) 878 * Lists of individual numbers (for single numeric values) 879 For non-array columns, duplicate values are removed and sorted. 880 Returns None if parsing fails. 881 Examples 882 -------- 883 >>> table_str = '''header1 || header2 884 ... |temp |[1, 2, 3]| 885 ... |value |42 |''' 886 >>> result = client._parse_table(table_str) 887 >>> print(result) 888 { 889 'temp': [1, 2, 3], 890 'value': [42] 891 } 892 """ 893 try: 894 # Split the table into lines 895 lines = table_str.strip().split('\n') 896 # Process each data row 897 result = {} 898 for line in lines[1:]: 899 if not line.startswith('|'): 900 continue 901 # Split the row into columns 902 columns = [col.strip() for col in line.split('|')[1:-1]] 903 if not columns: 904 continue 905 key = columns[0].replace(' ', '_').lower() 906 values = [] 907 # Process each value column 908 for col in columns[1:]: 909 # Handle list values 910 if col.startswith('[') and col.endswith(']'): 911 try: 912 # Clean and parse the list 913 list_str = col[1:-1].replace(',', ' ') 914 list_items = [item.strip() for item in list_str.split() if item.strip()] 915 num_list = [float(item) if '.' in item else int(item) for item in list_items] 916 values.append(num_list) 917 except (ValueError, SyntaxError): 918 pass 919 elif col.strip(): # Handle simple numeric values 920 try: 921 num = float(col) if '.' in col else int(col) 922 values.append(num) 923 except ValueError: 924 pass 925 # Store in result 926 if key: 927 if key in result: 928 result[key].extend(values) 929 else: 930 result[key] = values 931 # For temperature (simple values), remove duplicates and sort 932 for key in result: 933 if all(not isinstance(v, list) for v in result[key]): 934 result[key] = sorted(list(set(result[key]))) 935 return result 936 except Exception as e: 937 logger.error(f"Error parsing table: {e}") 938 logger.traceback(e) 939 return None 940 941 def _make_graphql_request(self, query: str, variables: Dict) -> Optional[Dict]: 942 """ 943 Makes a GraphQL request to the Xray API with the provided query and variables. 944 This internal method handles the execution of GraphQL queries against the Xray API, 945 including proper authentication and error handling. 946 Args: 947 query (str): The GraphQL query or mutation to execute 948 variables (Dict): Variables to be used in the GraphQL query/mutation 949 Returns: 950 Optional[Dict]: The 'data' field from the GraphQL response if successful, 951 None if the request fails or contains GraphQL errors 952 Raises: 953 No exceptions are raised - all errors are caught, logged, and return None 954 Example: 955 query = ''' 956 query GetTestPlan($id: String!) { 957 getTestPlan(issueId: $id) { 958 issueId 959 } 960 } 961 ''' 962 variables = {"id": "12345"} 963 result = self._make_graphql_request(query, variables) 964 Note: 965 - Automatically includes authentication token in request headers 966 - Logs errors if the request fails or if GraphQL errors are present 967 - Returns None instead of raising exceptions to allow for graceful error handling 968 - Only returns the 'data' portion of the GraphQL response 969 """ 970 try: 971 graphql_url = f"{self.xray_base_url}/api/v2/graphql" 972 headers = { 973 "Authorization": f"Bearer {self.token}", 974 "Content-Type": "application/json" 975 } 976 payload = {"query": query, "variables": variables} 977 # logger.debug(f'Making GraphQL request "query": {query}, "variables": {variables} ') 978 response = requests.post(graphql_url, headers=headers, json=payload) 979 jprint(response.json()) 980 981 response.raise_for_status() 982 try: 983 data = response.json() 984 jprint(data) 985 except: 986 data = response.text 987 logger.debug(f"Response text: {data}") 988 if 'errors' in data: 989 logger.error(f'GraphQL request failed: {data["errors"]}') 990 return None 991 return data['data'] 992 except Exception as e: 993 logger.error(f"GraphQL request failed due to {e} ") 994 logger.traceback(e) 995 return None 996 997 def get_issue_id_from_jira_id(self, issue_key: str, issue_type: str) -> Optional[str]: 998 """ 999 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 1000 This method queries the Xray GraphQL API to find the internal issue ID corresponding 1001 to a JIRA issue key. It supports different types of Xray artifacts including test plans, 1002 test executions, test sets, and tests. 1003 Args: 1004 issue_key (str): The JIRA issue key (e.g., "PROJECT-123") 1005 issue_type (str): The type of Xray artifact. Supported values are: 1006 - "plan" or contains "plan": For Test Plans 1007 - "exec" or contains "exec": For Test Executions 1008 - "set" or contains "set": For Test Sets 1009 - "test" or contains "test": For Tests 1010 If not provided, defaults to "plan" 1011 Returns: 1012 Optional[str]: The internal Xray issue ID if found, None if: 1013 - The issue key doesn't exist 1014 - The GraphQL request fails 1015 - Any other error occurs during processing 1016 Examples: 1017 >>> client.get_issue_id_from_jira_id("TEST-123", "plan") 1018 '10000' 1019 >>> client.get_issue_id_from_jira_id("TEST-456", "test") 1020 '10001' 1021 >>> client.get_issue_id_from_jira_id("INVALID-789", "plan") 1022 None 1023 Note: 1024 The method performs a case-insensitive comparison when matching issue keys. 1025 The project key is extracted from the issue_key (text before the hyphen) 1026 to filter results by project. 1027 """ 1028 try: 1029 parse_project = issue_key.split("-")[0] 1030 function_name = "getTestPlans" 1031 if not issue_type: 1032 issue_type = "plan" 1033 if "plan" in issue_type.lower(): 1034 function_name = "getTestPlans" 1035 query = """ 1036 query GetIds($limit: Int!, $jql: String!) { 1037 getTestPlans(limit: $limit, jql:$jql) { 1038 results { 1039 issueId 1040 jira(fields: ["key"]) 1041 } 1042 } 1043 } 1044 """ 1045 if "exec" in issue_type.lower(): 1046 function_name = "getTestExecutions" 1047 query = """ 1048 query GetIds($limit: Int!, $jql: String!) { 1049 getTestExecutions(limit: $limit, jql:$jql) { 1050 results { 1051 issueId 1052 jira(fields: ["key"]) 1053 } 1054 } 1055 } 1056 """ 1057 if "set" in issue_type.lower(): 1058 function_name = "getTestSets" 1059 query = """ 1060 query GetIds($limit: Int!, $jql: String!) { 1061 getTestSets(limit: $limit, jql:$jql) { 1062 results { 1063 issueId 1064 jira(fields: ["key"]) 1065 } 1066 } 1067 } 1068 """ 1069 if "test" in issue_type.lower(): 1070 function_name = "getTests" 1071 query = """ 1072 query GetIds($limit: Int!, $jql: String!) { 1073 getTests(limit: $limit, jql:$jql) { 1074 results { 1075 issueId 1076 jira(fields: ["key"]) 1077 } 1078 } 1079 } 1080 """ 1081 variables = { 1082 "limit": 10, 1083 "jql": f"project = '{parse_project}' AND key = '{issue_key}'" 1084 } 1085 data = self._make_graphql_request(query, variables) 1086 if not data: 1087 logger.error(f"Failed to get issue ID for {issue_key}") 1088 return None 1089 for issue in data[function_name]['results']: 1090 if str(issue['jira']['key']).lower() == issue_key.lower(): 1091 return issue['issueId'] 1092 return None 1093 except Exception as e: 1094 logger.error(f"Failed to get issue ID for {issue_key}") 1095 logger.traceback(e) 1096 return None 1097 1098 def get_test_details(self, issue_key: str, issue_type: str) -> Optional[str]: 1099 """ 1100 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 1101 This method queries the Xray GraphQL API to find the internal issue ID corresponding 1102 to a JIRA issue key. It supports different types of Xray artifacts including test plans, 1103 test executions, test sets, and tests. 1104 Args: 1105 issue_key (str): The JIRA issue key (e.g., "PROJECT-123") 1106 issue_type (str): The type of Xray artifact. Supported values are: 1107 - "plan" or contains "plan": For Test Plans 1108 - "exec" or contains "exec": For Test Executions 1109 - "set" or contains "set": For Test Sets 1110 - "test" or contains "test": For Tests 1111 If not provided, defaults to "plan" 1112 Returns: 1113 Optional[str]: The internal Xray issue ID if found, None if: 1114 - The issue key doesn't exist 1115 - The GraphQL request fails 1116 - Any other error occurs during processing 1117 Examples: 1118 >>> client.get_issue_id_from_jira_id("TEST-123", "plan") 1119 '10000' 1120 >>> client.get_issue_id_from_jira_id("TEST-456", "test") 1121 '10001' 1122 >>> client.get_issue_id_from_jira_id("INVALID-789", "plan") 1123 None 1124 Note: 1125 The method performs a case-insensitive comparison when matching issue keys. 1126 The project key is extracted from the issue_key (text before the hyphen) 1127 to filter results by project. 1128 """ 1129 try: 1130 parse_project = issue_key.split("-")[0] 1131 function_name = "getTestPlans" 1132 if not issue_type: 1133 issue_type = "plan" 1134 if "plan" in issue_type.lower(): 1135 function_name = "getTestPlans" 1136 jira_fields = [ 1137 "key", "summary", "description", "assignee", 1138 "status", "priority", "labels", "created", 1139 "updated", "dueDate", "components", "versions", 1140 "attachments", "comments" 1141 ] 1142 query = """ 1143 query GetDetails($limit: Int!, $jql: String!) { 1144 getTestPlans(limit: $limit, jql:$jql) { 1145 results { 1146 issueId 1147 jira(fields: ["key"]) 1148 } 1149 } 1150 } 1151 """ 1152 if "exec" in issue_type.lower(): 1153 function_name = "getTestExecutions" 1154 jira_fields = [ 1155 "key", "summary", "description", "assignee", 1156 "status", "priority", "labels", "created", 1157 "updated", "dueDate", "components", "versions", 1158 "attachments", "comments" 1159 ] 1160 query = """ 1161 query GetDetails($limit: Int!, $jql: String!) { 1162 getTestExecutions(limit: $limit, jql:$jql) { 1163 results { 1164 issueId 1165 jira(fields: ["key"]) 1166 } 1167 } 1168 } 1169 """ 1170 if "set" in issue_type.lower(): 1171 function_name = "getTestSets" 1172 jira_fields = [ 1173 "key", "summary", "description", "assignee", 1174 "status", "priority", "labels", "created", 1175 "updated", "dueDate", "components", "versions", 1176 "attachments", "comments" 1177 ] 1178 query = """ 1179 query GetDetails($limit: Int!, $jql: String!) { 1180 getTestSets(limit: $limit, jql:$jql) { 1181 results { 1182 issueId 1183 jira(fields: ["key"]) 1184 } 1185 } 1186 } 1187 """ 1188 if "test" in issue_type.lower(): 1189 function_name = "getTests" 1190 jira_fields = [ 1191 "key", "summary", "description", "assignee", 1192 "status", "priority", "labels", "created", 1193 "updated", "dueDate", "components", "versions", 1194 "attachments", "comments" 1195 ] 1196 query = """ 1197 query GetDetails($limit: Int!, $jql: String!, $jiraFields: [String!]!) { 1198 getTests(limit: $limit, jql:$jql) { 1199 results { 1200 issueId 1201 jira(fields: $jiraFields) 1202 steps { 1203 id 1204 action 1205 result 1206 attachments { 1207 id 1208 filename 1209 storedInJira 1210 downloadLink 1211 } 1212 } 1213 1214 } 1215 } 1216 } 1217 """ 1218 variables = { 1219 "limit": 10, 1220 "jql": f"project = '{parse_project}' AND key = '{issue_key}'", 1221 "jiraFields": jira_fields 1222 } 1223 data = self._make_graphql_request(query, variables) 1224 if not data: 1225 logger.error(f"Failed to get issue ID for {issue_key}") 1226 return None 1227 for issue in data[function_name]['results']: 1228 if str(issue['jira']['key']).lower() == issue_key.lower(): 1229 return issue # This now includes all metadata 1230 return None 1231 except Exception as e: 1232 logger.error(f"Failed to get issue ID for {issue_key}") 1233 logger.traceback(e) 1234 return None 1235 1236 def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]: 1237 """ 1238 Retrieves all tests associated with a given test plan from Xray. 1239 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1240 test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that 1241 ID to fetch the associated tests. 1242 Args: 1243 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1244 Returns: 1245 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1246 or None if the operation fails. For example: 1247 { 1248 "PROJECT-124": "10001", 1249 "PROJECT-125": "10002" 1250 } 1251 Returns None in the following cases: 1252 - Test plan ID cannot be found 1253 - GraphQL request fails 1254 - Any other error occurs during processing 1255 Example: 1256 >>> client = XrayGraphQL() 1257 >>> tests = client.get_tests_from_test_plan("PROJECT-123") 1258 >>> print(tests) 1259 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1260 Note: 1261 - The method is limited to retrieving 99999 tests per test plan 1262 - Test plan must exist in Xray and be accessible with current authentication 1263 """ 1264 try: 1265 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1266 if not test_plan_id: 1267 logger.error(f"Failed to get test plan ID for {test_plan}") 1268 return None 1269 query = """ 1270 query GetTestPlanTests($testPlanId: String!) { 1271 getTestPlan(issueId: $testPlanId) { 1272 tests(limit: 99999) { 1273 results { 1274 issueId 1275 jira(fields: ["key"]) 1276 } 1277 } 1278 } 1279 } 1280 """ 1281 variables = {"testPlanId": test_plan_id} 1282 data = self._make_graphql_request(query, variables) 1283 if not data: 1284 logger.error(f"Failed to get tests for plan {test_plan_id}") 1285 return None 1286 tests = {} 1287 for test in data['getTestPlan']['tests']['results']: 1288 tests[test['jira']['key']] = test['issueId'] 1289 return tests 1290 except Exception as e: 1291 logger.error(f"Failed to get tests for plan {test_plan_id}") 1292 logger.traceback(e) 1293 return None 1294 1295 def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]: 1296 """ 1297 Retrieves all tests associated with a given test set from Xray. 1298 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1299 test set. It first converts the JIRA test set key to an internal Xray ID, then uses that 1300 ID to fetch the associated tests. 1301 Args: 1302 test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") 1303 Returns: 1304 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1305 or None if the operation fails. For example: 1306 { 1307 "PROJECT-124": "10001", 1308 "PROJECT-125": "10002" 1309 } 1310 Returns None in the following cases: 1311 - Test set ID cannot be found 1312 - GraphQL request fails 1313 - Any other error occurs during processing 1314 Example: 1315 >>> client = XrayGraphQL() 1316 >>> tests = client.get_tests_from_test_set("PROJECT-123") 1317 >>> print(tests) 1318 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1319 Note: 1320 - The method is limited to retrieving 99999 tests per test set 1321 - Test set must exist in Xray and be accessible with current authentication 1322 """ 1323 try: 1324 test_set_id = self.get_issue_id_from_jira_id(test_set, "set") 1325 if not test_set_id: 1326 logger.error(f"Failed to get test set ID for {test_set}") 1327 return None 1328 query = """ 1329 query GetTestSetTests($testSetId: String!) { 1330 getTestSet(issueId: $testSetId) { 1331 tests(limit: 99999) { 1332 results { 1333 issueId 1334 jira(fields: ["key"]) 1335 } 1336 } 1337 } 1338 } 1339 """ 1340 variables = {"testSetId": test_set_id} 1341 data = self._make_graphql_request(query, variables) 1342 if not data: 1343 logger.error(f"Failed to get tests for set {test_set_id}") 1344 return None 1345 tests = {} 1346 for test in data['getTestSet']['tests']['results']: 1347 tests[test['jira']['key']] = test['issueId'] 1348 return tests 1349 except Exception as e: 1350 logger.error(f"Failed to get tests for set {test_set_id}") 1351 logger.traceback(e) 1352 return None 1353 1354 def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]: 1355 """ 1356 Retrieves all tests associated with a given test execution from Xray. 1357 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1358 test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that 1359 ID to fetch the associated tests. 1360 Args: 1361 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1362 Returns: 1363 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1364 or None if the operation fails. For example: 1365 { 1366 "PROJECT-124": "10001", 1367 "PROJECT-125": "10002" 1368 } 1369 Returns None in the following cases: 1370 - Test execution ID cannot be found 1371 - GraphQL request fails 1372 - Any other error occurs during processing 1373 Example: 1374 >>> client = XrayGraphQL() 1375 >>> tests = client.get_tests_from_test_execution("PROJECT-123") 1376 >>> print(tests) 1377 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1378 Note: 1379 - The method is limited to retrieving 99999 tests per test execution 1380 - Test execution must exist in Xray and be accessible with current authentication 1381 """ 1382 try: 1383 test_execution_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1384 if not test_execution_id: 1385 logger.error(f"Failed to get test execution ID for {test_execution}") 1386 return None 1387 query = """ 1388 query GetTestExecutionTests($testExecutionId: String!) { 1389 getTestExecution(issueId: $testExecutionId) { 1390 tests(limit: 100) { 1391 results { 1392 issueId 1393 jira(fields: ["key"]) 1394 } 1395 } 1396 } 1397 } 1398 """ 1399 variables = {"testExecutionId": test_execution_id} 1400 data = self._make_graphql_request(query, variables) 1401 if not data: 1402 logger.error(f"Failed to get tests for execution {test_execution_id}") 1403 return None 1404 tests = {} 1405 for test in data['getTestExecution']['tests']['results']: 1406 tests[test['jira']['key']] = test['issueId'] 1407 return tests 1408 except Exception as e: 1409 logger.error(f"Failed to get tests for execution {test_execution_id}") 1410 logger.traceback(e) 1411 return None 1412 1413 def get_test_plan_data(self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]: 1414 """ 1415 Retrieve and parse tabular data from a test plan's description field in Xray. 1416 This method fetches a test plan's description from Xray and parses any tables found within it. 1417 The tables in the description are expected to be in a specific format that can be parsed by 1418 the _parse_table method. The parsed data is returned as a dictionary containing numeric values 1419 and lists extracted from the table. 1420 Args: 1421 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1422 Returns: 1423 Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data 1424 where keys are derived from the first column of the table and values are lists of numeric 1425 values. Returns None if: 1426 - The test plan ID cannot be found 1427 - The GraphQL request fails 1428 - The description cannot be parsed 1429 - Any other error occurs during processing 1430 Example: 1431 >>> client = XrayGraphQL() 1432 >>> data = client.get_test_plan_data("TEST-123") 1433 >>> print(data) 1434 { 1435 'temperature': [20, 25, 30], 1436 'pressure': [1.0, 1.5, 2.0], 1437 'measurements': [[1, 2, 3], [4, 5, 6]] 1438 } 1439 Note: 1440 - The test plan must exist in Xray and be accessible with current authentication 1441 - The description must contain properly formatted tables for parsing 1442 - Table values are converted to numeric types (int or float) where possible 1443 - Lists in table cells should be formatted as [value1, value2, ...] 1444 - Failed operations are logged as errors with relevant details 1445 """ 1446 try: 1447 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1448 if not test_plan_id: 1449 logger.error(f"Failed to get test plan ID for {test_plan}") 1450 return None 1451 query = """ 1452 query GetTestPlanTests($testPlanId: String!) { 1453 getTestPlan(issueId: $testPlanId) { 1454 issueId 1455 jira(fields: ["key","description"]) 1456 } 1457 } 1458 """ 1459 variables = {"testPlanId": test_plan_id} 1460 data = self._make_graphql_request(query, variables) 1461 if not data: 1462 logger.error(f"Failed to get tests for plan {test_plan_id}") 1463 return None 1464 description = data['getTestPlan']['jira']['description'] 1465 test_plan_data = self._parse_table(description) 1466 return test_plan_data 1467 except Exception as e: 1468 logger.error(f"Failed to get tests for plan {test_plan_id}") 1469 logger.traceback(e) 1470 return None 1471 1472 def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]: 1473 """ 1474 Retrieves all test sets that contain a specific test case from Xray. 1475 This method queries the Xray GraphQL API to find all test sets that include the specified 1476 test case. It first converts the JIRA test case key to an internal Xray ID, then uses that 1477 ID to fetch all associated test sets. 1478 Args: 1479 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1480 Returns: 1481 Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, 1482 or None if the operation fails. For example: 1483 { 1484 "PROJECT-124": "Test Set for Feature A", 1485 "PROJECT-125": "Regression Test Set" 1486 } 1487 Returns None in the following cases: 1488 - Test case ID cannot be found 1489 - GraphQL request fails 1490 - Any other error occurs during processing 1491 Example: 1492 >>> client = XrayGraphQL() 1493 >>> test_sets = client.filter_test_set_by_test_case("PROJECT-123") 1494 >>> print(test_sets) 1495 {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} 1496 Note: 1497 - The method is limited to retrieving 99999 test sets per test case 1498 - Test case must exist in Xray and be accessible with current authentication 1499 """ 1500 try: 1501 test_id = self.get_issue_id_from_jira_id(test_key, "test") 1502 if not test_id: 1503 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1504 return None 1505 query = """ 1506 query GetTestDetails($testId: String!) { 1507 getTest(issueId: $testId) { 1508 testSets(limit: 100) { 1509 results { 1510 issueId 1511 jira(fields: ["key","summary"]) 1512 } 1513 } 1514 } 1515 } 1516 """ 1517 variables = { 1518 "testId": test_id 1519 } 1520 data = self._make_graphql_request(query, variables) 1521 if not data: 1522 logger.error(f"Failed to get tests for plan {test_id}") 1523 return None 1524 retDict = {} 1525 for test in data['getTest']['testSets']['results']: 1526 retDict[test['jira']['key']] = test['jira']['summary'] 1527 return retDict 1528 except Exception as e: 1529 logger.error(f"Error in getting test set by test id: {e}") 1530 logger.traceback(e) 1531 return None 1532 1533 def filter_tags_by_test_case(self, test_key: str) -> Optional[List[str]]: 1534 """ 1535 Extract and filter tags from test sets associated with a specific test case in Xray. 1536 This method queries the Xray GraphQL API to find all test sets associated with the given 1537 test case and extracts tags from their summaries. Tags are identified from test set summaries 1538 that start with either 'tag' or 'benchtype' prefixes. 1539 Args: 1540 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1541 Returns: 1542 Optional[List[str]]: A list of unique tags extracted from test set summaries, 1543 or None if no tags are found or an error occurs. Tags are: 1544 - Extracted from summaries starting with 'tag:' or 'benchtype:' 1545 - Split on commas, semicolons, double pipes, or whitespace 1546 - Converted to lowercase and stripped of whitespace 1547 Example: 1548 >>> client = XrayGraphQL() 1549 >>> tags = client.filter_tags_by_test_case("TEST-123") 1550 >>> print(tags) 1551 ['regression', 'smoke', 'performance'] 1552 Note: 1553 - Test sets must have summaries in the format "tag: tag1, tag2" or "benchtype: type1, type2" 1554 - Tags are extracted only from summaries with the correct prefix 1555 - All tags are converted to lowercase for consistency 1556 - Duplicate tags are automatically removed via set conversion 1557 - Returns None if no valid tags are found or if an error occurs 1558 """ 1559 try: 1560 test_id = self.get_issue_id_from_jira_id(test_key, "test") 1561 if not test_id: 1562 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1563 return None 1564 query = """ 1565 query GetTestDetails($testId: String!) { 1566 getTest(issueId: $testId) { 1567 testSets(limit: 100) { 1568 results { 1569 issueId 1570 jira(fields: ["key","summary"]) 1571 } 1572 } 1573 } 1574 } 1575 """ 1576 variables = { 1577 "testId": test_id 1578 } 1579 data = self._make_graphql_request(query, variables) 1580 if not data: 1581 logger.error(f"Failed to get tests for plan {test_id}") 1582 return None 1583 tags = set() 1584 for test in data['getTest']['testSets']['results']: 1585 summary = str(test['jira']['summary']).strip().lower() 1586 if summary.startswith(('tag', 'benchtype')): 1587 summary = summary.split(':', 1)[-1].strip() # Split only once and take last part 1588 tags.update(tag for tag in re.split(r'[,|;|\|\|]\s*|\s+', summary) if tag) 1589 if tags: 1590 return list(tags) 1591 else: 1592 return None 1593 except Exception as e: 1594 logger.error(f"Error in getting test set by test id: {e}") 1595 logger.traceback(e) 1596 return None 1597 1598 def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]: 1599 """ 1600 Retrieve the status of a test run for a specific test case within a test execution. 1601 This method queries the Xray GraphQL API to get the current status of a test run, 1602 which represents the execution status of a specific test case within a test execution. 1603 It first converts both the test case and test execution JIRA keys to their internal 1604 Xray IDs, then uses these to fetch the test run status. 1605 Args: 1606 test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1607 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") 1608 Returns: 1609 Tuple[Optional[str], Optional[str]]: A tuple containing: 1610 - test_run_id: The unique identifier of the test run (or None if not found) 1611 - test_run_status: The current status of the test run (or None if not found) 1612 Returns (None, None) if any error occurs during the process. 1613 Example: 1614 >>> client = XrayGraphQL() 1615 >>> run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") 1616 >>> print(f"Test Run ID: {run_id}, Status: {status}") 1617 Test Run ID: 10001, Status: PASS 1618 Note: 1619 - Both the test case and test execution must exist in Xray and be accessible 1620 - The test case must be associated with the test execution 1621 - The method performs two ID lookups before querying the test run status 1622 - Failed operations are logged as errors with relevant details 1623 """ 1624 try: 1625 test_case_id = self.get_issue_id_from_jira_id(test_case, "test") 1626 if not test_case_id: 1627 logger.error(f"Failed to get test ID for Test Case ({test_case})") 1628 return None 1629 test_exec_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1630 if not test_exec_id: 1631 logger.error(f"Failed to get test execution ID for Test Execution ({test_execution})") 1632 return None 1633 query = """ 1634 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1635 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1636 id 1637 status { 1638 name 1639 } 1640 } 1641 } 1642 """ 1643 variables = { 1644 "testId": test_case_id, 1645 "testExecutionId": test_exec_id, 1646 } 1647 # Add debug loggerging 1648 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_exec_id}") 1649 data = self._make_graphql_request(query, variables) 1650 if not data: 1651 logger.error(f"Failed to get test run status for test {test_case_id}") 1652 return None 1653 # jprint(data) 1654 test_run_id = data['getTestRun']['id'] 1655 test_run_status = data['getTestRun']['status']['name'] 1656 return (test_run_id, test_run_status) 1657 except Exception as e: 1658 logger.error(f"Error getting test run status: {str(e)}") 1659 logger.traceback(e) 1660 return (None, None) 1661 1662 def get_test_run_by_id(self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]: 1663 """ 1664 Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. 1665 Args: 1666 test_case_id (str): The ID of the test case to query 1667 test_execution_id (str): The ID of the test execution containing the test run 1668 Returns: 1669 tuple[Optional[str], Optional[str]]: A tuple containing: 1670 - test_run_id: The ID of the test run if found, None if not found or on error 1671 - test_run_status: The status name of the test run if found, None if not found or on error 1672 Note: 1673 The function makes a GraphQL request to fetch the test run information. If the request fails 1674 or encounters any errors, it will log the error and return (None, None). 1675 """ 1676 try: 1677 query = """ 1678 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1679 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1680 id 1681 status { 1682 name 1683 } 1684 } 1685 } 1686 """ 1687 variables = { 1688 "testId": test_case_id, 1689 "testExecutionId": test_execution_id, 1690 } 1691 # Add debug loggerging 1692 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_execution_id}") 1693 data = self._make_graphql_request(query, variables) 1694 if not data: 1695 logger.error(f"Failed to get test run status for test {test_case_id}") 1696 return None 1697 test_run_id = data['getTestRun']['id'] 1698 test_run_status = data['getTestRun']['status']['name'] 1699 return (test_run_id, test_run_status) 1700 except Exception as e: 1701 logger.error(f"Error getting test run status: {str(e)}") 1702 logger.traceback(e) 1703 return (None, None) 1704 1705 def get_test_execution(self, test_execution: str) -> Optional[Dict]: 1706 """ 1707 Retrieve detailed information about a test execution from Xray. 1708 This method queries the Xray GraphQL API to fetch information about a specific test execution, 1709 including its ID and associated tests. It first converts the JIRA test execution key to an 1710 internal Xray ID, then uses that ID to fetch the execution details. 1711 Args: 1712 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1713 Returns: 1714 Optional[Dict]: A dictionary containing test execution details if successful, None if failed. 1715 The dictionary has the following structure: 1716 { 1717 'id': str, # The internal Xray ID of the test execution 1718 'tests': { # Dictionary mapping test keys to their IDs 1719 'TEST-124': '10001', 1720 'TEST-125': '10002', 1721 ... 1722 } 1723 } 1724 Returns None in the following cases: 1725 - Test execution ID cannot be found 1726 - GraphQL request fails 1727 - No test execution found with the given ID 1728 - No tests found in the test execution 1729 - Any other error occurs during processing 1730 Example: 1731 >>> client = XrayGraphQL() 1732 >>> execution = client.get_test_execution("TEST-123") 1733 >>> print(execution) 1734 { 1735 'id': '10000', 1736 'tests': { 1737 'TEST-124': '10001', 1738 'TEST-125': '10002' 1739 } 1740 } 1741 Note: 1742 - The method is limited to retrieving 99999 tests per test execution 1743 - Test execution must exist in Xray and be accessible with current authentication 1744 - Failed operations are logged with appropriate error or warning messages 1745 """ 1746 try: 1747 test_execution_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1748 if not test_execution_id: 1749 logger.error(f"Failed to get test execution ID for {test_execution}") 1750 return None 1751 query = """ 1752 query GetTestExecution($testExecutionId: String!) { 1753 getTestExecution(issueId: $testExecutionId) { 1754 issueId 1755 projectId 1756 jira(fields: ["key", "summary", "description", "status"]) 1757 tests(limit: 100) { 1758 total 1759 start 1760 limit 1761 results { 1762 issueId 1763 jira(fields: ["key"]) 1764 } 1765 } 1766 } 1767 } 1768 """ 1769 variables = { 1770 "testExecutionId": test_execution_id 1771 } 1772 # Add debug loggerging 1773 logger.debug(f"Getting test execution details for {test_execution_id}") 1774 data = self._make_graphql_request(query, variables) 1775 # jprint(data) 1776 if not data: 1777 logger.error(f"Failed to get test execution details for {test_execution_id}") 1778 return None 1779 test_execution = data.get('getTestExecution',{}) 1780 if not test_execution: 1781 logger.warning(f"No test execution found with ID {test_execution_id}") 1782 return None 1783 tests = test_execution.get('tests',{}) 1784 if not tests: 1785 logger.warning(f"No tests found for test execution {test_execution_id}") 1786 return None 1787 tests_details = dict() 1788 for test in tests['results']: 1789 tests_details[test['jira']['key']] = test['issueId'] 1790 formatted_response = { 1791 'id': test_execution['issueId'], 1792 'tests': tests_details 1793 } 1794 return formatted_response 1795 except Exception as e: 1796 logger.error(f"Error getting test execution details: {str(e)}") 1797 logger.traceback(e) 1798 return None 1799 1800 def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]: 1801 """ 1802 Add a test execution to an existing test plan in Xray. 1803 This method associates a test execution with a test plan using the Xray GraphQL API. 1804 It first converts both the test plan and test execution JIRA keys to their internal 1805 Xray IDs, then creates the association between them. 1806 Args: 1807 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1808 test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") 1809 Returns: 1810 Optional[Dict]: A dictionary containing the response data if successful, None if failed. 1811 The dictionary has the following structure: 1812 { 1813 'addTestExecutionsToTestPlan': { 1814 'addedTestExecutions': [str], # List of added test execution IDs 1815 'warning': str # Any warnings from the operation 1816 } 1817 } 1818 Returns None in the following cases: 1819 - Test plan ID cannot be found 1820 - Test execution ID cannot be found 1821 - GraphQL request fails 1822 - Any other error occurs during processing 1823 Example: 1824 >>> client = XrayGraphQL() 1825 >>> result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") 1826 >>> print(result) 1827 { 1828 'addTestExecutionsToTestPlan': { 1829 'addedTestExecutions': ['10001'], 1830 'warning': None 1831 } 1832 } 1833 Note: 1834 - Both the test plan and test execution must exist in Xray and be accessible 1835 - The method performs two ID lookups before creating the association 1836 - Failed operations are logged as errors with relevant details 1837 """ 1838 try: 1839 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1840 if not test_plan_id: 1841 logger.error(f"Test plan ID is required") 1842 return None 1843 test_exec_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1844 if not test_exec_id: 1845 logger.error(f"Test execution ID is required") 1846 return None 1847 query = """ 1848 mutation AddTestExecutionToTestPlan($testPlanId: String!, $testExecutionIds: [String!]!) { 1849 addTestExecutionsToTestPlan(issueId: $testPlanId, testExecIssueIds: $testExecutionIds) { 1850 addedTestExecutions 1851 warning 1852 } 1853 } 1854 """ 1855 variables = { 1856 "testPlanId": test_plan_id, 1857 "testExecutionIds": [test_exec_id] 1858 } 1859 data = self._make_graphql_request(query, variables) 1860 return data 1861 except Exception as e: 1862 logger.error(f"Error adding test execution to test plan: {str(e)}") 1863 logger.traceback(e) 1864 return None 1865 1866 def create_test_execution(self, 1867 test_issue_keys: List[str], 1868 project_key: Optional[str] = None, 1869 summary: Optional[str] = None, 1870 description: Optional[str] = None) -> Optional[Dict]: 1871 """ 1872 Create a new test execution in Xray with specified test cases. 1873 This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. 1874 It handles validation of test issue keys, automatically derives project information if not provided, 1875 and creates appropriate default values for summary and description if not specified. 1876 Args: 1877 test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution 1878 (e.g., ["TEST-123", "TEST-124"]) 1879 project_key (Optional[str]): The JIRA project key where the test execution should be created. 1880 If not provided, it will be derived from the first test issue key. 1881 summary (Optional[str]): The summary/title for the test execution ticket. 1882 If not provided, a default summary will be generated using the test issue keys. 1883 description (Optional[str]): The description for the test execution ticket. 1884 If not provided, a default description will be generated using the test issue keys. 1885 Returns: 1886 Optional[Dict]: A dictionary containing the created test execution details if successful, 1887 None if the creation fails. The dictionary has the following structure: 1888 { 1889 'issueId': str, # The internal Xray ID of the created test execution 1890 'jira': { 1891 'key': str # The JIRA issue key of the created test execution 1892 } 1893 } 1894 Example: 1895 >>> client = XrayGraphQL() 1896 >>> test_execution = client.create_test_execution( 1897 ... test_issue_keys=["TEST-123", "TEST-124"], 1898 ... project_key="TEST", 1899 ... summary="Sprint 1 Regression Tests" 1900 ... ) 1901 >>> print(test_execution) 1902 {'issueId': '10001', 'jira': {'key': 'TEST-125'}} 1903 Note: 1904 - Invalid test issue keys are logged as warnings but don't prevent execution creation 1905 - At least one valid test issue key is required 1906 - The method validates each test issue key before creating the execution 1907 - Project key is automatically derived from the first test issue key if not provided 1908 """ 1909 try: 1910 invalid_keys = [] 1911 test_issue_ids = [] 1912 for key in test_issue_keys: 1913 test_issue_id = self.get_issue_id_from_jira_id(key, "test") 1914 if test_issue_id: 1915 test_issue_ids.append(test_issue_id) 1916 else: 1917 invalid_keys.append(key) 1918 if len(test_issue_ids) == 0: 1919 logger.error(f"No valid test issue keys provided: {invalid_keys}") 1920 return None 1921 if len(invalid_keys) > 0: 1922 logger.warning(f"Invalid test issue keys: {invalid_keys}") 1923 if not project_key: 1924 project_key = test_issue_keys[0].split("-")[0] 1925 if not summary: 1926 summary = f"Test Execution for Test Plan {test_issue_keys}" 1927 if not description: 1928 description = f"Test Execution for Test Plan {test_issue_keys}" 1929 mutation = """ 1930 mutation CreateTestExecutionForTestPlan( 1931 $testIssueId_list: [String!]!, 1932 $projectKey: String!, 1933 $summary: String!, 1934 $description: String 1935 ) { 1936 createTestExecution( 1937 testIssueIds: $testIssueId_list, 1938 jira: { 1939 fields: { 1940 project: { key: $projectKey }, 1941 summary: $summary, 1942 description: $description, 1943 issuetype: { name: "Test Execution" } 1944 } 1945 } 1946 ) { 1947 testExecution { 1948 issueId 1949 jira(fields: ["key"]) 1950 } 1951 warnings 1952 } 1953 } 1954 """ 1955 variables = { 1956 "testIssueId_list": test_issue_ids, 1957 "projectKey": project_key, 1958 "summary": summary, 1959 "description": description 1960 } 1961 data = self._make_graphql_request(mutation, variables) 1962 if not data: 1963 return None 1964 execution_details = data['createTestExecution']['testExecution'] 1965 # logger.info(f"Created test execution {execution_details['jira']['key']}") 1966 return execution_details 1967 except Exception as e: 1968 logger.error("Failed to create test execution : {e}") 1969 logger.traceback(e) 1970 return None 1971 1972 def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]: 1973 """Creates a test execution from a given test plan and associates all tests from the plan with the execution. 1974 This method performs several operations in sequence: 1975 1. Retrieves all tests from the specified test plan 1976 2. Creates a new test execution with those tests 1977 3. Associates the new test execution with the original test plan 1978 4. Creates test runs for each test in the execution 1979 Parameters 1980 ---------- 1981 test_plan : str 1982 The JIRA issue key of the test plan (e.g., "PROJECT-123") 1983 Returns 1984 ------- 1985 Optional[Dict[str, Dict[str, str]]] 1986 A dictionary mapping test case keys to their execution details, or None if the operation fails. 1987 The dictionary structure is:: 1988 { 1989 "TEST-123": { # Test case JIRA key 1990 "test_run_id": "12345", # Unique ID for this test run 1991 "test_execution_key": "TEST-456", # JIRA key of the created test execution 1992 "test_plan_key": "TEST-789" # Original test plan JIRA key 1993 }, 1994 "TEST-124": { 1995 ... 1996 } 1997 } 1998 Returns None in the following cases: 1999 * Test plan parameter is empty or invalid 2000 * No tests found in the test plan 2001 * Test execution creation fails 2002 * API request fails 2003 Examples 2004 -------- 2005 >>> client = XrayGraphQL() 2006 >>> result = client.create_test_execution_from_test_plan("TEST-123") 2007 >>> print(result) 2008 { 2009 "TEST-124": { 2010 "test_run_id": "5f7c3", 2011 "test_execution_key": "TEST-456", 2012 "test_plan_key": "TEST-123" 2013 }, 2014 "TEST-125": { 2015 "test_run_id": "5f7c4", 2016 "test_execution_key": "TEST-456", 2017 "test_plan_key": "TEST-123" 2018 } 2019 } 2020 Notes 2021 ----- 2022 - The test plan must exist and be accessible in Xray 2023 - All tests in the test plan must be valid and accessible 2024 - The method automatically generates a summary and description for the test execution 2025 - The created test execution is automatically linked back to the original test plan 2026 """ 2027 try: 2028 if not test_plan: 2029 logger.error("Test plan is required [ jira key]") 2030 return None 2031 project_key = test_plan.split("-")[0] 2032 summary = f"Test Execution for Test Plan {test_plan}" 2033 retDict = dict() 2034 #Get tests from test plan 2035 tests = self.get_tests_from_test_plan(test_plan) 2036 retDict["tests"] = tests 2037 testIssueId_list = list(tests.values()) 2038 # logger.info(f"Tests: {tests}") 2039 if not testIssueId_list: 2040 logger.error(f"No tests found for {test_plan}") 2041 return None 2042 description = f"Test Execution for {len(tests)} Test cases" 2043 # GraphQL mutation to create test execution 2044 query = """ 2045 mutation CreateTestExecutionForTestPlan( 2046 $testIssueId_list: [String!]!, 2047 $projectKey: String!, 2048 $summary: String!, 2049 $description: String 2050 ) { 2051 createTestExecution( 2052 testIssueIds: $testIssueId_list, 2053 jira: { 2054 fields: { 2055 project: { key: $projectKey }, 2056 summary: $summary, 2057 description: $description, 2058 issuetype: { name: "Test Execution" } 2059 } 2060 } 2061 ) { 2062 testExecution { 2063 issueId 2064 jira(fields: ["key"]) 2065 testRuns(limit: 100) { 2066 results { 2067 id 2068 test { 2069 issueId 2070 jira(fields: ["key"]) 2071 } 2072 } 2073 } 2074 } 2075 warnings 2076 } 2077 } 2078 """ 2079 variables = { 2080 "testIssueId_list": testIssueId_list, 2081 "projectKey": project_key, 2082 "summary": summary, 2083 "description": description or f"Test execution for total of {len(testIssueId_list)} test cases" 2084 } 2085 data = self._make_graphql_request(query, variables) 2086 if not data: 2087 return None 2088 test_exec_key = data['createTestExecution']['testExecution']['jira']['key'] 2089 test_exec_id = data['createTestExecution']['testExecution']['issueId'] 2090 #Add Test execution to test plan 2091 test_exec_dict= self.add_test_execution_to_test_plan(test_plan, test_exec_key) 2092 #Get test runs for test execution 2093 test_runs = data['createTestExecution']['testExecution']['testRuns']['results'] 2094 test_run_dict = dict() 2095 for test_run in test_runs: 2096 test_run_dict[test_run['test']['jira']['key']] = dict() 2097 test_run_dict[test_run['test']['jira']['key']]['test_run_id'] = test_run['id'] 2098 # test_run_dict[test_run['test']['jira']['key']]['test_issue_id'] = test_run['test']['issueId'] 2099 test_run_dict[test_run['test']['jira']['key']]['test_execution_key'] = test_exec_key 2100 # test_run_dict[test_run['test']['jira']['key']]['test_execution_id'] = test_exec_id 2101 test_run_dict[test_run['test']['jira']['key']]['test_plan_key'] = test_plan 2102 return test_run_dict 2103 except requests.exceptions.RequestException as e: 2104 logger.error(f"Error creating test execution: {e}") 2105 logger.traceback(e) 2106 return None 2107 2108 def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool: 2109 """ 2110 Update the status of a specific test run in Xray using the GraphQL API. 2111 This method allows updating the execution status of a test run identified by its ID. 2112 The status can be changed to reflect the current state of the test execution 2113 (e.g., "PASS", "FAIL", "TODO", etc.). 2114 Args: 2115 test_run_id (str): The unique identifier of the test run to update. 2116 This is the internal Xray ID for the test run, not the Jira issue key. 2117 test_run_status (str): The new status to set for the test run. 2118 Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. 2119 Returns: 2120 bool: True if the status update was successful, False otherwise. 2121 Returns None if an error occurs during the API request. 2122 Example: 2123 >>> client = XrayGraphQL() 2124 >>> test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") 2125 >>> success = client.update_test_run_status(test_run_id, "PASS") 2126 >>> print(success) 2127 True 2128 Note: 2129 - The test run ID must be valid and accessible with current authentication 2130 - The status value should be one of the valid status values configured in your Xray instance 2131 - Failed updates are logged as errors with details about the failure 2132 Raises: 2133 Exception: If there is an error making the GraphQL request or processing the response. 2134 The exception is caught and logged, and the method returns None. 2135 """ 2136 try: 2137 query = """ 2138 mutation UpdateTestRunStatus($testRunId: String!, $status: String!) { 2139 updateTestRunStatus( 2140 id: $testRunId, 2141 status: $status 2142 ) 2143 } 2144 """ 2145 variables = { 2146 "testRunId": test_run_id, 2147 "status": test_run_status 2148 } 2149 data = self._make_graphql_request(query, variables) 2150 if not data: 2151 logger.error(f"Failed to get test run status for test {data}") 2152 return None 2153 # logger.info(f"Test run status updated: {data}") 2154 return data['updateTestRunStatus'] 2155 except Exception as e: 2156 logger.error(f"Error updating test run status: {str(e)}") 2157 logger.traceback(e) 2158 return None 2159 2160 def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2161 """ 2162 Update the comment of a specific test run in Xray using the GraphQL API. 2163 This method allows adding or updating the comment associated with a test run 2164 identified by its ID. The comment can provide additional context, test results, 2165 or any other relevant information about the test execution. 2166 Args: 2167 test_run_id (str): The unique identifier of the test run to update. 2168 This is the internal Xray ID for the test run, not the Jira issue key. 2169 test_run_comment (str): The new comment text to set for the test run. 2170 This will replace any existing comment on the test run. 2171 Returns: 2172 bool: True if the comment update was successful, False otherwise. 2173 Returns None if an error occurs during the API request. 2174 Example: 2175 >>> client = XrayGraphQL() 2176 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2177 >>> success = client.update_test_run_comment( 2178 ... test_run_id, 2179 ... "Test passed with performance within expected range" 2180 ... ) 2181 >>> print(success) 2182 True 2183 Note: 2184 - The test run ID must be valid and accessible with current authentication 2185 - The comment can include any text content, including newlines and special characters 2186 - Failed updates are logged as errors with details about the failure 2187 - This method will overwrite any existing comment on the test run 2188 Raises: 2189 Exception: If there is an error making the GraphQL request or processing the response. 2190 The exception is caught and logged, and the method returns None. 2191 """ 2192 try: 2193 query = """ 2194 mutation UpdateTestRunStatus($testRunId: String!, $comment: String!) { 2195 updateTestRunComment( 2196 id: $testRunId, 2197 comment: $comment 2198 ) 2199 } 2200 """ 2201 variables = { 2202 "testRunId": test_run_id, 2203 "comment": test_run_comment 2204 } 2205 data = self._make_graphql_request(query, variables) 2206 if not data: 2207 logger.error(f"Failed to get test run comment for test {data}") 2208 return None 2209 # jprint(data) 2210 return data['updateTestRunComment'] 2211 except Exception as e: 2212 logger.error(f"Error updating test run comment: {str(e)}") 2213 logger.traceback(e) 2214 return None 2215 2216 def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool: 2217 """Add evidence (attachments) to a test run in Xray. 2218 This method allows attaching files as evidence to a specific test run. The file is 2219 read, converted to base64, and uploaded to Xray with appropriate MIME type detection. 2220 Parameters 2221 ---------- 2222 test_run_id : str 2223 The unique identifier of the test run to add evidence to 2224 evidence_path : str 2225 The local file system path to the evidence file to be attached 2226 Returns 2227 ------- 2228 bool 2229 True if the evidence was successfully added, None if the operation failed. 2230 Returns None in the following cases: 2231 - Test run ID is not provided 2232 - Evidence path is not provided 2233 - Evidence file does not exist 2234 - GraphQL request fails 2235 - Any other error occurs during processing 2236 Examples 2237 -------- 2238 >>> client = XrayGraphQL() 2239 >>> success = client.add_evidence_to_test_run( 2240 ... test_run_id="10001", 2241 ... evidence_path="/path/to/screenshot.png" 2242 ... ) 2243 >>> print(success) 2244 True 2245 Notes 2246 ----- 2247 - The evidence file must exist and be accessible 2248 - The file is automatically converted to base64 for upload 2249 - MIME type is automatically detected, defaults to "text/plain" if detection fails 2250 - The method supports various file types (images, documents, logs, etc.) 2251 - Failed operations are logged with appropriate error messages 2252 """ 2253 try: 2254 if not test_run_id: 2255 logger.error("Test run ID is required") 2256 return None 2257 if not evidence_path: 2258 logger.error("Evidence path is required") 2259 return None 2260 if not os.path.exists(evidence_path): 2261 logger.error(f"Evidence file not found: {evidence_path}") 2262 return None 2263 #if file exists then read the file in base64 2264 evidence_base64 = None 2265 mime_type = None 2266 filename = os.path.basename(evidence_path) 2267 with open(evidence_path, "rb") as file: 2268 evidence_data = file.read() 2269 evidence_base64 = base64.b64encode(evidence_data).decode('utf-8') 2270 mime_type = mimetypes.guess_type(evidence_path)[0] 2271 logger.info(f"For loop -- Mime type: {mime_type}") 2272 if not mime_type: 2273 mime_type = "text/plain" 2274 query = """ 2275 mutation AddEvidenceToTestRun($testRunId: String!, $filename: String!, $mimeType: String!, $evidenceBase64: String!) { 2276 addEvidenceToTestRun( 2277 id: $testRunId, 2278 evidence: [ 2279 { 2280 filename : $filename, 2281 mimeType : $mimeType, 2282 data : $evidenceBase64 2283 } 2284 ] 2285 ) { 2286 addedEvidence 2287 warnings 2288 } 2289 } 2290 """ 2291 variables = { 2292 "testRunId": test_run_id, 2293 "filename": filename, 2294 "mimeType": mime_type, 2295 "evidenceBase64": evidence_base64 2296 } 2297 data = self._make_graphql_request(query, variables) 2298 if not data: 2299 logger.error(f"Failed to add evidence to test run: {data}") 2300 return None 2301 return data['addEvidenceToTestRun'] 2302 except Exception as e: 2303 logger.error(f"Error adding evidence to test run: {str(e)}") 2304 logger.traceback(e) 2305 return None 2306 2307 def create_defect_from_test_run(self, test_run_id: str, project_key: str, parent_issue_key: str, defect_summary: str, defect_description: str) -> Optional[Dict]: 2308 """Create a defect from a test run and link it to the test run in Xray. 2309 This method performs two main operations: 2310 1. Creates a new defect in JIRA with the specified summary and description 2311 2. Links the created defect to the specified test run in Xray 2312 Parameters 2313 ---------- 2314 test_run_id : str 2315 The ID of the test run to create defect from 2316 project_key : str 2317 The JIRA project key where the defect should be created. 2318 If not provided, defaults to "EAGVAL" 2319 parent_issue_key : str 2320 The JIRA key of the parent issue to link the defect to 2321 defect_summary : str 2322 Summary/title of the defect. 2323 If not provided, defaults to "Please provide a summary for the defect" 2324 defect_description : str 2325 Description of the defect. 2326 If not provided, defaults to "Please provide a description for the defect" 2327 Returns 2328 ------- 2329 Optional[Dict] 2330 Response data from the GraphQL API if successful, None if failed. 2331 The response includes: 2332 - addedDefects: List of added defects 2333 - warnings: Any warnings from the operation 2334 Examples 2335 -------- 2336 >>> client = XrayGraphQL() 2337 >>> result = client.create_defect_from_test_run( 2338 ... test_run_id="10001", 2339 ... project_key="PROJ", 2340 ... parent_issue_key="PROJ-456", 2341 ... defect_summary="Test failure in login flow", 2342 ... defect_description="The login button is not responding to clicks" 2343 ... ) 2344 >>> print(result) 2345 { 2346 'addedDefects': ['PROJ-123'], 2347 'warnings': [] 2348 } 2349 Notes 2350 ----- 2351 - The project_key will be split on '-' and only the first part will be used 2352 - The defect will be created with issue type 'Bug' 2353 - The method handles missing parameters with default values 2354 - The parent issue must exist and be accessible to create the defect 2355 """ 2356 try: 2357 if not project_key: 2358 project_key = "EAGVAL" 2359 if not defect_summary: 2360 defect_summary = "Please provide a summary for the defect" 2361 if not defect_description: 2362 defect_description = "Please provide a description for the defect" 2363 project_key = project_key.split("-")[0] 2364 # Fix: Correct parameter order for create_issue 2365 defect_key, defect_id = self.create_issue( 2366 project_key=project_key, 2367 parent_issue_key=parent_issue_key, 2368 summary=defect_summary, 2369 description=defect_description, 2370 issue_type='Bug' 2371 ) 2372 if not defect_key: 2373 logger.error("Failed to create defect issue") 2374 return None 2375 # Then add the defect to the test run 2376 add_defect_mutation = """ 2377 mutation AddDefectsToTestRun( 2378 $testRunId: String!, 2379 $defectKey: String! 2380 ) { 2381 addDefectsToTestRun( id: $testRunId, issues: [$defectKey]) { 2382 addedDefects 2383 warnings 2384 } 2385 } 2386 """ 2387 variables = { 2388 "testRunId": test_run_id, 2389 "defectKey": defect_key 2390 } 2391 data = None 2392 retry_count = 0 2393 while retry_count < 3: 2394 data = self._make_graphql_request(add_defect_mutation, variables) 2395 if not data: 2396 logger.error(f"Failed to add defect [{defect_key}] to test run - {test_run_id}.. retrying... {retry_count}") 2397 retry_count += 1 2398 time.sleep(1) 2399 else: 2400 break 2401 return data 2402 except Exception as e: 2403 logger.error(f"Error creating defect from test run: {str(e)}") 2404 logger.traceback(e) 2405 return None 2406 2407 def get_test_run_comment(self, test_run_id: str) -> Optional[str]: 2408 """ 2409 Retrieve the comment of a specific test run from Xray using the GraphQL API. 2410 This method allows retrieving the comment associated with a test run 2411 identified by its ID. The comment can provide additional context, test results, 2412 or any other relevant information about the test execution. 2413 Args: 2414 test_run_id (str): The unique identifier of the test run to retrieve comment from. 2415 This is the internal Xray ID for the test run, not the Jira issue key. 2416 Returns: 2417 Optional[str]: The comment text of the test run if successful, None if: 2418 - The test run ID is not found 2419 - The GraphQL request fails 2420 - No comment exists for the test run 2421 - Any other error occurs during the API request 2422 Example: 2423 >>> client = XrayGraphQL() 2424 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2425 >>> comment = client.get_test_run_comment(test_run_id) 2426 >>> print(comment) 2427 "Test passed with performance within expected range" 2428 Note: 2429 - The test run ID must be valid and accessible with current authentication 2430 - If no comment exists for the test run, the method will return None 2431 - Failed requests are logged as errors with details about the failure 2432 - The method returns the raw comment text as stored in Xray 2433 Raises: 2434 Exception: If there is an error making the GraphQL request or processing the response. 2435 The exception is caught and logged, and the method returns None. 2436 """ 2437 try: 2438 # Try the direct ID approach first 2439 query = """ 2440 query GetTestRunComment($testRunId: String!) { 2441 getTestRunById(id: $testRunId) { 2442 id 2443 comment 2444 status { 2445 name 2446 } 2447 } 2448 } 2449 """ 2450 variables = { 2451 "testRunId": test_run_id 2452 } 2453 data = self._make_graphql_request(query, variables) 2454 jprint(data) 2455 if not data: 2456 logger.error(f"Failed to get test run comment for test run {test_run_id}") 2457 return None 2458 test_run = data.get('getTestRunById', {}) 2459 if not test_run: 2460 logger.warning(f"No test run found with ID {test_run_id}") 2461 return None 2462 comment = test_run.get('comment') 2463 return comment 2464 except Exception as e: 2465 logger.error(f"Error getting test run comment: {str(e)}") 2466 logger.traceback(e) 2467 return None 2468 2469 def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2470 """ 2471 Append the comment of a specific test run in Xray using the GraphQL API. 2472 This method allows appending the comment associated with a test run 2473 identified by its ID. The comment can provide additional context, test results, 2474 or any other relevant information about the test execution. 2475 Args: 2476 test_run_id (str): The unique identifier of the test run to update. 2477 This is the internal Xray ID for the test run, not the Jira issue key. 2478 test_run_comment (str): The comment text to append to the test run. 2479 This will be added to any existing comment on the test run with proper formatting. 2480 Returns: 2481 bool: True if the comment update was successful, False otherwise. 2482 Returns None if an error occurs during the API request. 2483 Example: 2484 >>> client = XrayGraphQL() 2485 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2486 >>> success = client.append_test_run_comment( 2487 ... test_run_id, 2488 ... "Test passed with performance within expected range" 2489 ... ) 2490 >>> print(success) 2491 True 2492 Note: 2493 - The test run ID must be valid and accessible with current authentication 2494 - The comment can include any text content, including newlines and special characters 2495 - Failed updates are logged as errors with details about the failure 2496 - This method will append to existing comments with proper line breaks 2497 - If no existing comment exists, the new comment will be set as the initial comment 2498 Raises: 2499 Exception: If there is an error making the GraphQL request or processing the response. 2500 The exception is caught and logged, and the method returns None. 2501 """ 2502 try: 2503 # Get existing comment 2504 existing_comment = self.get_test_run_comment(test_run_id) 2505 # Prepare the combined comment with proper formatting 2506 if existing_comment: 2507 # If there's an existing comment, append with double newline for proper separation 2508 combined_comment = f"{existing_comment}\n{test_run_comment}" 2509 else: 2510 # If no existing comment, use the new comment as is 2511 combined_comment = test_run_comment 2512 logger.debug(f"No existing comment found for test run {test_run_id}, setting initial comment") 2513 query = """ 2514 mutation UpdateTestRunComment($testRunId: String!, $comment: String!) { 2515 updateTestRunComment( 2516 id: $testRunId, 2517 comment: $comment 2518 ) 2519 } 2520 """ 2521 variables = { 2522 "testRunId": test_run_id, 2523 "comment": combined_comment 2524 } 2525 data = self._make_graphql_request(query, variables) 2526 if not data: 2527 logger.error(f"Failed to update test run comment for test run {test_run_id}") 2528 return None 2529 return data['updateTestRunComment'] 2530 except Exception as e: 2531 logger.error(f"Error updating test run comment: {str(e)}") 2532 logger.traceback(e) 2533 return None 2534 2535 def download_attachment(self, jira_key: str, file_extension: str) -> Optional[Dict]: 2536 ''' 2537 Download a JIRA attachment by its ID. 2538 ''' 2539 try: 2540 response = self.make_jira_request(jira_key, 'GET') 2541 2542 if not response or 'fields' not in response: 2543 logger.error(f"Error: Could not retrieve issue {jira_key}") 2544 return None 2545 2546 # Find attachment by filename 2547 attachments = response.get('fields', {}).get('attachment', []) 2548 target_attachment = [att for att in attachments if att.get('filename').endswith(file_extension)] 2549 if not target_attachment: 2550 logger.error(f"No attachment found for {jira_key} with extension {file_extension}") 2551 return None 2552 2553 combined_attachment = [] 2554 for attachment in target_attachment: 2555 attachment_id = attachment.get('id') 2556 mime_type = attachment.get('mimeType', '') 2557 combined_attachment.append(self.download_jira_attachment_by_id(attachment_id, mime_type)) 2558 2559 return combined_attachment 2560 except Exception as e: 2561 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2562 logger.traceback(e) 2563 return None
A comprehensive client for interacting with Xray Cloud's GraphQL API. This class extends JiraHandler to provide specialized methods for interacting with Xray Cloud's GraphQL API for test management. It handles authentication, test plans, test executions, test runs, defects, evidence, and other Xray-related operations through GraphQL queries and mutations.
Inherits
JiraHandler Base class providing JIRA client functionality and issue management
Attributes
client_id : str The client ID for Xray authentication client_secret : str The client secret for Xray authentication xray_base_url : str Base URL for Xray Cloud API (defaults to 'https://us.xray.cloud.getxray.app') logger : Logger Logger instance for debugging and error tracking token : str Authentication token obtained from Xray
Methods
Authentication & Setup
__init__() Initialize XrayGraphQL client with authentication and configuration settings. _get_auth_token() -> Optional[str] Authenticate with Xray Cloud API and obtain an authentication token. _make_graphql_request(query: str, variables: Dict) -> Optional[Dict] Makes a GraphQL request to the Xray API with proper authentication. _parse_table(table_str: str) -> Dict[str, Union[List[int], List[float]]] Parse a string representation of a table into a dictionary of numeric values.
Issue ID Management
get_issue_id_from_jira_id(issue_key: str, issue_type: str) -> Optional[str] Retrieves the internal Xray issue ID for a given JIRA issue key and type.
Test Plan Operations
get_tests_from_test_plan(test_plan: str) -> Optional[Dict[str, str]] Retrieves all tests associated with a given test plan. get_test_plan_data(test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]] Retrieves and parses tabular data from a test plan's description.
Test Set Operations
get_tests_from_test_set(test_set: str) -> Optional[Dict[str, str]] Retrieves all tests associated with a given test set. filter_test_set_by_test_case(test_key: str) -> Optional[Dict[str, str]] Retrieves all test sets containing a specific test case. filter_tags_by_test_case(test_key: str) -> Optional[List[str]] Extracts and filters tags from test sets associated with a test case.
Test Execution Operations
get_tests_from_test_execution(test_execution: str) -> Optional[Dict[str, str]] Retrieves all tests associated with a given test execution. get_test_execution(test_execution: str) -> Optional[Dict] Retrieve detailed information about a test execution from Xray. create_test_execution(test_issue_keys: List[str], project_key: Optional[str], summary: Optional[str], description: Optional[str]) -> Optional[Dict] Creates a new test execution with specified test cases. create_test_execution_from_test_plan(test_plan: str) -> Optional[Dict[str, Dict[str, str]]] Creates a test execution from a given test plan with all associated tests. add_test_execution_to_test_plan(test_plan: str, test_execution: str) -> Optional[Dict] Add a test execution to an existing test plan in Xray.
Test Run Operations
get_test_runstatus(test_case: str, test_execution: str) -> Optional[Tuple[str, str]] Retrieves the status of a test run for a specific test case. get_test_run_by_id(test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]] Retrieves the test run ID and status using internal Xray IDs. update_test_run_status(test_run_id: str, test_run_status: str) -> bool Updates the status of a specific test run. update_test_run_comment(test_run_id: str, test_run_comment: str) -> bool Updates the comment of a specific test run. get_test_run_comment(test_run_id: str) -> Optional[str] Retrieve the comment of a specific test run from Xray. append_test_run_comment(test_run_id: str, test_run_comment: str) -> bool Append a comment to an existing test run comment.
Evidence & Defect Management
add_evidence_to_test_run(test_run_id: str, evidence_path: str) -> bool Add evidence (attachments) to a test run in Xray. create_defect_from_test_run(test_run_id: str, project_key: str, parent_issue_key: str, defect_summary: str, defect_description: str) -> Optional[Dict] Create a defect from a test run and link it to the test run in Xray.
Examples
>>> client = XrayGraphQL()
>>> test_plan_tests = client.get_tests_from_test_plan("TEST-123")
>>> print(test_plan_tests)
{'TEST-124': '10001', 'TEST-125': '10002'}
>>> test_execution = client.create_test_execution_from_test_plan("TEST-123")
>>> print(test_execution)
{
'TEST-124': {
'test_run_id': '5f7c3',
'test_execution_key': 'TEST-456',
'test_plan_key': 'TEST-123'
}
}
>>> # Update test run status
>>> success = client.update_test_run_status("test_run_id", "PASS")
>>> print(success)
True
>>> # Add evidence to test run
>>> evidence_added = client.add_evidence_to_test_run("test_run_id", "/path/to/screenshot.png")
>>> print(evidence_added)
True
<h2 id="notes">Notes</h2>
- Requires valid Xray Cloud credentials (client_id and client_secret)
- Uses GraphQL for all API interactions
- Implements automatic token refresh
- Handles rate limiting and retries
- All methods include comprehensive error handling and logging
- Returns None for failed operations instead of raising exceptions
- Supports various file types for evidence attachments
- Integrates with JIRA for defect creation and issue management
- Provides both synchronous and asynchronous operation patterns
- Includes retry logic for transient failures
764 def __init__(self): 765 """Initialize XrayGraphQL client with authentication and configuration settings. 766 This constructor sets up the XrayGraphQL client by: 767 1. Loading environment variables from .env file 768 2. Reading required environment variables for authentication 769 3. Configuring the base URL for Xray Cloud 770 4. Obtaining an authentication token 771 Required Environment Variables 772 ---------------------------- 773 XRAY_CLIENT_ID : str 774 Client ID for Xray authentication 775 XRAY_CLIENT_SECRET : str 776 Client secret for Xray authentication 777 XRAY_BASE_URL : str, optional 778 Base URL for Xray Cloud API. Defaults to 'https://us.xray.cloud.getxray.app' 779 Attributes 780 ---------- 781 client_id : str 782 Xray client ID from environment 783 client_secret : str 784 Xray client secret from environment 785 xray_base_url : str 786 Base URL for Xray Cloud API 787 logger : Logger 788 Logger instance for debugging and error tracking 789 token : str 790 Authentication token obtained from Xray 791 Raises 792 ------ 793 Exception 794 If authentication fails or required environment variables are missing 795 """ 796 super().__init__() 797 try: 798 # Load environment variables from .env file 799 self.client_id = os.getenv('XRAY_CLIENT_ID') 800 self.client_secret = os.getenv('XRAY_CLIENT_SECRET') 801 self.xray_base_url = os.getenv('XRAY_BASE_URL', 'https://us.xray.cloud.getxray.app') 802 self.logger = logger 803 # Validate required environment variables 804 if not self.client_id or self.client_id == '<CLIENT_ID>': 805 raise ValueError("XRAY_CLIENT_ID environment variable is required") 806 if not self.client_secret or self.client_secret == '<CLIENT_SECRET>': 807 raise ValueError("XRAY_CLIENT_SECRET environment variable is required") 808 # Get authentication token 809 self.token = self._get_auth_token() 810 if not self.token: 811 logger.error("Failed to authenticate with Xray GraphQL") 812 raise Exception("Failed to initialize XrayGraphQL: No authentication token") 813 except Exception as e: 814 logger.error(f"Error initializing XrayGraphQL: {e}") 815 logger.traceback(e) 816 raise e
Initialize XrayGraphQL client with authentication and configuration settings.
This constructor sets up the XrayGraphQL client by:
- Loading environment variables from .env file
- Reading required environment variables for authentication
- Configuring the base URL for Xray Cloud
- Obtaining an authentication token
Required Environment Variables
XRAY_CLIENT_ID : str Client ID for Xray authentication XRAY_CLIENT_SECRET : str Client secret for Xray authentication XRAY_BASE_URL : str, optional Base URL for Xray Cloud API. Defaults to 'https://us.xray.cloud.getxray.app'
Attributes
client_id : str Xray client ID from environment client_secret : str Xray client secret from environment xray_base_url : str Base URL for Xray Cloud API logger : Logger Logger instance for debugging and error tracking token : str Authentication token obtained from Xray
Raises
Exception If authentication fails or required environment variables are missing
997 def get_issue_id_from_jira_id(self, issue_key: str, issue_type: str) -> Optional[str]: 998 """ 999 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 1000 This method queries the Xray GraphQL API to find the internal issue ID corresponding 1001 to a JIRA issue key. It supports different types of Xray artifacts including test plans, 1002 test executions, test sets, and tests. 1003 Args: 1004 issue_key (str): The JIRA issue key (e.g., "PROJECT-123") 1005 issue_type (str): The type of Xray artifact. Supported values are: 1006 - "plan" or contains "plan": For Test Plans 1007 - "exec" or contains "exec": For Test Executions 1008 - "set" or contains "set": For Test Sets 1009 - "test" or contains "test": For Tests 1010 If not provided, defaults to "plan" 1011 Returns: 1012 Optional[str]: The internal Xray issue ID if found, None if: 1013 - The issue key doesn't exist 1014 - The GraphQL request fails 1015 - Any other error occurs during processing 1016 Examples: 1017 >>> client.get_issue_id_from_jira_id("TEST-123", "plan") 1018 '10000' 1019 >>> client.get_issue_id_from_jira_id("TEST-456", "test") 1020 '10001' 1021 >>> client.get_issue_id_from_jira_id("INVALID-789", "plan") 1022 None 1023 Note: 1024 The method performs a case-insensitive comparison when matching issue keys. 1025 The project key is extracted from the issue_key (text before the hyphen) 1026 to filter results by project. 1027 """ 1028 try: 1029 parse_project = issue_key.split("-")[0] 1030 function_name = "getTestPlans" 1031 if not issue_type: 1032 issue_type = "plan" 1033 if "plan" in issue_type.lower(): 1034 function_name = "getTestPlans" 1035 query = """ 1036 query GetIds($limit: Int!, $jql: String!) { 1037 getTestPlans(limit: $limit, jql:$jql) { 1038 results { 1039 issueId 1040 jira(fields: ["key"]) 1041 } 1042 } 1043 } 1044 """ 1045 if "exec" in issue_type.lower(): 1046 function_name = "getTestExecutions" 1047 query = """ 1048 query GetIds($limit: Int!, $jql: String!) { 1049 getTestExecutions(limit: $limit, jql:$jql) { 1050 results { 1051 issueId 1052 jira(fields: ["key"]) 1053 } 1054 } 1055 } 1056 """ 1057 if "set" in issue_type.lower(): 1058 function_name = "getTestSets" 1059 query = """ 1060 query GetIds($limit: Int!, $jql: String!) { 1061 getTestSets(limit: $limit, jql:$jql) { 1062 results { 1063 issueId 1064 jira(fields: ["key"]) 1065 } 1066 } 1067 } 1068 """ 1069 if "test" in issue_type.lower(): 1070 function_name = "getTests" 1071 query = """ 1072 query GetIds($limit: Int!, $jql: String!) { 1073 getTests(limit: $limit, jql:$jql) { 1074 results { 1075 issueId 1076 jira(fields: ["key"]) 1077 } 1078 } 1079 } 1080 """ 1081 variables = { 1082 "limit": 10, 1083 "jql": f"project = '{parse_project}' AND key = '{issue_key}'" 1084 } 1085 data = self._make_graphql_request(query, variables) 1086 if not data: 1087 logger.error(f"Failed to get issue ID for {issue_key}") 1088 return None 1089 for issue in data[function_name]['results']: 1090 if str(issue['jira']['key']).lower() == issue_key.lower(): 1091 return issue['issueId'] 1092 return None 1093 except Exception as e: 1094 logger.error(f"Failed to get issue ID for {issue_key}") 1095 logger.traceback(e) 1096 return None
Retrieves the internal Xray issue ID for a given JIRA issue key and type. This method queries the Xray GraphQL API to find the internal issue ID corresponding to a JIRA issue key. It supports different types of Xray artifacts including test plans, test executions, test sets, and tests. Args: issue_key (str): The JIRA issue key (e.g., "PROJECT-123") issue_type (str): The type of Xray artifact. Supported values are: - "plan" or contains "plan": For Test Plans - "exec" or contains "exec": For Test Executions - "set" or contains "set": For Test Sets - "test" or contains "test": For Tests If not provided, defaults to "plan" Returns: Optional[str]: The internal Xray issue ID if found, None if: - The issue key doesn't exist - The GraphQL request fails - Any other error occurs during processing Examples:
client.get_issue_id_from_jira_id("TEST-123", "plan") '10000' client.get_issue_id_from_jira_id("TEST-456", "test") '10001' client.get_issue_id_from_jira_id("INVALID-789", "plan") None Note: The method performs a case-insensitive comparison when matching issue keys. The project key is extracted from the issue_key (text before the hyphen) to filter results by project.
1098 def get_test_details(self, issue_key: str, issue_type: str) -> Optional[str]: 1099 """ 1100 Retrieves the internal Xray issue ID for a given JIRA issue key and type. 1101 This method queries the Xray GraphQL API to find the internal issue ID corresponding 1102 to a JIRA issue key. It supports different types of Xray artifacts including test plans, 1103 test executions, test sets, and tests. 1104 Args: 1105 issue_key (str): The JIRA issue key (e.g., "PROJECT-123") 1106 issue_type (str): The type of Xray artifact. Supported values are: 1107 - "plan" or contains "plan": For Test Plans 1108 - "exec" or contains "exec": For Test Executions 1109 - "set" or contains "set": For Test Sets 1110 - "test" or contains "test": For Tests 1111 If not provided, defaults to "plan" 1112 Returns: 1113 Optional[str]: The internal Xray issue ID if found, None if: 1114 - The issue key doesn't exist 1115 - The GraphQL request fails 1116 - Any other error occurs during processing 1117 Examples: 1118 >>> client.get_issue_id_from_jira_id("TEST-123", "plan") 1119 '10000' 1120 >>> client.get_issue_id_from_jira_id("TEST-456", "test") 1121 '10001' 1122 >>> client.get_issue_id_from_jira_id("INVALID-789", "plan") 1123 None 1124 Note: 1125 The method performs a case-insensitive comparison when matching issue keys. 1126 The project key is extracted from the issue_key (text before the hyphen) 1127 to filter results by project. 1128 """ 1129 try: 1130 parse_project = issue_key.split("-")[0] 1131 function_name = "getTestPlans" 1132 if not issue_type: 1133 issue_type = "plan" 1134 if "plan" in issue_type.lower(): 1135 function_name = "getTestPlans" 1136 jira_fields = [ 1137 "key", "summary", "description", "assignee", 1138 "status", "priority", "labels", "created", 1139 "updated", "dueDate", "components", "versions", 1140 "attachments", "comments" 1141 ] 1142 query = """ 1143 query GetDetails($limit: Int!, $jql: String!) { 1144 getTestPlans(limit: $limit, jql:$jql) { 1145 results { 1146 issueId 1147 jira(fields: ["key"]) 1148 } 1149 } 1150 } 1151 """ 1152 if "exec" in issue_type.lower(): 1153 function_name = "getTestExecutions" 1154 jira_fields = [ 1155 "key", "summary", "description", "assignee", 1156 "status", "priority", "labels", "created", 1157 "updated", "dueDate", "components", "versions", 1158 "attachments", "comments" 1159 ] 1160 query = """ 1161 query GetDetails($limit: Int!, $jql: String!) { 1162 getTestExecutions(limit: $limit, jql:$jql) { 1163 results { 1164 issueId 1165 jira(fields: ["key"]) 1166 } 1167 } 1168 } 1169 """ 1170 if "set" in issue_type.lower(): 1171 function_name = "getTestSets" 1172 jira_fields = [ 1173 "key", "summary", "description", "assignee", 1174 "status", "priority", "labels", "created", 1175 "updated", "dueDate", "components", "versions", 1176 "attachments", "comments" 1177 ] 1178 query = """ 1179 query GetDetails($limit: Int!, $jql: String!) { 1180 getTestSets(limit: $limit, jql:$jql) { 1181 results { 1182 issueId 1183 jira(fields: ["key"]) 1184 } 1185 } 1186 } 1187 """ 1188 if "test" in issue_type.lower(): 1189 function_name = "getTests" 1190 jira_fields = [ 1191 "key", "summary", "description", "assignee", 1192 "status", "priority", "labels", "created", 1193 "updated", "dueDate", "components", "versions", 1194 "attachments", "comments" 1195 ] 1196 query = """ 1197 query GetDetails($limit: Int!, $jql: String!, $jiraFields: [String!]!) { 1198 getTests(limit: $limit, jql:$jql) { 1199 results { 1200 issueId 1201 jira(fields: $jiraFields) 1202 steps { 1203 id 1204 action 1205 result 1206 attachments { 1207 id 1208 filename 1209 storedInJira 1210 downloadLink 1211 } 1212 } 1213 1214 } 1215 } 1216 } 1217 """ 1218 variables = { 1219 "limit": 10, 1220 "jql": f"project = '{parse_project}' AND key = '{issue_key}'", 1221 "jiraFields": jira_fields 1222 } 1223 data = self._make_graphql_request(query, variables) 1224 if not data: 1225 logger.error(f"Failed to get issue ID for {issue_key}") 1226 return None 1227 for issue in data[function_name]['results']: 1228 if str(issue['jira']['key']).lower() == issue_key.lower(): 1229 return issue # This now includes all metadata 1230 return None 1231 except Exception as e: 1232 logger.error(f"Failed to get issue ID for {issue_key}") 1233 logger.traceback(e) 1234 return None
Retrieves the internal Xray issue ID for a given JIRA issue key and type. This method queries the Xray GraphQL API to find the internal issue ID corresponding to a JIRA issue key. It supports different types of Xray artifacts including test plans, test executions, test sets, and tests. Args: issue_key (str): The JIRA issue key (e.g., "PROJECT-123") issue_type (str): The type of Xray artifact. Supported values are: - "plan" or contains "plan": For Test Plans - "exec" or contains "exec": For Test Executions - "set" or contains "set": For Test Sets - "test" or contains "test": For Tests If not provided, defaults to "plan" Returns: Optional[str]: The internal Xray issue ID if found, None if: - The issue key doesn't exist - The GraphQL request fails - Any other error occurs during processing Examples:
client.get_issue_id_from_jira_id("TEST-123", "plan") '10000' client.get_issue_id_from_jira_id("TEST-456", "test") '10001' client.get_issue_id_from_jira_id("INVALID-789", "plan") None Note: The method performs a case-insensitive comparison when matching issue keys. The project key is extracted from the issue_key (text before the hyphen) to filter results by project.
1236 def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]: 1237 """ 1238 Retrieves all tests associated with a given test plan from Xray. 1239 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1240 test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that 1241 ID to fetch the associated tests. 1242 Args: 1243 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1244 Returns: 1245 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1246 or None if the operation fails. For example: 1247 { 1248 "PROJECT-124": "10001", 1249 "PROJECT-125": "10002" 1250 } 1251 Returns None in the following cases: 1252 - Test plan ID cannot be found 1253 - GraphQL request fails 1254 - Any other error occurs during processing 1255 Example: 1256 >>> client = XrayGraphQL() 1257 >>> tests = client.get_tests_from_test_plan("PROJECT-123") 1258 >>> print(tests) 1259 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1260 Note: 1261 - The method is limited to retrieving 99999 tests per test plan 1262 - Test plan must exist in Xray and be accessible with current authentication 1263 """ 1264 try: 1265 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1266 if not test_plan_id: 1267 logger.error(f"Failed to get test plan ID for {test_plan}") 1268 return None 1269 query = """ 1270 query GetTestPlanTests($testPlanId: String!) { 1271 getTestPlan(issueId: $testPlanId) { 1272 tests(limit: 99999) { 1273 results { 1274 issueId 1275 jira(fields: ["key"]) 1276 } 1277 } 1278 } 1279 } 1280 """ 1281 variables = {"testPlanId": test_plan_id} 1282 data = self._make_graphql_request(query, variables) 1283 if not data: 1284 logger.error(f"Failed to get tests for plan {test_plan_id}") 1285 return None 1286 tests = {} 1287 for test in data['getTestPlan']['tests']['results']: 1288 tests[test['jira']['key']] = test['issueId'] 1289 return tests 1290 except Exception as e: 1291 logger.error(f"Failed to get tests for plan {test_plan_id}") 1292 logger.traceback(e) 1293 return None
Retrieves all tests associated with a given test plan from Xray. This method queries the Xray GraphQL API to fetch all tests that are part of the specified test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that ID to fetch the associated tests. Args: test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, or None if the operation fails. For example: { "PROJECT-124": "10001", "PROJECT-125": "10002" } Returns None in the following cases: - Test plan ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:
client = XrayGraphQL() tests = client.get_tests_from_test_plan("PROJECT-123") print(tests) {"PROJECT-124": "10001", "PROJECT-125": "10002"} Note: - The method is limited to retrieving 99999 tests per test plan - Test plan must exist in Xray and be accessible with current authentication
1295 def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]: 1296 """ 1297 Retrieves all tests associated with a given test set from Xray. 1298 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1299 test set. It first converts the JIRA test set key to an internal Xray ID, then uses that 1300 ID to fetch the associated tests. 1301 Args: 1302 test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") 1303 Returns: 1304 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1305 or None if the operation fails. For example: 1306 { 1307 "PROJECT-124": "10001", 1308 "PROJECT-125": "10002" 1309 } 1310 Returns None in the following cases: 1311 - Test set ID cannot be found 1312 - GraphQL request fails 1313 - Any other error occurs during processing 1314 Example: 1315 >>> client = XrayGraphQL() 1316 >>> tests = client.get_tests_from_test_set("PROJECT-123") 1317 >>> print(tests) 1318 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1319 Note: 1320 - The method is limited to retrieving 99999 tests per test set 1321 - Test set must exist in Xray and be accessible with current authentication 1322 """ 1323 try: 1324 test_set_id = self.get_issue_id_from_jira_id(test_set, "set") 1325 if not test_set_id: 1326 logger.error(f"Failed to get test set ID for {test_set}") 1327 return None 1328 query = """ 1329 query GetTestSetTests($testSetId: String!) { 1330 getTestSet(issueId: $testSetId) { 1331 tests(limit: 99999) { 1332 results { 1333 issueId 1334 jira(fields: ["key"]) 1335 } 1336 } 1337 } 1338 } 1339 """ 1340 variables = {"testSetId": test_set_id} 1341 data = self._make_graphql_request(query, variables) 1342 if not data: 1343 logger.error(f"Failed to get tests for set {test_set_id}") 1344 return None 1345 tests = {} 1346 for test in data['getTestSet']['tests']['results']: 1347 tests[test['jira']['key']] = test['issueId'] 1348 return tests 1349 except Exception as e: 1350 logger.error(f"Failed to get tests for set {test_set_id}") 1351 logger.traceback(e) 1352 return None
Retrieves all tests associated with a given test set from Xray. This method queries the Xray GraphQL API to fetch all tests that are part of the specified test set. It first converts the JIRA test set key to an internal Xray ID, then uses that ID to fetch the associated tests. Args: test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, or None if the operation fails. For example: { "PROJECT-124": "10001", "PROJECT-125": "10002" } Returns None in the following cases: - Test set ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:
client = XrayGraphQL() tests = client.get_tests_from_test_set("PROJECT-123") print(tests) {"PROJECT-124": "10001", "PROJECT-125": "10002"} Note: - The method is limited to retrieving 99999 tests per test set - Test set must exist in Xray and be accessible with current authentication
1354 def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]: 1355 """ 1356 Retrieves all tests associated with a given test execution from Xray. 1357 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1358 test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that 1359 ID to fetch the associated tests. 1360 Args: 1361 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1362 Returns: 1363 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1364 or None if the operation fails. For example: 1365 { 1366 "PROJECT-124": "10001", 1367 "PROJECT-125": "10002" 1368 } 1369 Returns None in the following cases: 1370 - Test execution ID cannot be found 1371 - GraphQL request fails 1372 - Any other error occurs during processing 1373 Example: 1374 >>> client = XrayGraphQL() 1375 >>> tests = client.get_tests_from_test_execution("PROJECT-123") 1376 >>> print(tests) 1377 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1378 Note: 1379 - The method is limited to retrieving 99999 tests per test execution 1380 - Test execution must exist in Xray and be accessible with current authentication 1381 """ 1382 try: 1383 test_execution_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1384 if not test_execution_id: 1385 logger.error(f"Failed to get test execution ID for {test_execution}") 1386 return None 1387 query = """ 1388 query GetTestExecutionTests($testExecutionId: String!) { 1389 getTestExecution(issueId: $testExecutionId) { 1390 tests(limit: 100) { 1391 results { 1392 issueId 1393 jira(fields: ["key"]) 1394 } 1395 } 1396 } 1397 } 1398 """ 1399 variables = {"testExecutionId": test_execution_id} 1400 data = self._make_graphql_request(query, variables) 1401 if not data: 1402 logger.error(f"Failed to get tests for execution {test_execution_id}") 1403 return None 1404 tests = {} 1405 for test in data['getTestExecution']['tests']['results']: 1406 tests[test['jira']['key']] = test['issueId'] 1407 return tests 1408 except Exception as e: 1409 logger.error(f"Failed to get tests for execution {test_execution_id}") 1410 logger.traceback(e) 1411 return None
Retrieves all tests associated with a given test execution from Xray. This method queries the Xray GraphQL API to fetch all tests that are part of the specified test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that ID to fetch the associated tests. Args: test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, or None if the operation fails. For example: { "PROJECT-124": "10001", "PROJECT-125": "10002" } Returns None in the following cases: - Test execution ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:
client = XrayGraphQL() tests = client.get_tests_from_test_execution("PROJECT-123") print(tests) {"PROJECT-124": "10001", "PROJECT-125": "10002"} Note: - The method is limited to retrieving 99999 tests per test execution - Test execution must exist in Xray and be accessible with current authentication
1413 def get_test_plan_data(self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]: 1414 """ 1415 Retrieve and parse tabular data from a test plan's description field in Xray. 1416 This method fetches a test plan's description from Xray and parses any tables found within it. 1417 The tables in the description are expected to be in a specific format that can be parsed by 1418 the _parse_table method. The parsed data is returned as a dictionary containing numeric values 1419 and lists extracted from the table. 1420 Args: 1421 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1422 Returns: 1423 Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data 1424 where keys are derived from the first column of the table and values are lists of numeric 1425 values. Returns None if: 1426 - The test plan ID cannot be found 1427 - The GraphQL request fails 1428 - The description cannot be parsed 1429 - Any other error occurs during processing 1430 Example: 1431 >>> client = XrayGraphQL() 1432 >>> data = client.get_test_plan_data("TEST-123") 1433 >>> print(data) 1434 { 1435 'temperature': [20, 25, 30], 1436 'pressure': [1.0, 1.5, 2.0], 1437 'measurements': [[1, 2, 3], [4, 5, 6]] 1438 } 1439 Note: 1440 - The test plan must exist in Xray and be accessible with current authentication 1441 - The description must contain properly formatted tables for parsing 1442 - Table values are converted to numeric types (int or float) where possible 1443 - Lists in table cells should be formatted as [value1, value2, ...] 1444 - Failed operations are logged as errors with relevant details 1445 """ 1446 try: 1447 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1448 if not test_plan_id: 1449 logger.error(f"Failed to get test plan ID for {test_plan}") 1450 return None 1451 query = """ 1452 query GetTestPlanTests($testPlanId: String!) { 1453 getTestPlan(issueId: $testPlanId) { 1454 issueId 1455 jira(fields: ["key","description"]) 1456 } 1457 } 1458 """ 1459 variables = {"testPlanId": test_plan_id} 1460 data = self._make_graphql_request(query, variables) 1461 if not data: 1462 logger.error(f"Failed to get tests for plan {test_plan_id}") 1463 return None 1464 description = data['getTestPlan']['jira']['description'] 1465 test_plan_data = self._parse_table(description) 1466 return test_plan_data 1467 except Exception as e: 1468 logger.error(f"Failed to get tests for plan {test_plan_id}") 1469 logger.traceback(e) 1470 return None
Retrieve and parse tabular data from a test plan's description field in Xray. This method fetches a test plan's description from Xray and parses any tables found within it. The tables in the description are expected to be in a specific format that can be parsed by the _parse_table method. The parsed data is returned as a dictionary containing numeric values and lists extracted from the table. Args: test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") Returns: Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data where keys are derived from the first column of the table and values are lists of numeric values. Returns None if: - The test plan ID cannot be found - The GraphQL request fails - The description cannot be parsed - Any other error occurs during processing Example:
client = XrayGraphQL() data = client.get_test_plan_data("TEST-123") print(data) { 'temperature': [20, 25, 30], 'pressure': [1.0, 1.5, 2.0], 'measurements': [[1, 2, 3], [4, 5, 6]] } Note: - The test plan must exist in Xray and be accessible with current authentication - The description must contain properly formatted tables for parsing - Table values are converted to numeric types (int or float) where possible - Lists in table cells should be formatted as [value1, value2, ...] - Failed operations are logged as errors with relevant details
1472 def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]: 1473 """ 1474 Retrieves all test sets that contain a specific test case from Xray. 1475 This method queries the Xray GraphQL API to find all test sets that include the specified 1476 test case. It first converts the JIRA test case key to an internal Xray ID, then uses that 1477 ID to fetch all associated test sets. 1478 Args: 1479 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1480 Returns: 1481 Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, 1482 or None if the operation fails. For example: 1483 { 1484 "PROJECT-124": "Test Set for Feature A", 1485 "PROJECT-125": "Regression Test Set" 1486 } 1487 Returns None in the following cases: 1488 - Test case ID cannot be found 1489 - GraphQL request fails 1490 - Any other error occurs during processing 1491 Example: 1492 >>> client = XrayGraphQL() 1493 >>> test_sets = client.filter_test_set_by_test_case("PROJECT-123") 1494 >>> print(test_sets) 1495 {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} 1496 Note: 1497 - The method is limited to retrieving 99999 test sets per test case 1498 - Test case must exist in Xray and be accessible with current authentication 1499 """ 1500 try: 1501 test_id = self.get_issue_id_from_jira_id(test_key, "test") 1502 if not test_id: 1503 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1504 return None 1505 query = """ 1506 query GetTestDetails($testId: String!) { 1507 getTest(issueId: $testId) { 1508 testSets(limit: 100) { 1509 results { 1510 issueId 1511 jira(fields: ["key","summary"]) 1512 } 1513 } 1514 } 1515 } 1516 """ 1517 variables = { 1518 "testId": test_id 1519 } 1520 data = self._make_graphql_request(query, variables) 1521 if not data: 1522 logger.error(f"Failed to get tests for plan {test_id}") 1523 return None 1524 retDict = {} 1525 for test in data['getTest']['testSets']['results']: 1526 retDict[test['jira']['key']] = test['jira']['summary'] 1527 return retDict 1528 except Exception as e: 1529 logger.error(f"Error in getting test set by test id: {e}") 1530 logger.traceback(e) 1531 return None
Retrieves all test sets that contain a specific test case from Xray. This method queries the Xray GraphQL API to find all test sets that include the specified test case. It first converts the JIRA test case key to an internal Xray ID, then uses that ID to fetch all associated test sets. Args: test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, or None if the operation fails. For example: { "PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set" } Returns None in the following cases: - Test case ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:
client = XrayGraphQL() test_sets = client.filter_test_set_by_test_case("PROJECT-123") print(test_sets) {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} Note: - The method is limited to retrieving 99999 test sets per test case - Test case must exist in Xray and be accessible with current authentication
1598 def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]: 1599 """ 1600 Retrieve the status of a test run for a specific test case within a test execution. 1601 This method queries the Xray GraphQL API to get the current status of a test run, 1602 which represents the execution status of a specific test case within a test execution. 1603 It first converts both the test case and test execution JIRA keys to their internal 1604 Xray IDs, then uses these to fetch the test run status. 1605 Args: 1606 test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1607 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") 1608 Returns: 1609 Tuple[Optional[str], Optional[str]]: A tuple containing: 1610 - test_run_id: The unique identifier of the test run (or None if not found) 1611 - test_run_status: The current status of the test run (or None if not found) 1612 Returns (None, None) if any error occurs during the process. 1613 Example: 1614 >>> client = XrayGraphQL() 1615 >>> run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") 1616 >>> print(f"Test Run ID: {run_id}, Status: {status}") 1617 Test Run ID: 10001, Status: PASS 1618 Note: 1619 - Both the test case and test execution must exist in Xray and be accessible 1620 - The test case must be associated with the test execution 1621 - The method performs two ID lookups before querying the test run status 1622 - Failed operations are logged as errors with relevant details 1623 """ 1624 try: 1625 test_case_id = self.get_issue_id_from_jira_id(test_case, "test") 1626 if not test_case_id: 1627 logger.error(f"Failed to get test ID for Test Case ({test_case})") 1628 return None 1629 test_exec_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1630 if not test_exec_id: 1631 logger.error(f"Failed to get test execution ID for Test Execution ({test_execution})") 1632 return None 1633 query = """ 1634 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1635 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1636 id 1637 status { 1638 name 1639 } 1640 } 1641 } 1642 """ 1643 variables = { 1644 "testId": test_case_id, 1645 "testExecutionId": test_exec_id, 1646 } 1647 # Add debug loggerging 1648 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_exec_id}") 1649 data = self._make_graphql_request(query, variables) 1650 if not data: 1651 logger.error(f"Failed to get test run status for test {test_case_id}") 1652 return None 1653 # jprint(data) 1654 test_run_id = data['getTestRun']['id'] 1655 test_run_status = data['getTestRun']['status']['name'] 1656 return (test_run_id, test_run_status) 1657 except Exception as e: 1658 logger.error(f"Error getting test run status: {str(e)}") 1659 logger.traceback(e) 1660 return (None, None)
Retrieve the status of a test run for a specific test case within a test execution. This method queries the Xray GraphQL API to get the current status of a test run, which represents the execution status of a specific test case within a test execution. It first converts both the test case and test execution JIRA keys to their internal Xray IDs, then uses these to fetch the test run status. Args: test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") Returns: Tuple[Optional[str], Optional[str]]: A tuple containing: - test_run_id: The unique identifier of the test run (or None if not found) - test_run_status: The current status of the test run (or None if not found) Returns (None, None) if any error occurs during the process. Example:
client = XrayGraphQL() run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") print(f"Test Run ID: {run_id}, Status: {status}") Test Run ID: 10001, Status: PASS Note: - Both the test case and test execution must exist in Xray and be accessible - The test case must be associated with the test execution - The method performs two ID lookups before querying the test run status - Failed operations are logged as errors with relevant details
1662 def get_test_run_by_id(self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]: 1663 """ 1664 Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. 1665 Args: 1666 test_case_id (str): The ID of the test case to query 1667 test_execution_id (str): The ID of the test execution containing the test run 1668 Returns: 1669 tuple[Optional[str], Optional[str]]: A tuple containing: 1670 - test_run_id: The ID of the test run if found, None if not found or on error 1671 - test_run_status: The status name of the test run if found, None if not found or on error 1672 Note: 1673 The function makes a GraphQL request to fetch the test run information. If the request fails 1674 or encounters any errors, it will log the error and return (None, None). 1675 """ 1676 try: 1677 query = """ 1678 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1679 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1680 id 1681 status { 1682 name 1683 } 1684 } 1685 } 1686 """ 1687 variables = { 1688 "testId": test_case_id, 1689 "testExecutionId": test_execution_id, 1690 } 1691 # Add debug loggerging 1692 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_execution_id}") 1693 data = self._make_graphql_request(query, variables) 1694 if not data: 1695 logger.error(f"Failed to get test run status for test {test_case_id}") 1696 return None 1697 test_run_id = data['getTestRun']['id'] 1698 test_run_status = data['getTestRun']['status']['name'] 1699 return (test_run_id, test_run_status) 1700 except Exception as e: 1701 logger.error(f"Error getting test run status: {str(e)}") 1702 logger.traceback(e) 1703 return (None, None)
Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. Args: test_case_id (str): The ID of the test case to query test_execution_id (str): The ID of the test execution containing the test run Returns: tuple[Optional[str], Optional[str]]: A tuple containing: - test_run_id: The ID of the test run if found, None if not found or on error - test_run_status: The status name of the test run if found, None if not found or on error Note: The function makes a GraphQL request to fetch the test run information. If the request fails or encounters any errors, it will log the error and return (None, None).
1705 def get_test_execution(self, test_execution: str) -> Optional[Dict]: 1706 """ 1707 Retrieve detailed information about a test execution from Xray. 1708 This method queries the Xray GraphQL API to fetch information about a specific test execution, 1709 including its ID and associated tests. It first converts the JIRA test execution key to an 1710 internal Xray ID, then uses that ID to fetch the execution details. 1711 Args: 1712 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1713 Returns: 1714 Optional[Dict]: A dictionary containing test execution details if successful, None if failed. 1715 The dictionary has the following structure: 1716 { 1717 'id': str, # The internal Xray ID of the test execution 1718 'tests': { # Dictionary mapping test keys to their IDs 1719 'TEST-124': '10001', 1720 'TEST-125': '10002', 1721 ... 1722 } 1723 } 1724 Returns None in the following cases: 1725 - Test execution ID cannot be found 1726 - GraphQL request fails 1727 - No test execution found with the given ID 1728 - No tests found in the test execution 1729 - Any other error occurs during processing 1730 Example: 1731 >>> client = XrayGraphQL() 1732 >>> execution = client.get_test_execution("TEST-123") 1733 >>> print(execution) 1734 { 1735 'id': '10000', 1736 'tests': { 1737 'TEST-124': '10001', 1738 'TEST-125': '10002' 1739 } 1740 } 1741 Note: 1742 - The method is limited to retrieving 99999 tests per test execution 1743 - Test execution must exist in Xray and be accessible with current authentication 1744 - Failed operations are logged with appropriate error or warning messages 1745 """ 1746 try: 1747 test_execution_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1748 if not test_execution_id: 1749 logger.error(f"Failed to get test execution ID for {test_execution}") 1750 return None 1751 query = """ 1752 query GetTestExecution($testExecutionId: String!) { 1753 getTestExecution(issueId: $testExecutionId) { 1754 issueId 1755 projectId 1756 jira(fields: ["key", "summary", "description", "status"]) 1757 tests(limit: 100) { 1758 total 1759 start 1760 limit 1761 results { 1762 issueId 1763 jira(fields: ["key"]) 1764 } 1765 } 1766 } 1767 } 1768 """ 1769 variables = { 1770 "testExecutionId": test_execution_id 1771 } 1772 # Add debug loggerging 1773 logger.debug(f"Getting test execution details for {test_execution_id}") 1774 data = self._make_graphql_request(query, variables) 1775 # jprint(data) 1776 if not data: 1777 logger.error(f"Failed to get test execution details for {test_execution_id}") 1778 return None 1779 test_execution = data.get('getTestExecution',{}) 1780 if not test_execution: 1781 logger.warning(f"No test execution found with ID {test_execution_id}") 1782 return None 1783 tests = test_execution.get('tests',{}) 1784 if not tests: 1785 logger.warning(f"No tests found for test execution {test_execution_id}") 1786 return None 1787 tests_details = dict() 1788 for test in tests['results']: 1789 tests_details[test['jira']['key']] = test['issueId'] 1790 formatted_response = { 1791 'id': test_execution['issueId'], 1792 'tests': tests_details 1793 } 1794 return formatted_response 1795 except Exception as e: 1796 logger.error(f"Error getting test execution details: {str(e)}") 1797 logger.traceback(e) 1798 return None
Retrieve detailed information about a test execution from Xray. This method queries the Xray GraphQL API to fetch information about a specific test execution, including its ID and associated tests. It first converts the JIRA test execution key to an internal Xray ID, then uses that ID to fetch the execution details. Args: test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") Returns: Optional[Dict]: A dictionary containing test execution details if successful, None if failed. The dictionary has the following structure: { 'id': str, # The internal Xray ID of the test execution 'tests': { # Dictionary mapping test keys to their IDs 'TEST-124': '10001', 'TEST-125': '10002', ... } } Returns None in the following cases: - Test execution ID cannot be found - GraphQL request fails - No test execution found with the given ID - No tests found in the test execution - Any other error occurs during processing Example:
client = XrayGraphQL() execution = client.get_test_execution("TEST-123") print(execution) { 'id': '10000', 'tests': { 'TEST-124': '10001', 'TEST-125': '10002' } } Note: - The method is limited to retrieving 99999 tests per test execution - Test execution must exist in Xray and be accessible with current authentication - Failed operations are logged with appropriate error or warning messages
1800 def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]: 1801 """ 1802 Add a test execution to an existing test plan in Xray. 1803 This method associates a test execution with a test plan using the Xray GraphQL API. 1804 It first converts both the test plan and test execution JIRA keys to their internal 1805 Xray IDs, then creates the association between them. 1806 Args: 1807 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1808 test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") 1809 Returns: 1810 Optional[Dict]: A dictionary containing the response data if successful, None if failed. 1811 The dictionary has the following structure: 1812 { 1813 'addTestExecutionsToTestPlan': { 1814 'addedTestExecutions': [str], # List of added test execution IDs 1815 'warning': str # Any warnings from the operation 1816 } 1817 } 1818 Returns None in the following cases: 1819 - Test plan ID cannot be found 1820 - Test execution ID cannot be found 1821 - GraphQL request fails 1822 - Any other error occurs during processing 1823 Example: 1824 >>> client = XrayGraphQL() 1825 >>> result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") 1826 >>> print(result) 1827 { 1828 'addTestExecutionsToTestPlan': { 1829 'addedTestExecutions': ['10001'], 1830 'warning': None 1831 } 1832 } 1833 Note: 1834 - Both the test plan and test execution must exist in Xray and be accessible 1835 - The method performs two ID lookups before creating the association 1836 - Failed operations are logged as errors with relevant details 1837 """ 1838 try: 1839 test_plan_id = self.get_issue_id_from_jira_id(test_plan, "plan") 1840 if not test_plan_id: 1841 logger.error(f"Test plan ID is required") 1842 return None 1843 test_exec_id = self.get_issue_id_from_jira_id(test_execution, "exec") 1844 if not test_exec_id: 1845 logger.error(f"Test execution ID is required") 1846 return None 1847 query = """ 1848 mutation AddTestExecutionToTestPlan($testPlanId: String!, $testExecutionIds: [String!]!) { 1849 addTestExecutionsToTestPlan(issueId: $testPlanId, testExecIssueIds: $testExecutionIds) { 1850 addedTestExecutions 1851 warning 1852 } 1853 } 1854 """ 1855 variables = { 1856 "testPlanId": test_plan_id, 1857 "testExecutionIds": [test_exec_id] 1858 } 1859 data = self._make_graphql_request(query, variables) 1860 return data 1861 except Exception as e: 1862 logger.error(f"Error adding test execution to test plan: {str(e)}") 1863 logger.traceback(e) 1864 return None
Add a test execution to an existing test plan in Xray. This method associates a test execution with a test plan using the Xray GraphQL API. It first converts both the test plan and test execution JIRA keys to their internal Xray IDs, then creates the association between them. Args: test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") Returns: Optional[Dict]: A dictionary containing the response data if successful, None if failed. The dictionary has the following structure: { 'addTestExecutionsToTestPlan': { 'addedTestExecutions': [str], # List of added test execution IDs 'warning': str # Any warnings from the operation } } Returns None in the following cases: - Test plan ID cannot be found - Test execution ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:
client = XrayGraphQL() result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") print(result) { 'addTestExecutionsToTestPlan': { 'addedTestExecutions': ['10001'], 'warning': None } } Note: - Both the test plan and test execution must exist in Xray and be accessible - The method performs two ID lookups before creating the association - Failed operations are logged as errors with relevant details
1866 def create_test_execution(self, 1867 test_issue_keys: List[str], 1868 project_key: Optional[str] = None, 1869 summary: Optional[str] = None, 1870 description: Optional[str] = None) -> Optional[Dict]: 1871 """ 1872 Create a new test execution in Xray with specified test cases. 1873 This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. 1874 It handles validation of test issue keys, automatically derives project information if not provided, 1875 and creates appropriate default values for summary and description if not specified. 1876 Args: 1877 test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution 1878 (e.g., ["TEST-123", "TEST-124"]) 1879 project_key (Optional[str]): The JIRA project key where the test execution should be created. 1880 If not provided, it will be derived from the first test issue key. 1881 summary (Optional[str]): The summary/title for the test execution ticket. 1882 If not provided, a default summary will be generated using the test issue keys. 1883 description (Optional[str]): The description for the test execution ticket. 1884 If not provided, a default description will be generated using the test issue keys. 1885 Returns: 1886 Optional[Dict]: A dictionary containing the created test execution details if successful, 1887 None if the creation fails. The dictionary has the following structure: 1888 { 1889 'issueId': str, # The internal Xray ID of the created test execution 1890 'jira': { 1891 'key': str # The JIRA issue key of the created test execution 1892 } 1893 } 1894 Example: 1895 >>> client = XrayGraphQL() 1896 >>> test_execution = client.create_test_execution( 1897 ... test_issue_keys=["TEST-123", "TEST-124"], 1898 ... project_key="TEST", 1899 ... summary="Sprint 1 Regression Tests" 1900 ... ) 1901 >>> print(test_execution) 1902 {'issueId': '10001', 'jira': {'key': 'TEST-125'}} 1903 Note: 1904 - Invalid test issue keys are logged as warnings but don't prevent execution creation 1905 - At least one valid test issue key is required 1906 - The method validates each test issue key before creating the execution 1907 - Project key is automatically derived from the first test issue key if not provided 1908 """ 1909 try: 1910 invalid_keys = [] 1911 test_issue_ids = [] 1912 for key in test_issue_keys: 1913 test_issue_id = self.get_issue_id_from_jira_id(key, "test") 1914 if test_issue_id: 1915 test_issue_ids.append(test_issue_id) 1916 else: 1917 invalid_keys.append(key) 1918 if len(test_issue_ids) == 0: 1919 logger.error(f"No valid test issue keys provided: {invalid_keys}") 1920 return None 1921 if len(invalid_keys) > 0: 1922 logger.warning(f"Invalid test issue keys: {invalid_keys}") 1923 if not project_key: 1924 project_key = test_issue_keys[0].split("-")[0] 1925 if not summary: 1926 summary = f"Test Execution for Test Plan {test_issue_keys}" 1927 if not description: 1928 description = f"Test Execution for Test Plan {test_issue_keys}" 1929 mutation = """ 1930 mutation CreateTestExecutionForTestPlan( 1931 $testIssueId_list: [String!]!, 1932 $projectKey: String!, 1933 $summary: String!, 1934 $description: String 1935 ) { 1936 createTestExecution( 1937 testIssueIds: $testIssueId_list, 1938 jira: { 1939 fields: { 1940 project: { key: $projectKey }, 1941 summary: $summary, 1942 description: $description, 1943 issuetype: { name: "Test Execution" } 1944 } 1945 } 1946 ) { 1947 testExecution { 1948 issueId 1949 jira(fields: ["key"]) 1950 } 1951 warnings 1952 } 1953 } 1954 """ 1955 variables = { 1956 "testIssueId_list": test_issue_ids, 1957 "projectKey": project_key, 1958 "summary": summary, 1959 "description": description 1960 } 1961 data = self._make_graphql_request(mutation, variables) 1962 if not data: 1963 return None 1964 execution_details = data['createTestExecution']['testExecution'] 1965 # logger.info(f"Created test execution {execution_details['jira']['key']}") 1966 return execution_details 1967 except Exception as e: 1968 logger.error("Failed to create test execution : {e}") 1969 logger.traceback(e) 1970 return None
Create a new test execution in Xray with specified test cases. This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. It handles validation of test issue keys, automatically derives project information if not provided, and creates appropriate default values for summary and description if not specified. Args: test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution (e.g., ["TEST-123", "TEST-124"]) project_key (Optional[str]): The JIRA project key where the test execution should be created. If not provided, it will be derived from the first test issue key. summary (Optional[str]): The summary/title for the test execution ticket. If not provided, a default summary will be generated using the test issue keys. description (Optional[str]): The description for the test execution ticket. If not provided, a default description will be generated using the test issue keys. Returns: Optional[Dict]: A dictionary containing the created test execution details if successful, None if the creation fails. The dictionary has the following structure: { 'issueId': str, # The internal Xray ID of the created test execution 'jira': { 'key': str # The JIRA issue key of the created test execution } } Example:
client = XrayGraphQL() test_execution = client.create_test_execution( ... test_issue_keys=["TEST-123", "TEST-124"], ... project_key="TEST", ... summary="Sprint 1 Regression Tests" ... ) print(test_execution) {'issueId': '10001', 'jira': {'key': 'TEST-125'}} Note: - Invalid test issue keys are logged as warnings but don't prevent execution creation - At least one valid test issue key is required - The method validates each test issue key before creating the execution - Project key is automatically derived from the first test issue key if not provided
1972 def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]: 1973 """Creates a test execution from a given test plan and associates all tests from the plan with the execution. 1974 This method performs several operations in sequence: 1975 1. Retrieves all tests from the specified test plan 1976 2. Creates a new test execution with those tests 1977 3. Associates the new test execution with the original test plan 1978 4. Creates test runs for each test in the execution 1979 Parameters 1980 ---------- 1981 test_plan : str 1982 The JIRA issue key of the test plan (e.g., "PROJECT-123") 1983 Returns 1984 ------- 1985 Optional[Dict[str, Dict[str, str]]] 1986 A dictionary mapping test case keys to their execution details, or None if the operation fails. 1987 The dictionary structure is:: 1988 { 1989 "TEST-123": { # Test case JIRA key 1990 "test_run_id": "12345", # Unique ID for this test run 1991 "test_execution_key": "TEST-456", # JIRA key of the created test execution 1992 "test_plan_key": "TEST-789" # Original test plan JIRA key 1993 }, 1994 "TEST-124": { 1995 ... 1996 } 1997 } 1998 Returns None in the following cases: 1999 * Test plan parameter is empty or invalid 2000 * No tests found in the test plan 2001 * Test execution creation fails 2002 * API request fails 2003 Examples 2004 -------- 2005 >>> client = XrayGraphQL() 2006 >>> result = client.create_test_execution_from_test_plan("TEST-123") 2007 >>> print(result) 2008 { 2009 "TEST-124": { 2010 "test_run_id": "5f7c3", 2011 "test_execution_key": "TEST-456", 2012 "test_plan_key": "TEST-123" 2013 }, 2014 "TEST-125": { 2015 "test_run_id": "5f7c4", 2016 "test_execution_key": "TEST-456", 2017 "test_plan_key": "TEST-123" 2018 } 2019 } 2020 Notes 2021 ----- 2022 - The test plan must exist and be accessible in Xray 2023 - All tests in the test plan must be valid and accessible 2024 - The method automatically generates a summary and description for the test execution 2025 - The created test execution is automatically linked back to the original test plan 2026 """ 2027 try: 2028 if not test_plan: 2029 logger.error("Test plan is required [ jira key]") 2030 return None 2031 project_key = test_plan.split("-")[0] 2032 summary = f"Test Execution for Test Plan {test_plan}" 2033 retDict = dict() 2034 #Get tests from test plan 2035 tests = self.get_tests_from_test_plan(test_plan) 2036 retDict["tests"] = tests 2037 testIssueId_list = list(tests.values()) 2038 # logger.info(f"Tests: {tests}") 2039 if not testIssueId_list: 2040 logger.error(f"No tests found for {test_plan}") 2041 return None 2042 description = f"Test Execution for {len(tests)} Test cases" 2043 # GraphQL mutation to create test execution 2044 query = """ 2045 mutation CreateTestExecutionForTestPlan( 2046 $testIssueId_list: [String!]!, 2047 $projectKey: String!, 2048 $summary: String!, 2049 $description: String 2050 ) { 2051 createTestExecution( 2052 testIssueIds: $testIssueId_list, 2053 jira: { 2054 fields: { 2055 project: { key: $projectKey }, 2056 summary: $summary, 2057 description: $description, 2058 issuetype: { name: "Test Execution" } 2059 } 2060 } 2061 ) { 2062 testExecution { 2063 issueId 2064 jira(fields: ["key"]) 2065 testRuns(limit: 100) { 2066 results { 2067 id 2068 test { 2069 issueId 2070 jira(fields: ["key"]) 2071 } 2072 } 2073 } 2074 } 2075 warnings 2076 } 2077 } 2078 """ 2079 variables = { 2080 "testIssueId_list": testIssueId_list, 2081 "projectKey": project_key, 2082 "summary": summary, 2083 "description": description or f"Test execution for total of {len(testIssueId_list)} test cases" 2084 } 2085 data = self._make_graphql_request(query, variables) 2086 if not data: 2087 return None 2088 test_exec_key = data['createTestExecution']['testExecution']['jira']['key'] 2089 test_exec_id = data['createTestExecution']['testExecution']['issueId'] 2090 #Add Test execution to test plan 2091 test_exec_dict= self.add_test_execution_to_test_plan(test_plan, test_exec_key) 2092 #Get test runs for test execution 2093 test_runs = data['createTestExecution']['testExecution']['testRuns']['results'] 2094 test_run_dict = dict() 2095 for test_run in test_runs: 2096 test_run_dict[test_run['test']['jira']['key']] = dict() 2097 test_run_dict[test_run['test']['jira']['key']]['test_run_id'] = test_run['id'] 2098 # test_run_dict[test_run['test']['jira']['key']]['test_issue_id'] = test_run['test']['issueId'] 2099 test_run_dict[test_run['test']['jira']['key']]['test_execution_key'] = test_exec_key 2100 # test_run_dict[test_run['test']['jira']['key']]['test_execution_id'] = test_exec_id 2101 test_run_dict[test_run['test']['jira']['key']]['test_plan_key'] = test_plan 2102 return test_run_dict 2103 except requests.exceptions.RequestException as e: 2104 logger.error(f"Error creating test execution: {e}") 2105 logger.traceback(e) 2106 return None
Creates a test execution from a given test plan and associates all tests from the plan with the execution. This method performs several operations in sequence:
- Retrieves all tests from the specified test plan
- Creates a new test execution with those tests
- Associates the new test execution with the original test plan
- Creates test runs for each test in the execution
Parameters
test_plan : str The JIRA issue key of the test plan (e.g., "PROJECT-123")
Returns
Optional[Dict[str, Dict[str, str]]] A dictionary mapping test case keys to their execution details, or None if the operation fails. The dictionary structure is:: { "TEST-123": { # Test case JIRA key "test_run_id": "12345", # Unique ID for this test run "test_execution_key": "TEST-456", # JIRA key of the created test execution "test_plan_key": "TEST-789" # Original test plan JIRA key }, "TEST-124": { ... } } Returns None in the following cases: * Test plan parameter is empty or invalid * No tests found in the test plan * Test execution creation fails * API request fails
Examples
>>> client = XrayGraphQL()
>>> result = client.create_test_execution_from_test_plan("TEST-123")
>>> print(result)
{
"TEST-124": {
"test_run_id": "5f7c3",
"test_execution_key": "TEST-456",
"test_plan_key": "TEST-123"
},
"TEST-125": {
"test_run_id": "5f7c4",
"test_execution_key": "TEST-456",
"test_plan_key": "TEST-123"
}
}
<h2 id="notes">Notes</h2>
- The test plan must exist and be accessible in Xray
- All tests in the test plan must be valid and accessible
- The method automatically generates a summary and description for the test execution
- The created test execution is automatically linked back to the original test plan
2108 def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool: 2109 """ 2110 Update the status of a specific test run in Xray using the GraphQL API. 2111 This method allows updating the execution status of a test run identified by its ID. 2112 The status can be changed to reflect the current state of the test execution 2113 (e.g., "PASS", "FAIL", "TODO", etc.). 2114 Args: 2115 test_run_id (str): The unique identifier of the test run to update. 2116 This is the internal Xray ID for the test run, not the Jira issue key. 2117 test_run_status (str): The new status to set for the test run. 2118 Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. 2119 Returns: 2120 bool: True if the status update was successful, False otherwise. 2121 Returns None if an error occurs during the API request. 2122 Example: 2123 >>> client = XrayGraphQL() 2124 >>> test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") 2125 >>> success = client.update_test_run_status(test_run_id, "PASS") 2126 >>> print(success) 2127 True 2128 Note: 2129 - The test run ID must be valid and accessible with current authentication 2130 - The status value should be one of the valid status values configured in your Xray instance 2131 - Failed updates are logged as errors with details about the failure 2132 Raises: 2133 Exception: If there is an error making the GraphQL request or processing the response. 2134 The exception is caught and logged, and the method returns None. 2135 """ 2136 try: 2137 query = """ 2138 mutation UpdateTestRunStatus($testRunId: String!, $status: String!) { 2139 updateTestRunStatus( 2140 id: $testRunId, 2141 status: $status 2142 ) 2143 } 2144 """ 2145 variables = { 2146 "testRunId": test_run_id, 2147 "status": test_run_status 2148 } 2149 data = self._make_graphql_request(query, variables) 2150 if not data: 2151 logger.error(f"Failed to get test run status for test {data}") 2152 return None 2153 # logger.info(f"Test run status updated: {data}") 2154 return data['updateTestRunStatus'] 2155 except Exception as e: 2156 logger.error(f"Error updating test run status: {str(e)}") 2157 logger.traceback(e) 2158 return None
Update the status of a specific test run in Xray using the GraphQL API. This method allows updating the execution status of a test run identified by its ID. The status can be changed to reflect the current state of the test execution (e.g., "PASS", "FAIL", "TODO", etc.). Args: test_run_id (str): The unique identifier of the test run to update. This is the internal Xray ID for the test run, not the Jira issue key. test_run_status (str): The new status to set for the test run. Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. Returns: bool: True if the status update was successful, False otherwise. Returns None if an error occurs during the API request. Example:
client = XrayGraphQL() test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") success = client.update_test_run_status(test_run_id, "PASS") print(success) True Note: - The test run ID must be valid and accessible with current authentication - The status value should be one of the valid status values configured in your Xray instance - Failed updates are logged as errors with details about the failure Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.
2160 def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2161 """ 2162 Update the comment of a specific test run in Xray using the GraphQL API. 2163 This method allows adding or updating the comment associated with a test run 2164 identified by its ID. The comment can provide additional context, test results, 2165 or any other relevant information about the test execution. 2166 Args: 2167 test_run_id (str): The unique identifier of the test run to update. 2168 This is the internal Xray ID for the test run, not the Jira issue key. 2169 test_run_comment (str): The new comment text to set for the test run. 2170 This will replace any existing comment on the test run. 2171 Returns: 2172 bool: True if the comment update was successful, False otherwise. 2173 Returns None if an error occurs during the API request. 2174 Example: 2175 >>> client = XrayGraphQL() 2176 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2177 >>> success = client.update_test_run_comment( 2178 ... test_run_id, 2179 ... "Test passed with performance within expected range" 2180 ... ) 2181 >>> print(success) 2182 True 2183 Note: 2184 - The test run ID must be valid and accessible with current authentication 2185 - The comment can include any text content, including newlines and special characters 2186 - Failed updates are logged as errors with details about the failure 2187 - This method will overwrite any existing comment on the test run 2188 Raises: 2189 Exception: If there is an error making the GraphQL request or processing the response. 2190 The exception is caught and logged, and the method returns None. 2191 """ 2192 try: 2193 query = """ 2194 mutation UpdateTestRunStatus($testRunId: String!, $comment: String!) { 2195 updateTestRunComment( 2196 id: $testRunId, 2197 comment: $comment 2198 ) 2199 } 2200 """ 2201 variables = { 2202 "testRunId": test_run_id, 2203 "comment": test_run_comment 2204 } 2205 data = self._make_graphql_request(query, variables) 2206 if not data: 2207 logger.error(f"Failed to get test run comment for test {data}") 2208 return None 2209 # jprint(data) 2210 return data['updateTestRunComment'] 2211 except Exception as e: 2212 logger.error(f"Error updating test run comment: {str(e)}") 2213 logger.traceback(e) 2214 return None
Update the comment of a specific test run in Xray using the GraphQL API. This method allows adding or updating the comment associated with a test run identified by its ID. The comment can provide additional context, test results, or any other relevant information about the test execution. Args: test_run_id (str): The unique identifier of the test run to update. This is the internal Xray ID for the test run, not the Jira issue key. test_run_comment (str): The new comment text to set for the test run. This will replace any existing comment on the test run. Returns: bool: True if the comment update was successful, False otherwise. Returns None if an error occurs during the API request. Example:
client = XrayGraphQL() test_run_id = "67fcfd4b9e6d63d4c1d57b32" success = client.update_test_run_comment( ... test_run_id, ... "Test passed with performance within expected range" ... ) print(success) True Note: - The test run ID must be valid and accessible with current authentication - The comment can include any text content, including newlines and special characters - Failed updates are logged as errors with details about the failure - This method will overwrite any existing comment on the test run Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.
2216 def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool: 2217 """Add evidence (attachments) to a test run in Xray. 2218 This method allows attaching files as evidence to a specific test run. The file is 2219 read, converted to base64, and uploaded to Xray with appropriate MIME type detection. 2220 Parameters 2221 ---------- 2222 test_run_id : str 2223 The unique identifier of the test run to add evidence to 2224 evidence_path : str 2225 The local file system path to the evidence file to be attached 2226 Returns 2227 ------- 2228 bool 2229 True if the evidence was successfully added, None if the operation failed. 2230 Returns None in the following cases: 2231 - Test run ID is not provided 2232 - Evidence path is not provided 2233 - Evidence file does not exist 2234 - GraphQL request fails 2235 - Any other error occurs during processing 2236 Examples 2237 -------- 2238 >>> client = XrayGraphQL() 2239 >>> success = client.add_evidence_to_test_run( 2240 ... test_run_id="10001", 2241 ... evidence_path="/path/to/screenshot.png" 2242 ... ) 2243 >>> print(success) 2244 True 2245 Notes 2246 ----- 2247 - The evidence file must exist and be accessible 2248 - The file is automatically converted to base64 for upload 2249 - MIME type is automatically detected, defaults to "text/plain" if detection fails 2250 - The method supports various file types (images, documents, logs, etc.) 2251 - Failed operations are logged with appropriate error messages 2252 """ 2253 try: 2254 if not test_run_id: 2255 logger.error("Test run ID is required") 2256 return None 2257 if not evidence_path: 2258 logger.error("Evidence path is required") 2259 return None 2260 if not os.path.exists(evidence_path): 2261 logger.error(f"Evidence file not found: {evidence_path}") 2262 return None 2263 #if file exists then read the file in base64 2264 evidence_base64 = None 2265 mime_type = None 2266 filename = os.path.basename(evidence_path) 2267 with open(evidence_path, "rb") as file: 2268 evidence_data = file.read() 2269 evidence_base64 = base64.b64encode(evidence_data).decode('utf-8') 2270 mime_type = mimetypes.guess_type(evidence_path)[0] 2271 logger.info(f"For loop -- Mime type: {mime_type}") 2272 if not mime_type: 2273 mime_type = "text/plain" 2274 query = """ 2275 mutation AddEvidenceToTestRun($testRunId: String!, $filename: String!, $mimeType: String!, $evidenceBase64: String!) { 2276 addEvidenceToTestRun( 2277 id: $testRunId, 2278 evidence: [ 2279 { 2280 filename : $filename, 2281 mimeType : $mimeType, 2282 data : $evidenceBase64 2283 } 2284 ] 2285 ) { 2286 addedEvidence 2287 warnings 2288 } 2289 } 2290 """ 2291 variables = { 2292 "testRunId": test_run_id, 2293 "filename": filename, 2294 "mimeType": mime_type, 2295 "evidenceBase64": evidence_base64 2296 } 2297 data = self._make_graphql_request(query, variables) 2298 if not data: 2299 logger.error(f"Failed to add evidence to test run: {data}") 2300 return None 2301 return data['addEvidenceToTestRun'] 2302 except Exception as e: 2303 logger.error(f"Error adding evidence to test run: {str(e)}") 2304 logger.traceback(e) 2305 return None
Add evidence (attachments) to a test run in Xray. This method allows attaching files as evidence to a specific test run. The file is read, converted to base64, and uploaded to Xray with appropriate MIME type detection.
Parameters
test_run_id : str The unique identifier of the test run to add evidence to evidence_path : str The local file system path to the evidence file to be attached
Returns
bool True if the evidence was successfully added, None if the operation failed. Returns None in the following cases: - Test run ID is not provided - Evidence path is not provided - Evidence file does not exist - GraphQL request fails - Any other error occurs during processing
Examples
>>> client = XrayGraphQL()
>>> success = client.add_evidence_to_test_run(
... test_run_id="10001",
... evidence_path="/path/to/screenshot.png"
... )
>>> print(success)
True
<h2 id="notes">Notes</h2>
- The evidence file must exist and be accessible
- The file is automatically converted to base64 for upload
- MIME type is automatically detected, defaults to "text/plain" if detection fails
- The method supports various file types (images, documents, logs, etc.)
- Failed operations are logged with appropriate error messages
2307 def create_defect_from_test_run(self, test_run_id: str, project_key: str, parent_issue_key: str, defect_summary: str, defect_description: str) -> Optional[Dict]: 2308 """Create a defect from a test run and link it to the test run in Xray. 2309 This method performs two main operations: 2310 1. Creates a new defect in JIRA with the specified summary and description 2311 2. Links the created defect to the specified test run in Xray 2312 Parameters 2313 ---------- 2314 test_run_id : str 2315 The ID of the test run to create defect from 2316 project_key : str 2317 The JIRA project key where the defect should be created. 2318 If not provided, defaults to "EAGVAL" 2319 parent_issue_key : str 2320 The JIRA key of the parent issue to link the defect to 2321 defect_summary : str 2322 Summary/title of the defect. 2323 If not provided, defaults to "Please provide a summary for the defect" 2324 defect_description : str 2325 Description of the defect. 2326 If not provided, defaults to "Please provide a description for the defect" 2327 Returns 2328 ------- 2329 Optional[Dict] 2330 Response data from the GraphQL API if successful, None if failed. 2331 The response includes: 2332 - addedDefects: List of added defects 2333 - warnings: Any warnings from the operation 2334 Examples 2335 -------- 2336 >>> client = XrayGraphQL() 2337 >>> result = client.create_defect_from_test_run( 2338 ... test_run_id="10001", 2339 ... project_key="PROJ", 2340 ... parent_issue_key="PROJ-456", 2341 ... defect_summary="Test failure in login flow", 2342 ... defect_description="The login button is not responding to clicks" 2343 ... ) 2344 >>> print(result) 2345 { 2346 'addedDefects': ['PROJ-123'], 2347 'warnings': [] 2348 } 2349 Notes 2350 ----- 2351 - The project_key will be split on '-' and only the first part will be used 2352 - The defect will be created with issue type 'Bug' 2353 - The method handles missing parameters with default values 2354 - The parent issue must exist and be accessible to create the defect 2355 """ 2356 try: 2357 if not project_key: 2358 project_key = "EAGVAL" 2359 if not defect_summary: 2360 defect_summary = "Please provide a summary for the defect" 2361 if not defect_description: 2362 defect_description = "Please provide a description for the defect" 2363 project_key = project_key.split("-")[0] 2364 # Fix: Correct parameter order for create_issue 2365 defect_key, defect_id = self.create_issue( 2366 project_key=project_key, 2367 parent_issue_key=parent_issue_key, 2368 summary=defect_summary, 2369 description=defect_description, 2370 issue_type='Bug' 2371 ) 2372 if not defect_key: 2373 logger.error("Failed to create defect issue") 2374 return None 2375 # Then add the defect to the test run 2376 add_defect_mutation = """ 2377 mutation AddDefectsToTestRun( 2378 $testRunId: String!, 2379 $defectKey: String! 2380 ) { 2381 addDefectsToTestRun( id: $testRunId, issues: [$defectKey]) { 2382 addedDefects 2383 warnings 2384 } 2385 } 2386 """ 2387 variables = { 2388 "testRunId": test_run_id, 2389 "defectKey": defect_key 2390 } 2391 data = None 2392 retry_count = 0 2393 while retry_count < 3: 2394 data = self._make_graphql_request(add_defect_mutation, variables) 2395 if not data: 2396 logger.error(f"Failed to add defect [{defect_key}] to test run - {test_run_id}.. retrying... {retry_count}") 2397 retry_count += 1 2398 time.sleep(1) 2399 else: 2400 break 2401 return data 2402 except Exception as e: 2403 logger.error(f"Error creating defect from test run: {str(e)}") 2404 logger.traceback(e) 2405 return None
Create a defect from a test run and link it to the test run in Xray. This method performs two main operations:
- Creates a new defect in JIRA with the specified summary and description
- Links the created defect to the specified test run in Xray
Parameters
test_run_id : str The ID of the test run to create defect from project_key : str The JIRA project key where the defect should be created. If not provided, defaults to "EAGVAL" parent_issue_key : str The JIRA key of the parent issue to link the defect to defect_summary : str Summary/title of the defect. If not provided, defaults to "Please provide a summary for the defect" defect_description : str Description of the defect. If not provided, defaults to "Please provide a description for the defect"
Returns
Optional[Dict] Response data from the GraphQL API if successful, None if failed. The response includes: - addedDefects: List of added defects - warnings: Any warnings from the operation
Examples
>>> client = XrayGraphQL()
>>> result = client.create_defect_from_test_run(
... test_run_id="10001",
... project_key="PROJ",
... parent_issue_key="PROJ-456",
... defect_summary="Test failure in login flow",
... defect_description="The login button is not responding to clicks"
... )
>>> print(result)
{
'addedDefects': ['PROJ-123'],
'warnings': []
}
<h2 id="notes">Notes</h2>
- The project_key will be split on '-' and only the first part will be used
- The defect will be created with issue type 'Bug'
- The method handles missing parameters with default values
- The parent issue must exist and be accessible to create the defect
2407 def get_test_run_comment(self, test_run_id: str) -> Optional[str]: 2408 """ 2409 Retrieve the comment of a specific test run from Xray using the GraphQL API. 2410 This method allows retrieving the comment associated with a test run 2411 identified by its ID. The comment can provide additional context, test results, 2412 or any other relevant information about the test execution. 2413 Args: 2414 test_run_id (str): The unique identifier of the test run to retrieve comment from. 2415 This is the internal Xray ID for the test run, not the Jira issue key. 2416 Returns: 2417 Optional[str]: The comment text of the test run if successful, None if: 2418 - The test run ID is not found 2419 - The GraphQL request fails 2420 - No comment exists for the test run 2421 - Any other error occurs during the API request 2422 Example: 2423 >>> client = XrayGraphQL() 2424 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2425 >>> comment = client.get_test_run_comment(test_run_id) 2426 >>> print(comment) 2427 "Test passed with performance within expected range" 2428 Note: 2429 - The test run ID must be valid and accessible with current authentication 2430 - If no comment exists for the test run, the method will return None 2431 - Failed requests are logged as errors with details about the failure 2432 - The method returns the raw comment text as stored in Xray 2433 Raises: 2434 Exception: If there is an error making the GraphQL request or processing the response. 2435 The exception is caught and logged, and the method returns None. 2436 """ 2437 try: 2438 # Try the direct ID approach first 2439 query = """ 2440 query GetTestRunComment($testRunId: String!) { 2441 getTestRunById(id: $testRunId) { 2442 id 2443 comment 2444 status { 2445 name 2446 } 2447 } 2448 } 2449 """ 2450 variables = { 2451 "testRunId": test_run_id 2452 } 2453 data = self._make_graphql_request(query, variables) 2454 jprint(data) 2455 if not data: 2456 logger.error(f"Failed to get test run comment for test run {test_run_id}") 2457 return None 2458 test_run = data.get('getTestRunById', {}) 2459 if not test_run: 2460 logger.warning(f"No test run found with ID {test_run_id}") 2461 return None 2462 comment = test_run.get('comment') 2463 return comment 2464 except Exception as e: 2465 logger.error(f"Error getting test run comment: {str(e)}") 2466 logger.traceback(e) 2467 return None
Retrieve the comment of a specific test run from Xray using the GraphQL API. This method allows retrieving the comment associated with a test run identified by its ID. The comment can provide additional context, test results, or any other relevant information about the test execution. Args: test_run_id (str): The unique identifier of the test run to retrieve comment from. This is the internal Xray ID for the test run, not the Jira issue key. Returns: Optional[str]: The comment text of the test run if successful, None if: - The test run ID is not found - The GraphQL request fails - No comment exists for the test run - Any other error occurs during the API request Example:
client = XrayGraphQL() test_run_id = "67fcfd4b9e6d63d4c1d57b32" comment = client.get_test_run_comment(test_run_id) print(comment) "Test passed with performance within expected range" Note: - The test run ID must be valid and accessible with current authentication - If no comment exists for the test run, the method will return None - Failed requests are logged as errors with details about the failure - The method returns the raw comment text as stored in Xray Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.
2469 def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2470 """ 2471 Append the comment of a specific test run in Xray using the GraphQL API. 2472 This method allows appending the comment associated with a test run 2473 identified by its ID. The comment can provide additional context, test results, 2474 or any other relevant information about the test execution. 2475 Args: 2476 test_run_id (str): The unique identifier of the test run to update. 2477 This is the internal Xray ID for the test run, not the Jira issue key. 2478 test_run_comment (str): The comment text to append to the test run. 2479 This will be added to any existing comment on the test run with proper formatting. 2480 Returns: 2481 bool: True if the comment update was successful, False otherwise. 2482 Returns None if an error occurs during the API request. 2483 Example: 2484 >>> client = XrayGraphQL() 2485 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2486 >>> success = client.append_test_run_comment( 2487 ... test_run_id, 2488 ... "Test passed with performance within expected range" 2489 ... ) 2490 >>> print(success) 2491 True 2492 Note: 2493 - The test run ID must be valid and accessible with current authentication 2494 - The comment can include any text content, including newlines and special characters 2495 - Failed updates are logged as errors with details about the failure 2496 - This method will append to existing comments with proper line breaks 2497 - If no existing comment exists, the new comment will be set as the initial comment 2498 Raises: 2499 Exception: If there is an error making the GraphQL request or processing the response. 2500 The exception is caught and logged, and the method returns None. 2501 """ 2502 try: 2503 # Get existing comment 2504 existing_comment = self.get_test_run_comment(test_run_id) 2505 # Prepare the combined comment with proper formatting 2506 if existing_comment: 2507 # If there's an existing comment, append with double newline for proper separation 2508 combined_comment = f"{existing_comment}\n{test_run_comment}" 2509 else: 2510 # If no existing comment, use the new comment as is 2511 combined_comment = test_run_comment 2512 logger.debug(f"No existing comment found for test run {test_run_id}, setting initial comment") 2513 query = """ 2514 mutation UpdateTestRunComment($testRunId: String!, $comment: String!) { 2515 updateTestRunComment( 2516 id: $testRunId, 2517 comment: $comment 2518 ) 2519 } 2520 """ 2521 variables = { 2522 "testRunId": test_run_id, 2523 "comment": combined_comment 2524 } 2525 data = self._make_graphql_request(query, variables) 2526 if not data: 2527 logger.error(f"Failed to update test run comment for test run {test_run_id}") 2528 return None 2529 return data['updateTestRunComment'] 2530 except Exception as e: 2531 logger.error(f"Error updating test run comment: {str(e)}") 2532 logger.traceback(e) 2533 return None
Append the comment of a specific test run in Xray using the GraphQL API. This method allows appending the comment associated with a test run identified by its ID. The comment can provide additional context, test results, or any other relevant information about the test execution. Args: test_run_id (str): The unique identifier of the test run to update. This is the internal Xray ID for the test run, not the Jira issue key. test_run_comment (str): The comment text to append to the test run. This will be added to any existing comment on the test run with proper formatting. Returns: bool: True if the comment update was successful, False otherwise. Returns None if an error occurs during the API request. Example:
client = XrayGraphQL() test_run_id = "67fcfd4b9e6d63d4c1d57b32" success = client.append_test_run_comment( ... test_run_id, ... "Test passed with performance within expected range" ... ) print(success) True Note: - The test run ID must be valid and accessible with current authentication - The comment can include any text content, including newlines and special characters - Failed updates are logged as errors with details about the failure - This method will append to existing comments with proper line breaks - If no existing comment exists, the new comment will be set as the initial comment Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.
2535 def download_attachment(self, jira_key: str, file_extension: str) -> Optional[Dict]: 2536 ''' 2537 Download a JIRA attachment by its ID. 2538 ''' 2539 try: 2540 response = self.make_jira_request(jira_key, 'GET') 2541 2542 if not response or 'fields' not in response: 2543 logger.error(f"Error: Could not retrieve issue {jira_key}") 2544 return None 2545 2546 # Find attachment by filename 2547 attachments = response.get('fields', {}).get('attachment', []) 2548 target_attachment = [att for att in attachments if att.get('filename').endswith(file_extension)] 2549 if not target_attachment: 2550 logger.error(f"No attachment found for {jira_key} with extension {file_extension}") 2551 return None 2552 2553 combined_attachment = [] 2554 for attachment in target_attachment: 2555 attachment_id = attachment.get('id') 2556 mime_type = attachment.get('mimeType', '') 2557 combined_attachment.append(self.download_jira_attachment_by_id(attachment_id, mime_type)) 2558 2559 return combined_attachment 2560 except Exception as e: 2561 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2562 logger.traceback(e) 2563 return None
Download a JIRA attachment by its ID.