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