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 result 1153 attachments { 1154 id 1155 filename 1156 storedInJira 1157 downloadLink 1158 } 1159 } 1160 1161 } 1162 } 1163 } 1164 """ 1165 variables = { 1166 "limit": 10, 1167 "jql": f"project = '{parse_project}' AND key = '{issue_key}'", 1168 "jiraFields": jira_fields 1169 } 1170 data = self._make_graphql_request(query, variables) 1171 if not data: 1172 logger.error(f"Failed to get issue ID for {issue_key}") 1173 return None 1174 for issue in data[function_name]['results']: 1175 if str(issue['jira']['key']).lower() == issue_key.lower(): 1176 return issue # This now includes all metadata 1177 return None 1178 except Exception as e: 1179 logger.error(f"Failed to get issue ID for {issue_key}") 1180 logger.traceback(e) 1181 return None 1182 1183 def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]: 1184 """ 1185 Retrieves all tests associated with a given test plan from Xray. 1186 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1187 test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that 1188 ID to fetch the associated tests. 1189 Args: 1190 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1191 Returns: 1192 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1193 or None if the operation fails. For example: 1194 { 1195 "PROJECT-124": "10001", 1196 "PROJECT-125": "10002" 1197 } 1198 Returns None in the following cases: 1199 - Test plan ID cannot be found 1200 - GraphQL request fails 1201 - Any other error occurs during processing 1202 Example: 1203 >>> client = XrayGraphQL() 1204 >>> tests = client.get_tests_from_test_plan("PROJECT-123") 1205 >>> print(tests) 1206 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1207 Note: 1208 - The method is limited to retrieving 99999 tests per test plan 1209 - Test plan must exist in Xray and be accessible with current authentication 1210 """ 1211 try: 1212 test_plan_id = self.get_issue_id_from_jira_id(test_plan) 1213 if not test_plan_id: 1214 logger.error(f"Failed to get test plan ID for {test_plan}") 1215 return None 1216 query = """ 1217 query GetTestPlanTests($testPlanId: String!) { 1218 getTestPlan(issueId: $testPlanId) { 1219 tests(limit: 100) { 1220 results { 1221 issueId 1222 jira(fields: ["key"]) 1223 } 1224 } 1225 } 1226 } 1227 """ 1228 variables = {"testPlanId": test_plan_id} 1229 data = self._make_graphql_request(query, variables) 1230 if not data: 1231 logger.error(f"Failed to get tests for plan {test_plan_id}") 1232 return None 1233 tests = {} 1234 for test in data['getTestPlan']['tests']['results']: 1235 tests[test['jira']['key']] = test['issueId'] 1236 return tests 1237 except Exception as e: 1238 logger.error(f"Failed to get tests for plan {test_plan_id}") 1239 logger.traceback(e) 1240 return None 1241 1242 def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]: 1243 """ 1244 Retrieves all tests associated with a given test set from Xray. 1245 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1246 test set. It first converts the JIRA test set key to an internal Xray ID, then uses that 1247 ID to fetch the associated tests. 1248 Args: 1249 test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") 1250 Returns: 1251 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1252 or None if the operation fails. For example: 1253 { 1254 "PROJECT-124": "10001", 1255 "PROJECT-125": "10002" 1256 } 1257 Returns None in the following cases: 1258 - Test set ID cannot be found 1259 - GraphQL request fails 1260 - Any other error occurs during processing 1261 Example: 1262 >>> client = XrayGraphQL() 1263 >>> tests = client.get_tests_from_test_set("PROJECT-123") 1264 >>> print(tests) 1265 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1266 Note: 1267 - The method is limited to retrieving 99999 tests per test set 1268 - Test set must exist in Xray and be accessible with current authentication 1269 """ 1270 try: 1271 test_set_id = self.get_issue_id_from_jira_id(test_set) 1272 if not test_set_id: 1273 logger.error(f"Failed to get test set ID for {test_set}") 1274 return None 1275 query = """ 1276 query GetTestSetTests($testSetId: String!) { 1277 getTestSet(issueId: $testSetId) { 1278 tests(limit: 100) { 1279 results { 1280 issueId 1281 jira(fields: ["key"]) 1282 } 1283 } 1284 } 1285 } 1286 """ 1287 variables = {"testSetId": test_set_id} 1288 data = self._make_graphql_request(query, variables) 1289 if not data: 1290 logger.error(f"Failed to get tests for set {test_set_id}") 1291 return None 1292 tests = {} 1293 for test in data['getTestSet']['tests']['results']: 1294 tests[test['jira']['key']] = test['issueId'] 1295 return tests 1296 except Exception as e: 1297 logger.error(f"Failed to get tests for set {test_set_id}") 1298 logger.traceback(e) 1299 return None 1300 1301 def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]: 1302 """ 1303 Retrieves all tests associated with a given test execution from Xray. 1304 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1305 test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that 1306 ID to fetch the associated tests. 1307 Args: 1308 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1309 Returns: 1310 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1311 or None if the operation fails. For example: 1312 { 1313 "PROJECT-124": "10001", 1314 "PROJECT-125": "10002" 1315 } 1316 Returns None in the following cases: 1317 - Test execution ID cannot be found 1318 - GraphQL request fails 1319 - Any other error occurs during processing 1320 Example: 1321 >>> client = XrayGraphQL() 1322 >>> tests = client.get_tests_from_test_execution("PROJECT-123") 1323 >>> print(tests) 1324 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1325 Note: 1326 - The method is limited to retrieving 99999 tests per test execution 1327 - Test execution must exist in Xray and be accessible with current authentication 1328 """ 1329 try: 1330 test_execution_id = self.get_issue_id_from_jira_id(test_execution) 1331 if not test_execution_id: 1332 logger.error(f"Failed to get test execution ID for {test_execution}") 1333 return None 1334 query = """ 1335 query GetTestExecutionTests($testExecutionId: String!) { 1336 getTestExecution(issueId: $testExecutionId) { 1337 tests(limit: 100) { 1338 results { 1339 issueId 1340 jira(fields: ["key"]) 1341 } 1342 } 1343 } 1344 } 1345 """ 1346 variables = {"testExecutionId": test_execution_id} 1347 data = self._make_graphql_request(query, variables) 1348 if not data: 1349 logger.error(f"Failed to get tests for execution {test_execution_id}") 1350 return None 1351 tests = {} 1352 for test in data['getTestExecution']['tests']['results']: 1353 tests[test['jira']['key']] = test['issueId'] 1354 return tests 1355 except Exception as e: 1356 logger.error(f"Failed to get tests for execution {test_execution_id}") 1357 logger.traceback(e) 1358 return None 1359 1360 def get_test_plan_data(self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]: 1361 """ 1362 Retrieve and parse tabular data from a test plan's description field in Xray. 1363 This method fetches a test plan's description from Xray and parses any tables found within it. 1364 The tables in the description are expected to be in a specific format that can be parsed by 1365 the _parse_table method. The parsed data is returned as a dictionary containing numeric values 1366 and lists extracted from the table. 1367 Args: 1368 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1369 Returns: 1370 Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data 1371 where keys are derived from the first column of the table and values are lists of numeric 1372 values. Returns None if: 1373 - The test plan ID cannot be found 1374 - The GraphQL request fails 1375 - The description cannot be parsed 1376 - Any other error occurs during processing 1377 Example: 1378 >>> client = XrayGraphQL() 1379 >>> data = client.get_test_plan_data("TEST-123") 1380 >>> print(data) 1381 { 1382 'temperature': [20, 25, 30], 1383 'pressure': [1.0, 1.5, 2.0], 1384 'measurements': [[1, 2, 3], [4, 5, 6]] 1385 } 1386 Note: 1387 - The test plan must exist in Xray and be accessible with current authentication 1388 - The description must contain properly formatted tables for parsing 1389 - Table values are converted to numeric types (int or float) where possible 1390 - Lists in table cells should be formatted as [value1, value2, ...] 1391 - Failed operations are logged as errors with relevant details 1392 """ 1393 try: 1394 test_plan_id = self.get_issue_id_from_jira_id(test_plan) 1395 if not test_plan_id: 1396 logger.error(f"Failed to get test plan ID for {test_plan}") 1397 return None 1398 query = """ 1399 query GetTestPlanTests($testPlanId: String!) { 1400 getTestPlan(issueId: $testPlanId) { 1401 issueId 1402 jira(fields: ["key","description"]) 1403 } 1404 } 1405 """ 1406 variables = {"testPlanId": test_plan_id} 1407 data = self._make_graphql_request(query, variables) 1408 if not data: 1409 logger.error(f"Failed to get tests for plan {test_plan_id}") 1410 return None 1411 description = data['getTestPlan']['jira']['description'] 1412 test_plan_data = self._parse_table(description) 1413 return test_plan_data 1414 except Exception as e: 1415 logger.error(f"Failed to get tests for plan {test_plan_id}") 1416 logger.traceback(e) 1417 return None 1418 1419 def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]: 1420 """ 1421 Retrieves all test sets that contain a specific test case from Xray. 1422 This method queries the Xray GraphQL API to find all test sets that include the specified 1423 test case. It first converts the JIRA test case key to an internal Xray ID, then uses that 1424 ID to fetch all associated test sets. 1425 Args: 1426 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1427 Returns: 1428 Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, 1429 or None if the operation fails. For example: 1430 { 1431 "PROJECT-124": "Test Set for Feature A", 1432 "PROJECT-125": "Regression Test Set" 1433 } 1434 Returns None in the following cases: 1435 - Test case ID cannot be found 1436 - GraphQL request fails 1437 - Any other error occurs during processing 1438 Example: 1439 >>> client = XrayGraphQL() 1440 >>> test_sets = client.filter_test_set_by_test_case("PROJECT-123") 1441 >>> print(test_sets) 1442 {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} 1443 Note: 1444 - The method is limited to retrieving 99999 test sets per test case 1445 - Test case must exist in Xray and be accessible with current authentication 1446 """ 1447 try: 1448 test_id = self.get_issue_id_from_jira_id(test_key) 1449 if not test_id: 1450 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1451 return None 1452 query = """ 1453 query GetTestDetails($testId: String!) { 1454 getTest(issueId: $testId) { 1455 testSets(limit: 100) { 1456 results { 1457 issueId 1458 jira(fields: ["key","summary"]) 1459 } 1460 } 1461 } 1462 } 1463 """ 1464 variables = { 1465 "testId": test_id 1466 } 1467 data = self._make_graphql_request(query, variables) 1468 if not data: 1469 logger.error(f"Failed to get tests for plan {test_id}") 1470 return None 1471 retDict = {} 1472 for test in data['getTest']['testSets']['results']: 1473 retDict[test['jira']['key']] = test['jira']['summary'] 1474 return retDict 1475 except Exception as e: 1476 logger.error(f"Error in getting test set by test id: {e}") 1477 logger.traceback(e) 1478 return None 1479 1480 def filter_tags_by_test_case(self, test_key: str) -> Optional[List[str]]: 1481 """ 1482 Extract and filter tags from test sets associated with a specific test case in Xray. 1483 This method queries the Xray GraphQL API to find all test sets associated with the given 1484 test case and extracts tags from their summaries. Tags are identified from test set summaries 1485 that start with either 'tag' or 'benchtype' prefixes. 1486 Args: 1487 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1488 Returns: 1489 Optional[List[str]]: A list of unique tags extracted from test set summaries, 1490 or None if no tags are found or an error occurs. Tags are: 1491 - Extracted from summaries starting with 'tag:' or 'benchtype:' 1492 - Split on commas, semicolons, double pipes, or whitespace 1493 - Converted to lowercase and stripped of whitespace 1494 Example: 1495 >>> client = XrayGraphQL() 1496 >>> tags = client.filter_tags_by_test_case("TEST-123") 1497 >>> print(tags) 1498 ['regression', 'smoke', 'performance'] 1499 Note: 1500 - Test sets must have summaries in the format "tag: tag1, tag2" or "benchtype: type1, type2" 1501 - Tags are extracted only from summaries with the correct prefix 1502 - All tags are converted to lowercase for consistency 1503 - Duplicate tags are automatically removed via set conversion 1504 - Returns None if no valid tags are found or if an error occurs 1505 """ 1506 try: 1507 test_id = self.get_issue_id_from_jira_id(test_key) 1508 if not test_id: 1509 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1510 return None 1511 query = """ 1512 query GetTestDetails($testId: String!) { 1513 getTest(issueId: $testId) { 1514 testSets(limit: 100) { 1515 results { 1516 issueId 1517 jira(fields: ["key","summary"]) 1518 } 1519 } 1520 } 1521 } 1522 """ 1523 variables = { 1524 "testId": test_id 1525 } 1526 data = self._make_graphql_request(query, variables) 1527 if not data: 1528 logger.error(f"Failed to get tests for plan {test_id}") 1529 return None 1530 tags = set() 1531 for test in data['getTest']['testSets']['results']: 1532 summary = str(test['jira']['summary']).strip().lower() 1533 if summary.startswith(('tag', 'benchtype')): 1534 summary = summary.split(':', 1)[-1].strip() # Split only once and take last part 1535 tags.update(tag for tag in re.split(r'[,|;|\|\|]\s*|\s+', summary) if tag) 1536 if tags: 1537 return list(tags) 1538 else: 1539 return None 1540 except Exception as e: 1541 logger.error(f"Error in getting test set by test id: {e}") 1542 logger.traceback(e) 1543 return None 1544 1545 def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]: 1546 """ 1547 Retrieve the status of a test run for a specific test case within a test execution. 1548 This method queries the Xray GraphQL API to get the current status of a test run, 1549 which represents the execution status of a specific test case within a test execution. 1550 It first converts both the test case and test execution JIRA keys to their internal 1551 Xray IDs, then uses these to fetch the test run status. 1552 Args: 1553 test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1554 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") 1555 Returns: 1556 Tuple[Optional[str], Optional[str]]: A tuple containing: 1557 - test_run_id: The unique identifier of the test run (or None if not found) 1558 - test_run_status: The current status of the test run (or None if not found) 1559 Returns (None, None) if any error occurs during the process. 1560 Example: 1561 >>> client = XrayGraphQL() 1562 >>> run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") 1563 >>> print(f"Test Run ID: {run_id}, Status: {status}") 1564 Test Run ID: 10001, Status: PASS 1565 Note: 1566 - Both the test case and test execution must exist in Xray and be accessible 1567 - The test case must be associated with the test execution 1568 - The method performs two ID lookups before querying the test run status 1569 - Failed operations are logged as errors with relevant details 1570 """ 1571 try: 1572 test_case_id = self.get_issue_id_from_jira_id(test_case) 1573 if not test_case_id: 1574 logger.error(f"Failed to get test ID for Test Case ({test_case})") 1575 return None 1576 test_exec_id = self.get_issue_id_from_jira_id(test_execution) 1577 if not test_exec_id: 1578 logger.error(f"Failed to get test execution ID for Test Execution ({test_execution})") 1579 return None 1580 query = """ 1581 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1582 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1583 id 1584 status { 1585 name 1586 } 1587 } 1588 } 1589 """ 1590 variables = { 1591 "testId": test_case_id, 1592 "testExecutionId": test_exec_id, 1593 } 1594 # Add debug loggerging 1595 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_exec_id}") 1596 data = self._make_graphql_request(query, variables) 1597 if not data: 1598 logger.error(f"Failed to get test run status for test {test_case_id}") 1599 return None 1600 # jprint(data) 1601 test_run_id = data['getTestRun']['id'] 1602 test_run_status = data['getTestRun']['status']['name'] 1603 return (test_run_id, test_run_status) 1604 except Exception as e: 1605 logger.error(f"Error getting test run status: {str(e)}") 1606 logger.traceback(e) 1607 return (None, None) 1608 1609 def get_test_run_by_id(self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]: 1610 """ 1611 Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. 1612 Args: 1613 test_case_id (str): The ID of the test case to query 1614 test_execution_id (str): The ID of the test execution containing the test run 1615 Returns: 1616 tuple[Optional[str], Optional[str]]: A tuple containing: 1617 - test_run_id: The ID of the test run if found, None if not found or on error 1618 - test_run_status: The status name of the test run if found, None if not found or on error 1619 Note: 1620 The function makes a GraphQL request to fetch the test run information. If the request fails 1621 or encounters any errors, it will log the error and return (None, None). 1622 """ 1623 try: 1624 query = """ 1625 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1626 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1627 id 1628 status { 1629 name 1630 } 1631 } 1632 } 1633 """ 1634 variables = { 1635 "testId": test_case_id, 1636 "testExecutionId": test_execution_id, 1637 } 1638 # Add debug loggerging 1639 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_execution_id}") 1640 data = self._make_graphql_request(query, variables) 1641 if not data: 1642 logger.error(f"Failed to get test run status for test {test_case_id}") 1643 return None 1644 test_run_id = data['getTestRun']['id'] 1645 test_run_status = data['getTestRun']['status']['name'] 1646 return (test_run_id, test_run_status) 1647 except Exception as e: 1648 logger.error(f"Error getting test run status: {str(e)}") 1649 logger.traceback(e) 1650 return (None, None) 1651 1652 def get_test_execution(self, test_execution: str) -> Optional[Dict]: 1653 """ 1654 Retrieve detailed information about a test execution from Xray. 1655 This method queries the Xray GraphQL API to fetch information about a specific test execution, 1656 including its ID and associated tests. It first converts the JIRA test execution key to an 1657 internal Xray ID, then uses that ID to fetch the execution details. 1658 Args: 1659 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1660 Returns: 1661 Optional[Dict]: A dictionary containing test execution details if successful, None if failed. 1662 The dictionary has the following structure: 1663 { 1664 'id': str, # The internal Xray ID of the test execution 1665 'tests': { # Dictionary mapping test keys to their IDs 1666 'TEST-124': '10001', 1667 'TEST-125': '10002', 1668 ... 1669 } 1670 } 1671 Returns None in the following cases: 1672 - Test execution ID cannot be found 1673 - GraphQL request fails 1674 - No test execution found with the given ID 1675 - No tests found in the test execution 1676 - Any other error occurs during processing 1677 Example: 1678 >>> client = XrayGraphQL() 1679 >>> execution = client.get_test_execution("TEST-123") 1680 >>> print(execution) 1681 { 1682 'id': '10000', 1683 'tests': { 1684 'TEST-124': '10001', 1685 'TEST-125': '10002' 1686 } 1687 } 1688 Note: 1689 - The method is limited to retrieving 99999 tests per test execution 1690 - Test execution must exist in Xray and be accessible with current authentication 1691 - Failed operations are logged with appropriate error or warning messages 1692 """ 1693 try: 1694 test_execution_id = self.get_issue_id_from_jira_id(test_execution) 1695 if not test_execution_id: 1696 logger.error(f"Failed to get test execution ID for {test_execution}") 1697 return None 1698 query = """ 1699 query GetTestExecution($testExecutionId: String!) { 1700 getTestExecution(issueId: $testExecutionId) { 1701 issueId 1702 projectId 1703 jira(fields: ["key", "summary", "description", "status"]) 1704 tests(limit: 100) { 1705 total 1706 start 1707 limit 1708 results { 1709 issueId 1710 jira(fields: ["key"]) 1711 } 1712 } 1713 } 1714 } 1715 """ 1716 variables = { 1717 "testExecutionId": test_execution_id 1718 } 1719 # Add debug loggerging 1720 logger.debug(f"Getting test execution details for {test_execution_id}") 1721 data = self._make_graphql_request(query, variables) 1722 # jprint(data) 1723 if not data: 1724 logger.error(f"Failed to get test execution details for {test_execution_id}") 1725 return None 1726 test_execution = data.get('getTestExecution',{}) 1727 if not test_execution: 1728 logger.warning(f"No test execution found with ID {test_execution_id}") 1729 return None 1730 tests = test_execution.get('tests',{}) 1731 if not tests: 1732 logger.warning(f"No tests found for test execution {test_execution_id}") 1733 return None 1734 tests_details = dict() 1735 for test in tests['results']: 1736 tests_details[test['jira']['key']] = test['issueId'] 1737 formatted_response = { 1738 'id': test_execution['issueId'], 1739 'tests': tests_details 1740 } 1741 return formatted_response 1742 except Exception as e: 1743 logger.error(f"Error getting test execution details: {str(e)}") 1744 logger.traceback(e) 1745 return None 1746 1747 def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]: 1748 """ 1749 Add a test execution to an existing test plan in Xray. 1750 This method associates a test execution with a test plan using the Xray GraphQL API. 1751 It first converts both the test plan and test execution JIRA keys to their internal 1752 Xray IDs, then creates the association between them. 1753 Args: 1754 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1755 test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") 1756 Returns: 1757 Optional[Dict]: A dictionary containing the response data if successful, None if failed. 1758 The dictionary has the following structure: 1759 { 1760 'addTestExecutionsToTestPlan': { 1761 'addedTestExecutions': [str], # List of added test execution IDs 1762 'warning': str # Any warnings from the operation 1763 } 1764 } 1765 Returns None in the following cases: 1766 - Test plan ID cannot be found 1767 - Test execution ID cannot be found 1768 - GraphQL request fails 1769 - Any other error occurs during processing 1770 Example: 1771 >>> client = XrayGraphQL() 1772 >>> result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") 1773 >>> print(result) 1774 { 1775 'addTestExecutionsToTestPlan': { 1776 'addedTestExecutions': ['10001'], 1777 'warning': None 1778 } 1779 } 1780 Note: 1781 - Both the test plan and test execution must exist in Xray and be accessible 1782 - The method performs two ID lookups before creating the association 1783 - Failed operations are logged as errors with relevant details 1784 """ 1785 try: 1786 test_plan_id = self.get_issue_id_from_jira_id(test_plan) 1787 if not test_plan_id: 1788 logger.error(f"Test plan ID is required") 1789 return None 1790 test_exec_id = self.get_issue_id_from_jira_id(test_execution) 1791 if not test_exec_id: 1792 logger.error(f"Test execution ID is required") 1793 return None 1794 query = """ 1795 mutation AddTestExecutionToTestPlan($testPlanId: String!, $testExecutionIds: [String!]!) { 1796 addTestExecutionsToTestPlan(issueId: $testPlanId, testExecIssueIds: $testExecutionIds) { 1797 addedTestExecutions 1798 warning 1799 } 1800 } 1801 """ 1802 variables = { 1803 "testPlanId": test_plan_id, 1804 "testExecutionIds": [test_exec_id] 1805 } 1806 data = self._make_graphql_request(query, variables) 1807 return data 1808 except Exception as e: 1809 logger.error(f"Error adding test execution to test plan: {str(e)}") 1810 logger.traceback(e) 1811 return None 1812 1813 def create_test_execution(self, 1814 test_issue_keys: List[str], 1815 project_key: Optional[str] = None, 1816 summary: Optional[str] = None, 1817 description: Optional[str] = None) -> Optional[Dict]: 1818 """ 1819 Create a new test execution in Xray with specified test cases. 1820 This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. 1821 It handles validation of test issue keys, automatically derives project information if not provided, 1822 and creates appropriate default values for summary and description if not specified. 1823 Args: 1824 test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution 1825 (e.g., ["TEST-123", "TEST-124"]) 1826 project_key (Optional[str]): The JIRA project key where the test execution should be created. 1827 If not provided, it will be derived from the first test issue key. 1828 summary (Optional[str]): The summary/title for the test execution ticket. 1829 If not provided, a default summary will be generated using the test issue keys. 1830 description (Optional[str]): The description for the test execution ticket. 1831 If not provided, a default description will be generated using the test issue keys. 1832 Returns: 1833 Optional[Dict]: A dictionary containing the created test execution details if successful, 1834 None if the creation fails. The dictionary has the following structure: 1835 { 1836 'issueId': str, # The internal Xray ID of the created test execution 1837 'jira': { 1838 'key': str # The JIRA issue key of the created test execution 1839 } 1840 } 1841 Example: 1842 >>> client = XrayGraphQL() 1843 >>> test_execution = client.create_test_execution( 1844 ... test_issue_keys=["TEST-123", "TEST-124"], 1845 ... project_key="TEST", 1846 ... summary="Sprint 1 Regression Tests" 1847 ... ) 1848 >>> print(test_execution) 1849 {'issueId': '10001', 'jira': {'key': 'TEST-125'}} 1850 Note: 1851 - Invalid test issue keys are logged as warnings but don't prevent execution creation 1852 - At least one valid test issue key is required 1853 - The method validates each test issue key before creating the execution 1854 - Project key is automatically derived from the first test issue key if not provided 1855 """ 1856 try: 1857 invalid_keys = [] 1858 test_issue_ids = [] 1859 for key in test_issue_keys: 1860 test_issue_id = self.get_issue_id_from_jira_id(key) 1861 if test_issue_id: 1862 test_issue_ids.append(test_issue_id) 1863 else: 1864 invalid_keys.append(key) 1865 if len(test_issue_ids) == 0: 1866 logger.error(f"No valid test issue keys provided: {invalid_keys}") 1867 return None 1868 if len(invalid_keys) > 0: 1869 logger.warning(f"Invalid test issue keys: {invalid_keys}") 1870 if not project_key: 1871 project_key = test_issue_keys[0].split("-")[0] 1872 if not summary: 1873 summary = f"Test Execution for Test Plan {test_issue_keys}" 1874 if not description: 1875 description = f"Test Execution for Test Plan {test_issue_keys}" 1876 mutation = """ 1877 mutation CreateTestExecutionForTestPlan( 1878 $testIssueId_list: [String!]!, 1879 $projectKey: String!, 1880 $summary: String!, 1881 $description: String 1882 ) { 1883 createTestExecution( 1884 testIssueIds: $testIssueId_list, 1885 jira: { 1886 fields: { 1887 project: { key: $projectKey }, 1888 summary: $summary, 1889 description: $description, 1890 issuetype: { name: "Test Execution" } 1891 } 1892 } 1893 ) { 1894 testExecution { 1895 issueId 1896 jira(fields: ["key"]) 1897 } 1898 warnings 1899 } 1900 } 1901 """ 1902 variables = { 1903 "testIssueId_list": test_issue_ids, 1904 "projectKey": project_key, 1905 "summary": summary, 1906 "description": description 1907 } 1908 data = self._make_graphql_request(mutation, variables) 1909 if not data: 1910 return None 1911 execution_details = data['createTestExecution']['testExecution'] 1912 # logger.info(f"Created test execution {execution_details['jira']['key']}") 1913 return execution_details 1914 except Exception as e: 1915 logger.error("Failed to create test execution : {e}") 1916 logger.traceback(e) 1917 return None 1918 1919 def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]: 1920 """Creates a test execution from a given test plan and associates all tests from the plan with the execution. 1921 This method performs several operations in sequence: 1922 1. Retrieves all tests from the specified test plan 1923 2. Creates a new test execution with those tests 1924 3. Associates the new test execution with the original test plan 1925 4. Creates test runs for each test in the execution 1926 Parameters 1927 ---------- 1928 test_plan : str 1929 The JIRA issue key of the test plan (e.g., "PROJECT-123") 1930 Returns 1931 ------- 1932 Optional[Dict[str, Dict[str, str]]] 1933 A dictionary mapping test case keys to their execution details, or None if the operation fails. 1934 The dictionary structure is:: 1935 { 1936 "TEST-123": { # Test case JIRA key 1937 "test_run_id": "12345", # Unique ID for this test run 1938 "test_execution_key": "TEST-456", # JIRA key of the created test execution 1939 "test_plan_key": "TEST-789" # Original test plan JIRA key 1940 }, 1941 "TEST-124": { 1942 ... 1943 } 1944 } 1945 Returns None in the following cases: 1946 * Test plan parameter is empty or invalid 1947 * No tests found in the test plan 1948 * Test execution creation fails 1949 * API request fails 1950 Examples 1951 -------- 1952 >>> client = XrayGraphQL() 1953 >>> result = client.create_test_execution_from_test_plan("TEST-123") 1954 >>> print(result) 1955 { 1956 "TEST-124": { 1957 "test_run_id": "5f7c3", 1958 "test_execution_key": "TEST-456", 1959 "test_plan_key": "TEST-123" 1960 }, 1961 "TEST-125": { 1962 "test_run_id": "5f7c4", 1963 "test_execution_key": "TEST-456", 1964 "test_plan_key": "TEST-123" 1965 } 1966 } 1967 Notes 1968 ----- 1969 - The test plan must exist and be accessible in Xray 1970 - All tests in the test plan must be valid and accessible 1971 - The method automatically generates a summary and description for the test execution 1972 - The created test execution is automatically linked back to the original test plan 1973 """ 1974 try: 1975 if not test_plan: 1976 logger.error("Test plan is required [ jira key]") 1977 return None 1978 project_key = test_plan.split("-")[0] 1979 summary = f"Test Execution for Test Plan {test_plan}" 1980 retDict = dict() 1981 #Get tests from test plan 1982 tests = self.get_tests_from_test_plan(test_plan) 1983 retDict["tests"] = tests 1984 testIssueId_list = list(tests.values()) 1985 # logger.info(f"Tests: {tests}") 1986 if not testIssueId_list: 1987 logger.error(f"No tests found for {test_plan}") 1988 return None 1989 description = f"Test Execution for {len(tests)} Test cases" 1990 # GraphQL mutation to create test execution 1991 query = """ 1992 mutation CreateTestExecutionForTestPlan( 1993 $testIssueId_list: [String!]!, 1994 $projectKey: String!, 1995 $summary: String!, 1996 $description: String 1997 ) { 1998 createTestExecution( 1999 testIssueIds: $testIssueId_list, 2000 jira: { 2001 fields: { 2002 project: { key: $projectKey }, 2003 summary: $summary, 2004 description: $description, 2005 issuetype: { name: "Test Execution" } 2006 } 2007 } 2008 ) { 2009 testExecution { 2010 issueId 2011 jira(fields: ["key"]) 2012 testRuns(limit: 100) { 2013 results { 2014 id 2015 test { 2016 issueId 2017 jira(fields: ["key"]) 2018 } 2019 } 2020 } 2021 } 2022 warnings 2023 } 2024 } 2025 """ 2026 variables = { 2027 "testIssueId_list": testIssueId_list, 2028 "projectKey": project_key, 2029 "summary": summary, 2030 "description": description or f"Test execution for total of {len(testIssueId_list)} test cases" 2031 } 2032 data = self._make_graphql_request(query, variables) 2033 if not data: 2034 return None 2035 test_exec_key = data['createTestExecution']['testExecution']['jira']['key'] 2036 test_exec_id = data['createTestExecution']['testExecution']['issueId'] 2037 #Add Test execution to test plan 2038 test_exec_dict= self.add_test_execution_to_test_plan(test_plan, test_exec_key) 2039 #Get test runs for test execution 2040 test_runs = data['createTestExecution']['testExecution']['testRuns']['results'] 2041 test_run_dict = dict() 2042 for test_run in test_runs: 2043 test_run_dict[test_run['test']['jira']['key']] = dict() 2044 test_run_dict[test_run['test']['jira']['key']]['test_run_id'] = test_run['id'] 2045 # test_run_dict[test_run['test']['jira']['key']]['test_issue_id'] = test_run['test']['issueId'] 2046 test_run_dict[test_run['test']['jira']['key']]['test_execution_key'] = test_exec_key 2047 # test_run_dict[test_run['test']['jira']['key']]['test_execution_id'] = test_exec_id 2048 test_run_dict[test_run['test']['jira']['key']]['test_plan_key'] = test_plan 2049 return test_run_dict 2050 except requests.exceptions.RequestException as e: 2051 logger.error(f"Error creating test execution: {e}") 2052 logger.traceback(e) 2053 return None 2054 2055 def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool: 2056 """ 2057 Update the status of a specific test run in Xray using the GraphQL API. 2058 This method allows updating the execution status of a test run identified by its ID. 2059 The status can be changed to reflect the current state of the test execution 2060 (e.g., "PASS", "FAIL", "TODO", etc.). 2061 Args: 2062 test_run_id (str): The unique identifier of the test run to update. 2063 This is the internal Xray ID for the test run, not the Jira issue key. 2064 test_run_status (str): The new status to set for the test run. 2065 Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. 2066 Returns: 2067 bool: True if the status update was successful, False otherwise. 2068 Returns None if an error occurs during the API request. 2069 Example: 2070 >>> client = XrayGraphQL() 2071 >>> test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") 2072 >>> success = client.update_test_run_status(test_run_id, "PASS") 2073 >>> print(success) 2074 True 2075 Note: 2076 - The test run ID must be valid and accessible with current authentication 2077 - The status value should be one of the valid status values configured in your Xray instance 2078 - Failed updates are logged as errors with details about the failure 2079 Raises: 2080 Exception: If there is an error making the GraphQL request or processing the response. 2081 The exception is caught and logged, and the method returns None. 2082 """ 2083 try: 2084 query = """ 2085 mutation UpdateTestRunStatus($testRunId: String!, $status: String!) { 2086 updateTestRunStatus( 2087 id: $testRunId, 2088 status: $status 2089 ) 2090 } 2091 """ 2092 variables = { 2093 "testRunId": test_run_id, 2094 "status": test_run_status 2095 } 2096 data = self._make_graphql_request(query, variables) 2097 if not data: 2098 logger.error(f"Failed to get test run status for test {data}") 2099 return None 2100 # logger.info(f"Test run status updated: {data}") 2101 return data['updateTestRunStatus'] 2102 except Exception as e: 2103 logger.error(f"Error updating test run status: {str(e)}") 2104 logger.traceback(e) 2105 return None 2106 2107 def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2108 """ 2109 Update the comment of a specific test run in Xray using the GraphQL API. 2110 This method allows adding or updating the comment associated with a test run 2111 identified by its ID. The comment can provide additional context, test results, 2112 or any other relevant information about the test execution. 2113 Args: 2114 test_run_id (str): The unique identifier of the test run to update. 2115 This is the internal Xray ID for the test run, not the Jira issue key. 2116 test_run_comment (str): The new comment text to set for the test run. 2117 This will replace any existing comment on the test run. 2118 Returns: 2119 bool: True if the comment update was successful, False otherwise. 2120 Returns None if an error occurs during the API request. 2121 Example: 2122 >>> client = XrayGraphQL() 2123 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2124 >>> success = client.update_test_run_comment( 2125 ... test_run_id, 2126 ... "Test passed with performance within expected range" 2127 ... ) 2128 >>> print(success) 2129 True 2130 Note: 2131 - The test run ID must be valid and accessible with current authentication 2132 - The comment can include any text content, including newlines and special characters 2133 - Failed updates are logged as errors with details about the failure 2134 - This method will overwrite any existing comment on the test run 2135 Raises: 2136 Exception: If there is an error making the GraphQL request or processing the response. 2137 The exception is caught and logged, and the method returns None. 2138 """ 2139 try: 2140 query = """ 2141 mutation UpdateTestRunStatus($testRunId: String!, $comment: String!) { 2142 updateTestRunComment( 2143 id: $testRunId, 2144 comment: $comment 2145 ) 2146 } 2147 """ 2148 variables = { 2149 "testRunId": test_run_id, 2150 "comment": test_run_comment 2151 } 2152 data = self._make_graphql_request(query, variables) 2153 if not data: 2154 logger.error(f"Failed to get test run comment for test {data}") 2155 return None 2156 # jprint(data) 2157 return data['updateTestRunComment'] 2158 except Exception as e: 2159 logger.error(f"Error updating test run comment: {str(e)}") 2160 logger.traceback(e) 2161 return None 2162 2163 def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool: 2164 """Add evidence (attachments) to a test run in Xray. 2165 This method allows attaching files as evidence to a specific test run. The file is 2166 read, converted to base64, and uploaded to Xray with appropriate MIME type detection. 2167 Parameters 2168 ---------- 2169 test_run_id : str 2170 The unique identifier of the test run to add evidence to 2171 evidence_path : str 2172 The local file system path to the evidence file to be attached 2173 Returns 2174 ------- 2175 bool 2176 True if the evidence was successfully added, None if the operation failed. 2177 Returns None in the following cases: 2178 - Test run ID is not provided 2179 - Evidence path is not provided 2180 - Evidence file does not exist 2181 - GraphQL request fails 2182 - Any other error occurs during processing 2183 Examples 2184 -------- 2185 >>> client = XrayGraphQL() 2186 >>> success = client.add_evidence_to_test_run( 2187 ... test_run_id="10001", 2188 ... evidence_path="/path/to/screenshot.png" 2189 ... ) 2190 >>> print(success) 2191 True 2192 Notes 2193 ----- 2194 - The evidence file must exist and be accessible 2195 - The file is automatically converted to base64 for upload 2196 - MIME type is automatically detected, defaults to "text/plain" if detection fails 2197 - The method supports various file types (images, documents, logs, etc.) 2198 - Failed operations are logged with appropriate error messages 2199 """ 2200 try: 2201 if not test_run_id: 2202 logger.error("Test run ID is required") 2203 return None 2204 if not evidence_path: 2205 logger.error("Evidence path is required") 2206 return None 2207 if not os.path.exists(evidence_path): 2208 logger.error(f"Evidence file not found: {evidence_path}") 2209 return None 2210 #if file exists then read the file in base64 2211 evidence_base64 = None 2212 mime_type = None 2213 filename = os.path.basename(evidence_path) 2214 with open(evidence_path, "rb") as file: 2215 evidence_data = file.read() 2216 evidence_base64 = base64.b64encode(evidence_data).decode('utf-8') 2217 mime_type = mimetypes.guess_type(evidence_path)[0] 2218 logger.info(f"For loop -- Mime type: {mime_type}") 2219 if not mime_type: 2220 mime_type = "text/plain" 2221 query = """ 2222 mutation AddEvidenceToTestRun($testRunId: String!, $filename: String!, $mimeType: String!, $evidenceBase64: String!) { 2223 addEvidenceToTestRun( 2224 id: $testRunId, 2225 evidence: [ 2226 { 2227 filename : $filename, 2228 mimeType : $mimeType, 2229 data : $evidenceBase64 2230 } 2231 ] 2232 ) { 2233 addedEvidence 2234 warnings 2235 } 2236 } 2237 """ 2238 variables = { 2239 "testRunId": test_run_id, 2240 "filename": filename, 2241 "mimeType": mime_type, 2242 "evidenceBase64": evidence_base64 2243 } 2244 data = self._make_graphql_request(query, variables) 2245 if not data: 2246 logger.error(f"Failed to add evidence to test run: {data}") 2247 return None 2248 return data['addEvidenceToTestRun'] 2249 except Exception as e: 2250 logger.error(f"Error adding evidence to test run: {str(e)}") 2251 logger.traceback(e) 2252 return None 2253 2254 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]: 2255 """Create a defect from a test run and link it to the test run in Xray. 2256 This method performs two main operations: 2257 1. Creates a new defect in JIRA with the specified summary and description 2258 2. Links the created defect to the specified test run in Xray 2259 Parameters 2260 ---------- 2261 test_run_id : str 2262 The ID of the test run to create defect from 2263 project_key : str 2264 The JIRA project key where the defect should be created. 2265 If not provided, defaults to "EAGVAL" 2266 parent_issue_key : str 2267 The JIRA key of the parent issue to link the defect to 2268 defect_summary : str 2269 Summary/title of the defect. 2270 If not provided, defaults to "Please provide a summary for the defect" 2271 defect_description : str 2272 Description of the defect. 2273 If not provided, defaults to "Please provide a description for the defect" 2274 Returns 2275 ------- 2276 Optional[Dict] 2277 Response data from the GraphQL API if successful, None if failed. 2278 The response includes: 2279 - addedDefects: List of added defects 2280 - warnings: Any warnings from the operation 2281 Examples 2282 -------- 2283 >>> client = XrayGraphQL() 2284 >>> result = client.create_defect_from_test_run( 2285 ... test_run_id="10001", 2286 ... project_key="PROJ", 2287 ... parent_issue_key="PROJ-456", 2288 ... defect_summary="Test failure in login flow", 2289 ... defect_description="The login button is not responding to clicks" 2290 ... ) 2291 >>> print(result) 2292 { 2293 'addedDefects': ['PROJ-123'], 2294 'warnings': [] 2295 } 2296 Notes 2297 ----- 2298 - The project_key will be split on '-' and only the first part will be used 2299 - The defect will be created with issue type 'Bug' 2300 - The method handles missing parameters with default values 2301 - The parent issue must exist and be accessible to create the defect 2302 """ 2303 try: 2304 if not project_key: 2305 project_key = "EAGVAL" 2306 if not defect_summary: 2307 defect_summary = "Please provide a summary for the defect" 2308 if not defect_description: 2309 defect_description = "Please provide a description for the defect" 2310 project_key = project_key.split("-")[0] 2311 # Fix: Correct parameter order for create_issue 2312 defect_key, defect_id = self.create_issue( 2313 project_key=project_key, 2314 parent_issue_key=parent_issue_key, 2315 summary=defect_summary, 2316 description=defect_description, 2317 issue_type='Bug' 2318 ) 2319 if not defect_key: 2320 logger.error("Failed to create defect issue") 2321 return None 2322 # Then add the defect to the test run 2323 add_defect_mutation = """ 2324 mutation AddDefectsToTestRun( 2325 $testRunId: String!, 2326 $defectKey: String! 2327 ) { 2328 addDefectsToTestRun( id: $testRunId, issues: [$defectKey]) { 2329 addedDefects 2330 warnings 2331 } 2332 } 2333 """ 2334 variables = { 2335 "testRunId": test_run_id, 2336 "defectKey": defect_key 2337 } 2338 data = None 2339 retry_count = 0 2340 while retry_count < 3: 2341 data = self._make_graphql_request(add_defect_mutation, variables) 2342 if not data: 2343 logger.error(f"Failed to add defect [{defect_key}] to test run - {test_run_id}.. retrying... {retry_count}") 2344 retry_count += 1 2345 time.sleep(1) 2346 else: 2347 break 2348 return data 2349 except Exception as e: 2350 logger.error(f"Error creating defect from test run: {str(e)}") 2351 logger.traceback(e) 2352 return None 2353 2354 def get_test_run_comment(self, test_run_id: str) -> Optional[str]: 2355 """ 2356 Retrieve the comment of a specific test run from Xray using the GraphQL API. 2357 This method allows retrieving the comment associated with a test run 2358 identified by its ID. The comment can provide additional context, test results, 2359 or any other relevant information about the test execution. 2360 Args: 2361 test_run_id (str): The unique identifier of the test run to retrieve comment from. 2362 This is the internal Xray ID for the test run, not the Jira issue key. 2363 Returns: 2364 Optional[str]: The comment text of the test run if successful, None if: 2365 - The test run ID is not found 2366 - The GraphQL request fails 2367 - No comment exists for the test run 2368 - Any other error occurs during the API request 2369 Example: 2370 >>> client = XrayGraphQL() 2371 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2372 >>> comment = client.get_test_run_comment(test_run_id) 2373 >>> print(comment) 2374 "Test passed with performance within expected range" 2375 Note: 2376 - The test run ID must be valid and accessible with current authentication 2377 - If no comment exists for the test run, the method will return None 2378 - Failed requests are logged as errors with details about the failure 2379 - The method returns the raw comment text as stored in Xray 2380 Raises: 2381 Exception: If there is an error making the GraphQL request or processing the response. 2382 The exception is caught and logged, and the method returns None. 2383 """ 2384 try: 2385 # Try the direct ID approach first 2386 query = """ 2387 query GetTestRunComment($testRunId: String!) { 2388 getTestRunById(id: $testRunId) { 2389 id 2390 comment 2391 status { 2392 name 2393 } 2394 } 2395 } 2396 """ 2397 variables = { 2398 "testRunId": test_run_id 2399 } 2400 data = self._make_graphql_request(query, variables) 2401 # jprint(data) 2402 if not data: 2403 logger.error(f"Failed to get test run comment for test run {test_run_id}") 2404 return None 2405 test_run = data.get('getTestRunById', {}) 2406 if not test_run: 2407 logger.warning(f"No test run found with ID {test_run_id}") 2408 return None 2409 comment = test_run.get('comment') 2410 return comment 2411 except Exception as e: 2412 logger.error(f"Error getting test run comment: {str(e)}") 2413 logger.traceback(e) 2414 return None 2415 2416 def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2417 """ 2418 Append the comment of a specific test run in Xray using the GraphQL API. 2419 This method allows appending the comment associated with a test run 2420 identified by its ID. The comment can provide additional context, test results, 2421 or any other relevant information about the test execution. 2422 Args: 2423 test_run_id (str): The unique identifier of the test run to update. 2424 This is the internal Xray ID for the test run, not the Jira issue key. 2425 test_run_comment (str): The comment text to append to the test run. 2426 This will be added to any existing comment on the test run with proper formatting. 2427 Returns: 2428 bool: True if the comment update was successful, False otherwise. 2429 Returns None if an error occurs during the API request. 2430 Example: 2431 >>> client = XrayGraphQL() 2432 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2433 >>> success = client.append_test_run_comment( 2434 ... test_run_id, 2435 ... "Test passed with performance within expected range" 2436 ... ) 2437 >>> print(success) 2438 True 2439 Note: 2440 - The test run ID must be valid and accessible with current authentication 2441 - The comment can include any text content, including newlines and special characters 2442 - Failed updates are logged as errors with details about the failure 2443 - This method will append to existing comments with proper line breaks 2444 - If no existing comment exists, the new comment will be set as the initial comment 2445 Raises: 2446 Exception: If there is an error making the GraphQL request or processing the response. 2447 The exception is caught and logged, and the method returns None. 2448 """ 2449 try: 2450 # Get existing comment 2451 existing_comment = self.get_test_run_comment(test_run_id) 2452 # Prepare the combined comment with proper formatting 2453 if existing_comment: 2454 # If there's an existing comment, append with double newline for proper separation 2455 combined_comment = f"{existing_comment}\n{test_run_comment}" 2456 else: 2457 # If no existing comment, use the new comment as is 2458 combined_comment = test_run_comment 2459 logger.debug(f"No existing comment found for test run {test_run_id}, setting initial comment") 2460 query = """ 2461 mutation UpdateTestRunComment($testRunId: String!, $comment: String!) { 2462 updateTestRunComment( 2463 id: $testRunId, 2464 comment: $comment 2465 ) 2466 } 2467 """ 2468 variables = { 2469 "testRunId": test_run_id, 2470 "comment": combined_comment 2471 } 2472 data = self._make_graphql_request(query, variables) 2473 if not data: 2474 logger.error(f"Failed to update test run comment for test run {test_run_id}") 2475 return None 2476 return data['updateTestRunComment'] 2477 except Exception as e: 2478 logger.error(f"Error updating test run comment: {str(e)}") 2479 logger.traceback(e) 2480 return None 2481 2482 def download_attachment_by_extension(self, jira_key: str, file_extension: str) -> Optional[Dict]: 2483 """ 2484 Download JIRA attachments by file extension. 2485 2486 Retrieves all attachments from a JIRA issue that match the specified file extension 2487 and downloads their content. This method searches through all attachments on the 2488 issue and filters by filename ending with the provided extension. 2489 2490 Args: 2491 jira_key (str): The JIRA issue key (e.g., 'PROJ-123') 2492 file_extension (str): The file extension to search for (e.g., '.json', '.txt') 2493 Should include the dot prefix 2494 2495 Returns: 2496 Optional[Dict]: A list of dictionaries containing attachment data, where each 2497 dictionary has filename as key and attachment content as value. 2498 Returns None if: 2499 - Issue cannot be retrieved 2500 - No attachments found with the specified extension 2501 - Error occurs during download 2502 2503 Example: 2504 >>> client = XrayGraphQL() 2505 >>> attachments = client.download_attachment_by_extension('PROJ-123', '.json') 2506 >>> # Returns: [{'document.json': {'content': b'...', 'mime_type': 'application/json'}}] 2507 2508 Raises: 2509 Exception: Logged and handled internally, returns None on any error 2510 """ 2511 try: 2512 response = self.make_jira_request(jira_key, 'GET') 2513 2514 if not response or 'fields' not in response: 2515 logger.error(f"Error: Could not retrieve issue {jira_key}") 2516 return None 2517 2518 # Find attachment by filename 2519 attachments = response.get('fields', {}).get('attachment', []) 2520 target_attachment = [att for att in attachments if att.get('filename').endswith(file_extension)] 2521 if not target_attachment: 2522 logger.error(f"No attachment found for {jira_key} with extension {file_extension}") 2523 return None 2524 2525 combined_attachment = [] 2526 fileDetails = dict() 2527 for attachment in target_attachment: 2528 attachment_id = attachment.get('id') 2529 mime_type = attachment.get('mimeType', '') 2530 fileDetails[attachment.get('filename')] = self.download_jira_attachment_by_id(attachment_id, mime_type) 2531 combined_attachment.append(fileDetails) 2532 2533 return combined_attachment 2534 except Exception as e: 2535 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2536 logger.traceback(e) 2537 return None 2538 2539 def download_attachment_by_name(self, jira_key: str, file_name: str) -> Optional[Dict]: 2540 """ 2541 Download JIRA attachments by filename prefix. 2542 2543 Retrieves all attachments from a JIRA issue whose filenames start with the 2544 specified name (case-insensitive). This method searches through all attachments 2545 on the issue and filters by filename starting with the provided name. 2546 2547 Args: 2548 jira_key (str): The JIRA issue key (e.g., 'PROJ-123') 2549 file_name (str): The filename prefix to search for (e.g., 'report', 'test') 2550 Case-insensitive matching is performed 2551 2552 Returns: 2553 Optional[Dict]: A list of dictionaries containing attachment data, where each 2554 dictionary has filename as key and attachment content as value. 2555 Returns None if: 2556 - Issue cannot be retrieved 2557 - No attachments found with the specified filename prefix 2558 - Error occurs during download 2559 2560 Example: 2561 >>> client = XrayGraphQL() 2562 >>> attachments = client.download_attachment_by_name('PROJ-123', 'report') 2563 >>> # Returns: [{'report_v1.json': {'content': b'...', 'mime_type': 'application/json'}}, 2564 >>> # {'report_v2.json': {'content': b'...', 'mime_type': 'application/json'}}] 2565 2566 Raises: 2567 Exception: Logged and handled internally, returns None on any error 2568 """ 2569 try: 2570 response = self.make_jira_request(jira_key, 'GET') 2571 2572 if not response or 'fields' not in response: 2573 logger.error(f"Error: Could not retrieve issue {jira_key}") 2574 return None 2575 2576 # Find attachment by filename 2577 attachments = response.get('fields', {}).get('attachment', []) 2578 target_attachment = [att for att in attachments if att.get('filename').lower().startswith(file_name.lower())] 2579 if not target_attachment: 2580 logger.error(f"No attachment found for {jira_key} with extension {file_name}") 2581 return None 2582 2583 combined_attachment = [] 2584 fileDetails = dict() 2585 for attachment in target_attachment: 2586 attachment_id = attachment.get('id') 2587 mime_type = attachment.get('mimeType', '') 2588 fileDetails[attachment.get('filename')] = self.download_jira_attachment_by_id(attachment_id, mime_type) 2589 combined_attachment.append(fileDetails) 2590 2591 return combined_attachment 2592 except Exception as e: 2593 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2594 logger.traceback(e) 2595 return None 2596 2597 def generate_json_from_sentence(self, sentence, template_schema, debug=False): 2598 """Extract information using template schema and spaCy components""" 2599 def _ensure_spacy_model(): 2600 """Ensure spaCy model is available, download if needed""" 2601 try: 2602 nlp = spacy.load("en_core_web_md") 2603 return nlp 2604 except OSError: 2605 import subprocess 2606 import sys 2607 logger.info("Downloading required spaCy model...") 2608 try: 2609 subprocess.check_call([ 2610 sys.executable, "-m", "spacy", "download", "en_core_web_md" 2611 ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 2612 return spacy.load("en_core_web_md") 2613 except subprocess.CalledProcessError: 2614 raise RuntimeError( 2615 "Failed to download spaCy model. Please run manually: " 2616 "python -m spacy download en_core_web_md" 2617 ) 2618 2619 def analyze_model_components(nlp): 2620 """Analyze the loaded model's components and capabilities""" 2621 logger.info("=== Model Analysis ===") 2622 logger.info(f"Model: {nlp.meta['name']}") 2623 logger.info(f"Version: {nlp.meta['version']}") 2624 logger.info(f"Pipeline: {nlp.pipe_names}") 2625 logger.info(f"Components: {list(nlp.pipeline)}") 2626 2627 def parse_template_pattern(pattern_value): 2628 """Parse template pattern to extract structure and placeholders""" 2629 if not isinstance(pattern_value, str): 2630 return None 2631 2632 # Extract placeholders like <string> from the pattern 2633 placeholders = re.findall(r'<(\w+)>', pattern_value) 2634 2635 # Create a regex pattern by replacing placeholders with capture groups 2636 regex_pattern = pattern_value 2637 for placeholder in placeholders: 2638 # Replace <string> with a regex that captures word characters 2639 regex_pattern = regex_pattern.replace(f'<{placeholder}>', r'(\w+)') 2640 2641 return { 2642 'original': pattern_value, 2643 'placeholders': placeholders, 2644 'regex_pattern': regex_pattern, 2645 'regex': re.compile(regex_pattern, re.IGNORECASE) 2646 } 2647 2648 def match_pattern_from_template(pattern_value, doc, debug=False): 2649 """Match a pattern based on template value and return the matched text""" 2650 2651 if isinstance(pattern_value, list): 2652 # Handle list of exact values (for environment, region) 2653 for value in pattern_value: 2654 for token in doc: 2655 if token.text.lower() == value.lower(): 2656 if debug: 2657 logger.info(f"✓ Matched list value '{value}' -> {token.text}") 2658 return token.text 2659 return None 2660 2661 elif isinstance(pattern_value, str): 2662 # Parse the template pattern dynamically 2663 pattern_info = parse_template_pattern(pattern_value) 2664 if not pattern_info: 2665 return None 2666 2667 if debug: 2668 logger.info(f"Parsed pattern: {pattern_info['original']}") 2669 logger.info(f"Regex pattern: {pattern_info['regex_pattern']}") 2670 2671 # Look for tokens that match the pattern 2672 for token in doc: 2673 if pattern_info['regex'].match(token.text): 2674 if debug: 2675 logger.info(f"✓ Matched template pattern '{pattern_value}' -> {token.text}") 2676 return token.text 2677 2678 return None 2679 2680 try: 2681 if not sentence: 2682 logger.error("Sentence is required") 2683 return None 2684 if not template_schema or not isinstance(template_schema, dict): 2685 logger.error("Template schema is required") 2686 return None 2687 if not debug: 2688 debug = False 2689 2690 # Fix: Initialize result with all template schema keys 2691 result = {key: None for key in template_schema.keys()} 2692 result["sentences"] = [] # Initialize as empty list instead of string 2693 2694 except Exception as e: 2695 logger.error(f"Error generating JSON from sentence: {str(e)}") 2696 logger.traceback(e) 2697 return None 2698 2699 # Only add debug fields if debug mode is enabled 2700 if debug: 2701 result.update({ 2702 "tokens_analysis": [], 2703 "entities": [], 2704 "dependencies": [] 2705 }) 2706 2707 try: 2708 nlp = _ensure_spacy_model() 2709 if not nlp: 2710 logger.error("Failed to load spaCy model") 2711 return None 2712 if debug: 2713 # Analyze model capabilities 2714 analyze_model_components(nlp) 2715 doc = nlp(sentence) 2716 2717 # Fix: Ensure sentences list exists before appending 2718 if "sentences" not in result: 2719 result["sentences"] = [] 2720 2721 for sent in doc.sents: 2722 result["sentences"].append(sent.text.strip()) 2723 2724 # 2. Tokenize and analyze each token with spaCy components (only in debug mode) 2725 if debug: 2726 for token in doc: 2727 token_info = { 2728 "text": token.text, 2729 "lemma": token.lemma_, 2730 "pos": token.pos_, 2731 "tag": token.tag_, 2732 "dep": token.dep_, 2733 "head": token.head.text, 2734 "is_alpha": token.is_alpha, 2735 "is_digit": token.is_digit, 2736 "is_punct": token.is_punct, 2737 "shape": token.shape_, 2738 "is_stop": token.is_stop 2739 } 2740 result["tokens_analysis"].append(token_info) 2741 2742 # 3. Dynamic pattern matching based on template schema values 2743 for pattern_key, pattern_value in template_schema.items(): 2744 if not result[pattern_key]: # Only search if not already found 2745 matched_value = match_pattern_from_template(pattern_value, doc, debug) 2746 if matched_value: 2747 result[pattern_key] = matched_value 2748 2749 # 4. Use NER (Named Entity Recognition) component (only in debug mode) 2750 if debug: 2751 for ent in doc.ents: 2752 entity_info = { 2753 "text": ent.text, 2754 "label": ent.label_, 2755 "start": ent.start_char, 2756 "end": ent.end_char 2757 } 2758 result["entities"].append(entity_info) 2759 logger.info(f"✓ NER Entity: {ent.text} - {ent.label_}") 2760 2761 # 5. Use dependency parsing to find relationships (only in debug mode) 2762 if debug: 2763 for token in doc: 2764 dep_info = { 2765 "token": token.text, 2766 "head": token.head.text, 2767 "dependency": token.dep_, 2768 "children": [child.text for child in token.children] 2769 } 2770 result["dependencies"].append(dep_info) 2771 2772 # 6. Use lemmatizer to find action verbs and their objects 2773 for token in doc: 2774 if token.lemma_ in ["verify", "validate", "check", "test"]: 2775 if debug: 2776 logger.info(f"✓ Found action verb: {token.text} (lemma: {token.lemma_})") 2777 # Find what's being verified/validated 2778 for child in token.children: 2779 if child.dep_ in ["dobj", "pobj"]: 2780 if debug: 2781 logger.info(f"✓ Found verification target: {child.text}") 2782 2783 return result 2784 except Exception as e: 2785 logger.error(f"Error extracting information from document: {str(e)}") 2786 logger.traceback(e) 2787 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 result 1153 attachments { 1154 id 1155 filename 1156 storedInJira 1157 downloadLink 1158 } 1159 } 1160 1161 } 1162 } 1163 } 1164 """ 1165 variables = { 1166 "limit": 10, 1167 "jql": f"project = '{parse_project}' AND key = '{issue_key}'", 1168 "jiraFields": jira_fields 1169 } 1170 data = self._make_graphql_request(query, variables) 1171 if not data: 1172 logger.error(f"Failed to get issue ID for {issue_key}") 1173 return None 1174 for issue in data[function_name]['results']: 1175 if str(issue['jira']['key']).lower() == issue_key.lower(): 1176 return issue # This now includes all metadata 1177 return None 1178 except Exception as e: 1179 logger.error(f"Failed to get issue ID for {issue_key}") 1180 logger.traceback(e) 1181 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.
1183 def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]: 1184 """ 1185 Retrieves all tests associated with a given test plan from Xray. 1186 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1187 test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that 1188 ID to fetch the associated tests. 1189 Args: 1190 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1191 Returns: 1192 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1193 or None if the operation fails. For example: 1194 { 1195 "PROJECT-124": "10001", 1196 "PROJECT-125": "10002" 1197 } 1198 Returns None in the following cases: 1199 - Test plan ID cannot be found 1200 - GraphQL request fails 1201 - Any other error occurs during processing 1202 Example: 1203 >>> client = XrayGraphQL() 1204 >>> tests = client.get_tests_from_test_plan("PROJECT-123") 1205 >>> print(tests) 1206 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1207 Note: 1208 - The method is limited to retrieving 99999 tests per test plan 1209 - Test plan must exist in Xray and be accessible with current authentication 1210 """ 1211 try: 1212 test_plan_id = self.get_issue_id_from_jira_id(test_plan) 1213 if not test_plan_id: 1214 logger.error(f"Failed to get test plan ID for {test_plan}") 1215 return None 1216 query = """ 1217 query GetTestPlanTests($testPlanId: String!) { 1218 getTestPlan(issueId: $testPlanId) { 1219 tests(limit: 100) { 1220 results { 1221 issueId 1222 jira(fields: ["key"]) 1223 } 1224 } 1225 } 1226 } 1227 """ 1228 variables = {"testPlanId": test_plan_id} 1229 data = self._make_graphql_request(query, variables) 1230 if not data: 1231 logger.error(f"Failed to get tests for plan {test_plan_id}") 1232 return None 1233 tests = {} 1234 for test in data['getTestPlan']['tests']['results']: 1235 tests[test['jira']['key']] = test['issueId'] 1236 return tests 1237 except Exception as e: 1238 logger.error(f"Failed to get tests for plan {test_plan_id}") 1239 logger.traceback(e) 1240 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
1242 def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]: 1243 """ 1244 Retrieves all tests associated with a given test set from Xray. 1245 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1246 test set. It first converts the JIRA test set key to an internal Xray ID, then uses that 1247 ID to fetch the associated tests. 1248 Args: 1249 test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") 1250 Returns: 1251 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1252 or None if the operation fails. For example: 1253 { 1254 "PROJECT-124": "10001", 1255 "PROJECT-125": "10002" 1256 } 1257 Returns None in the following cases: 1258 - Test set ID cannot be found 1259 - GraphQL request fails 1260 - Any other error occurs during processing 1261 Example: 1262 >>> client = XrayGraphQL() 1263 >>> tests = client.get_tests_from_test_set("PROJECT-123") 1264 >>> print(tests) 1265 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1266 Note: 1267 - The method is limited to retrieving 99999 tests per test set 1268 - Test set must exist in Xray and be accessible with current authentication 1269 """ 1270 try: 1271 test_set_id = self.get_issue_id_from_jira_id(test_set) 1272 if not test_set_id: 1273 logger.error(f"Failed to get test set ID for {test_set}") 1274 return None 1275 query = """ 1276 query GetTestSetTests($testSetId: String!) { 1277 getTestSet(issueId: $testSetId) { 1278 tests(limit: 100) { 1279 results { 1280 issueId 1281 jira(fields: ["key"]) 1282 } 1283 } 1284 } 1285 } 1286 """ 1287 variables = {"testSetId": test_set_id} 1288 data = self._make_graphql_request(query, variables) 1289 if not data: 1290 logger.error(f"Failed to get tests for set {test_set_id}") 1291 return None 1292 tests = {} 1293 for test in data['getTestSet']['tests']['results']: 1294 tests[test['jira']['key']] = test['issueId'] 1295 return tests 1296 except Exception as e: 1297 logger.error(f"Failed to get tests for set {test_set_id}") 1298 logger.traceback(e) 1299 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
1301 def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]: 1302 """ 1303 Retrieves all tests associated with a given test execution from Xray. 1304 This method queries the Xray GraphQL API to fetch all tests that are part of the specified 1305 test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that 1306 ID to fetch the associated tests. 1307 Args: 1308 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1309 Returns: 1310 Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, 1311 or None if the operation fails. For example: 1312 { 1313 "PROJECT-124": "10001", 1314 "PROJECT-125": "10002" 1315 } 1316 Returns None in the following cases: 1317 - Test execution ID cannot be found 1318 - GraphQL request fails 1319 - Any other error occurs during processing 1320 Example: 1321 >>> client = XrayGraphQL() 1322 >>> tests = client.get_tests_from_test_execution("PROJECT-123") 1323 >>> print(tests) 1324 {"PROJECT-124": "10001", "PROJECT-125": "10002"} 1325 Note: 1326 - The method is limited to retrieving 99999 tests per test execution 1327 - Test execution must exist in Xray and be accessible with current authentication 1328 """ 1329 try: 1330 test_execution_id = self.get_issue_id_from_jira_id(test_execution) 1331 if not test_execution_id: 1332 logger.error(f"Failed to get test execution ID for {test_execution}") 1333 return None 1334 query = """ 1335 query GetTestExecutionTests($testExecutionId: String!) { 1336 getTestExecution(issueId: $testExecutionId) { 1337 tests(limit: 100) { 1338 results { 1339 issueId 1340 jira(fields: ["key"]) 1341 } 1342 } 1343 } 1344 } 1345 """ 1346 variables = {"testExecutionId": test_execution_id} 1347 data = self._make_graphql_request(query, variables) 1348 if not data: 1349 logger.error(f"Failed to get tests for execution {test_execution_id}") 1350 return None 1351 tests = {} 1352 for test in data['getTestExecution']['tests']['results']: 1353 tests[test['jira']['key']] = test['issueId'] 1354 return tests 1355 except Exception as e: 1356 logger.error(f"Failed to get tests for execution {test_execution_id}") 1357 logger.traceback(e) 1358 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
1360 def get_test_plan_data(self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]: 1361 """ 1362 Retrieve and parse tabular data from a test plan's description field in Xray. 1363 This method fetches a test plan's description from Xray and parses any tables found within it. 1364 The tables in the description are expected to be in a specific format that can be parsed by 1365 the _parse_table method. The parsed data is returned as a dictionary containing numeric values 1366 and lists extracted from the table. 1367 Args: 1368 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1369 Returns: 1370 Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data 1371 where keys are derived from the first column of the table and values are lists of numeric 1372 values. Returns None if: 1373 - The test plan ID cannot be found 1374 - The GraphQL request fails 1375 - The description cannot be parsed 1376 - Any other error occurs during processing 1377 Example: 1378 >>> client = XrayGraphQL() 1379 >>> data = client.get_test_plan_data("TEST-123") 1380 >>> print(data) 1381 { 1382 'temperature': [20, 25, 30], 1383 'pressure': [1.0, 1.5, 2.0], 1384 'measurements': [[1, 2, 3], [4, 5, 6]] 1385 } 1386 Note: 1387 - The test plan must exist in Xray and be accessible with current authentication 1388 - The description must contain properly formatted tables for parsing 1389 - Table values are converted to numeric types (int or float) where possible 1390 - Lists in table cells should be formatted as [value1, value2, ...] 1391 - Failed operations are logged as errors with relevant details 1392 """ 1393 try: 1394 test_plan_id = self.get_issue_id_from_jira_id(test_plan) 1395 if not test_plan_id: 1396 logger.error(f"Failed to get test plan ID for {test_plan}") 1397 return None 1398 query = """ 1399 query GetTestPlanTests($testPlanId: String!) { 1400 getTestPlan(issueId: $testPlanId) { 1401 issueId 1402 jira(fields: ["key","description"]) 1403 } 1404 } 1405 """ 1406 variables = {"testPlanId": test_plan_id} 1407 data = self._make_graphql_request(query, variables) 1408 if not data: 1409 logger.error(f"Failed to get tests for plan {test_plan_id}") 1410 return None 1411 description = data['getTestPlan']['jira']['description'] 1412 test_plan_data = self._parse_table(description) 1413 return test_plan_data 1414 except Exception as e: 1415 logger.error(f"Failed to get tests for plan {test_plan_id}") 1416 logger.traceback(e) 1417 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
1419 def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]: 1420 """ 1421 Retrieves all test sets that contain a specific test case from Xray. 1422 This method queries the Xray GraphQL API to find all test sets that include the specified 1423 test case. It first converts the JIRA test case key to an internal Xray ID, then uses that 1424 ID to fetch all associated test sets. 1425 Args: 1426 test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1427 Returns: 1428 Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, 1429 or None if the operation fails. For example: 1430 { 1431 "PROJECT-124": "Test Set for Feature A", 1432 "PROJECT-125": "Regression Test Set" 1433 } 1434 Returns None in the following cases: 1435 - Test case ID cannot be found 1436 - GraphQL request fails 1437 - Any other error occurs during processing 1438 Example: 1439 >>> client = XrayGraphQL() 1440 >>> test_sets = client.filter_test_set_by_test_case("PROJECT-123") 1441 >>> print(test_sets) 1442 {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} 1443 Note: 1444 - The method is limited to retrieving 99999 test sets per test case 1445 - Test case must exist in Xray and be accessible with current authentication 1446 """ 1447 try: 1448 test_id = self.get_issue_id_from_jira_id(test_key) 1449 if not test_id: 1450 logger.error(f"Failed to get test ID for Test Case ({test_key})") 1451 return None 1452 query = """ 1453 query GetTestDetails($testId: String!) { 1454 getTest(issueId: $testId) { 1455 testSets(limit: 100) { 1456 results { 1457 issueId 1458 jira(fields: ["key","summary"]) 1459 } 1460 } 1461 } 1462 } 1463 """ 1464 variables = { 1465 "testId": test_id 1466 } 1467 data = self._make_graphql_request(query, variables) 1468 if not data: 1469 logger.error(f"Failed to get tests for plan {test_id}") 1470 return None 1471 retDict = {} 1472 for test in data['getTest']['testSets']['results']: 1473 retDict[test['jira']['key']] = test['jira']['summary'] 1474 return retDict 1475 except Exception as e: 1476 logger.error(f"Error in getting test set by test id: {e}") 1477 logger.traceback(e) 1478 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
1545 def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]: 1546 """ 1547 Retrieve the status of a test run for a specific test case within a test execution. 1548 This method queries the Xray GraphQL API to get the current status of a test run, 1549 which represents the execution status of a specific test case within a test execution. 1550 It first converts both the test case and test execution JIRA keys to their internal 1551 Xray IDs, then uses these to fetch the test run status. 1552 Args: 1553 test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") 1554 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") 1555 Returns: 1556 Tuple[Optional[str], Optional[str]]: A tuple containing: 1557 - test_run_id: The unique identifier of the test run (or None if not found) 1558 - test_run_status: The current status of the test run (or None if not found) 1559 Returns (None, None) if any error occurs during the process. 1560 Example: 1561 >>> client = XrayGraphQL() 1562 >>> run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") 1563 >>> print(f"Test Run ID: {run_id}, Status: {status}") 1564 Test Run ID: 10001, Status: PASS 1565 Note: 1566 - Both the test case and test execution must exist in Xray and be accessible 1567 - The test case must be associated with the test execution 1568 - The method performs two ID lookups before querying the test run status 1569 - Failed operations are logged as errors with relevant details 1570 """ 1571 try: 1572 test_case_id = self.get_issue_id_from_jira_id(test_case) 1573 if not test_case_id: 1574 logger.error(f"Failed to get test ID for Test Case ({test_case})") 1575 return None 1576 test_exec_id = self.get_issue_id_from_jira_id(test_execution) 1577 if not test_exec_id: 1578 logger.error(f"Failed to get test execution ID for Test Execution ({test_execution})") 1579 return None 1580 query = """ 1581 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1582 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1583 id 1584 status { 1585 name 1586 } 1587 } 1588 } 1589 """ 1590 variables = { 1591 "testId": test_case_id, 1592 "testExecutionId": test_exec_id, 1593 } 1594 # Add debug loggerging 1595 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_exec_id}") 1596 data = self._make_graphql_request(query, variables) 1597 if not data: 1598 logger.error(f"Failed to get test run status for test {test_case_id}") 1599 return None 1600 # jprint(data) 1601 test_run_id = data['getTestRun']['id'] 1602 test_run_status = data['getTestRun']['status']['name'] 1603 return (test_run_id, test_run_status) 1604 except Exception as e: 1605 logger.error(f"Error getting test run status: {str(e)}") 1606 logger.traceback(e) 1607 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
1609 def get_test_run_by_id(self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]: 1610 """ 1611 Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. 1612 Args: 1613 test_case_id (str): The ID of the test case to query 1614 test_execution_id (str): The ID of the test execution containing the test run 1615 Returns: 1616 tuple[Optional[str], Optional[str]]: A tuple containing: 1617 - test_run_id: The ID of the test run if found, None if not found or on error 1618 - test_run_status: The status name of the test run if found, None if not found or on error 1619 Note: 1620 The function makes a GraphQL request to fetch the test run information. If the request fails 1621 or encounters any errors, it will log the error and return (None, None). 1622 """ 1623 try: 1624 query = """ 1625 query GetTestRunStatus($testId: String!, $testExecutionId: String!) { 1626 getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) { 1627 id 1628 status { 1629 name 1630 } 1631 } 1632 } 1633 """ 1634 variables = { 1635 "testId": test_case_id, 1636 "testExecutionId": test_execution_id, 1637 } 1638 # Add debug loggerging 1639 logger.debug(f"Getting test run status for test {test_case_id} in execution {test_execution_id}") 1640 data = self._make_graphql_request(query, variables) 1641 if not data: 1642 logger.error(f"Failed to get test run status for test {test_case_id}") 1643 return None 1644 test_run_id = data['getTestRun']['id'] 1645 test_run_status = data['getTestRun']['status']['name'] 1646 return (test_run_id, test_run_status) 1647 except Exception as e: 1648 logger.error(f"Error getting test run status: {str(e)}") 1649 logger.traceback(e) 1650 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).
1652 def get_test_execution(self, test_execution: str) -> Optional[Dict]: 1653 """ 1654 Retrieve detailed information about a test execution from Xray. 1655 This method queries the Xray GraphQL API to fetch information about a specific test execution, 1656 including its ID and associated tests. It first converts the JIRA test execution key to an 1657 internal Xray ID, then uses that ID to fetch the execution details. 1658 Args: 1659 test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") 1660 Returns: 1661 Optional[Dict]: A dictionary containing test execution details if successful, None if failed. 1662 The dictionary has the following structure: 1663 { 1664 'id': str, # The internal Xray ID of the test execution 1665 'tests': { # Dictionary mapping test keys to their IDs 1666 'TEST-124': '10001', 1667 'TEST-125': '10002', 1668 ... 1669 } 1670 } 1671 Returns None in the following cases: 1672 - Test execution ID cannot be found 1673 - GraphQL request fails 1674 - No test execution found with the given ID 1675 - No tests found in the test execution 1676 - Any other error occurs during processing 1677 Example: 1678 >>> client = XrayGraphQL() 1679 >>> execution = client.get_test_execution("TEST-123") 1680 >>> print(execution) 1681 { 1682 'id': '10000', 1683 'tests': { 1684 'TEST-124': '10001', 1685 'TEST-125': '10002' 1686 } 1687 } 1688 Note: 1689 - The method is limited to retrieving 99999 tests per test execution 1690 - Test execution must exist in Xray and be accessible with current authentication 1691 - Failed operations are logged with appropriate error or warning messages 1692 """ 1693 try: 1694 test_execution_id = self.get_issue_id_from_jira_id(test_execution) 1695 if not test_execution_id: 1696 logger.error(f"Failed to get test execution ID for {test_execution}") 1697 return None 1698 query = """ 1699 query GetTestExecution($testExecutionId: String!) { 1700 getTestExecution(issueId: $testExecutionId) { 1701 issueId 1702 projectId 1703 jira(fields: ["key", "summary", "description", "status"]) 1704 tests(limit: 100) { 1705 total 1706 start 1707 limit 1708 results { 1709 issueId 1710 jira(fields: ["key"]) 1711 } 1712 } 1713 } 1714 } 1715 """ 1716 variables = { 1717 "testExecutionId": test_execution_id 1718 } 1719 # Add debug loggerging 1720 logger.debug(f"Getting test execution details for {test_execution_id}") 1721 data = self._make_graphql_request(query, variables) 1722 # jprint(data) 1723 if not data: 1724 logger.error(f"Failed to get test execution details for {test_execution_id}") 1725 return None 1726 test_execution = data.get('getTestExecution',{}) 1727 if not test_execution: 1728 logger.warning(f"No test execution found with ID {test_execution_id}") 1729 return None 1730 tests = test_execution.get('tests',{}) 1731 if not tests: 1732 logger.warning(f"No tests found for test execution {test_execution_id}") 1733 return None 1734 tests_details = dict() 1735 for test in tests['results']: 1736 tests_details[test['jira']['key']] = test['issueId'] 1737 formatted_response = { 1738 'id': test_execution['issueId'], 1739 'tests': tests_details 1740 } 1741 return formatted_response 1742 except Exception as e: 1743 logger.error(f"Error getting test execution details: {str(e)}") 1744 logger.traceback(e) 1745 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
1747 def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]: 1748 """ 1749 Add a test execution to an existing test plan in Xray. 1750 This method associates a test execution with a test plan using the Xray GraphQL API. 1751 It first converts both the test plan and test execution JIRA keys to their internal 1752 Xray IDs, then creates the association between them. 1753 Args: 1754 test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") 1755 test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") 1756 Returns: 1757 Optional[Dict]: A dictionary containing the response data if successful, None if failed. 1758 The dictionary has the following structure: 1759 { 1760 'addTestExecutionsToTestPlan': { 1761 'addedTestExecutions': [str], # List of added test execution IDs 1762 'warning': str # Any warnings from the operation 1763 } 1764 } 1765 Returns None in the following cases: 1766 - Test plan ID cannot be found 1767 - Test execution ID cannot be found 1768 - GraphQL request fails 1769 - Any other error occurs during processing 1770 Example: 1771 >>> client = XrayGraphQL() 1772 >>> result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") 1773 >>> print(result) 1774 { 1775 'addTestExecutionsToTestPlan': { 1776 'addedTestExecutions': ['10001'], 1777 'warning': None 1778 } 1779 } 1780 Note: 1781 - Both the test plan and test execution must exist in Xray and be accessible 1782 - The method performs two ID lookups before creating the association 1783 - Failed operations are logged as errors with relevant details 1784 """ 1785 try: 1786 test_plan_id = self.get_issue_id_from_jira_id(test_plan) 1787 if not test_plan_id: 1788 logger.error(f"Test plan ID is required") 1789 return None 1790 test_exec_id = self.get_issue_id_from_jira_id(test_execution) 1791 if not test_exec_id: 1792 logger.error(f"Test execution ID is required") 1793 return None 1794 query = """ 1795 mutation AddTestExecutionToTestPlan($testPlanId: String!, $testExecutionIds: [String!]!) { 1796 addTestExecutionsToTestPlan(issueId: $testPlanId, testExecIssueIds: $testExecutionIds) { 1797 addedTestExecutions 1798 warning 1799 } 1800 } 1801 """ 1802 variables = { 1803 "testPlanId": test_plan_id, 1804 "testExecutionIds": [test_exec_id] 1805 } 1806 data = self._make_graphql_request(query, variables) 1807 return data 1808 except Exception as e: 1809 logger.error(f"Error adding test execution to test plan: {str(e)}") 1810 logger.traceback(e) 1811 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
1813 def create_test_execution(self, 1814 test_issue_keys: List[str], 1815 project_key: Optional[str] = None, 1816 summary: Optional[str] = None, 1817 description: Optional[str] = None) -> Optional[Dict]: 1818 """ 1819 Create a new test execution in Xray with specified test cases. 1820 This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. 1821 It handles validation of test issue keys, automatically derives project information if not provided, 1822 and creates appropriate default values for summary and description if not specified. 1823 Args: 1824 test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution 1825 (e.g., ["TEST-123", "TEST-124"]) 1826 project_key (Optional[str]): The JIRA project key where the test execution should be created. 1827 If not provided, it will be derived from the first test issue key. 1828 summary (Optional[str]): The summary/title for the test execution ticket. 1829 If not provided, a default summary will be generated using the test issue keys. 1830 description (Optional[str]): The description for the test execution ticket. 1831 If not provided, a default description will be generated using the test issue keys. 1832 Returns: 1833 Optional[Dict]: A dictionary containing the created test execution details if successful, 1834 None if the creation fails. The dictionary has the following structure: 1835 { 1836 'issueId': str, # The internal Xray ID of the created test execution 1837 'jira': { 1838 'key': str # The JIRA issue key of the created test execution 1839 } 1840 } 1841 Example: 1842 >>> client = XrayGraphQL() 1843 >>> test_execution = client.create_test_execution( 1844 ... test_issue_keys=["TEST-123", "TEST-124"], 1845 ... project_key="TEST", 1846 ... summary="Sprint 1 Regression Tests" 1847 ... ) 1848 >>> print(test_execution) 1849 {'issueId': '10001', 'jira': {'key': 'TEST-125'}} 1850 Note: 1851 - Invalid test issue keys are logged as warnings but don't prevent execution creation 1852 - At least one valid test issue key is required 1853 - The method validates each test issue key before creating the execution 1854 - Project key is automatically derived from the first test issue key if not provided 1855 """ 1856 try: 1857 invalid_keys = [] 1858 test_issue_ids = [] 1859 for key in test_issue_keys: 1860 test_issue_id = self.get_issue_id_from_jira_id(key) 1861 if test_issue_id: 1862 test_issue_ids.append(test_issue_id) 1863 else: 1864 invalid_keys.append(key) 1865 if len(test_issue_ids) == 0: 1866 logger.error(f"No valid test issue keys provided: {invalid_keys}") 1867 return None 1868 if len(invalid_keys) > 0: 1869 logger.warning(f"Invalid test issue keys: {invalid_keys}") 1870 if not project_key: 1871 project_key = test_issue_keys[0].split("-")[0] 1872 if not summary: 1873 summary = f"Test Execution for Test Plan {test_issue_keys}" 1874 if not description: 1875 description = f"Test Execution for Test Plan {test_issue_keys}" 1876 mutation = """ 1877 mutation CreateTestExecutionForTestPlan( 1878 $testIssueId_list: [String!]!, 1879 $projectKey: String!, 1880 $summary: String!, 1881 $description: String 1882 ) { 1883 createTestExecution( 1884 testIssueIds: $testIssueId_list, 1885 jira: { 1886 fields: { 1887 project: { key: $projectKey }, 1888 summary: $summary, 1889 description: $description, 1890 issuetype: { name: "Test Execution" } 1891 } 1892 } 1893 ) { 1894 testExecution { 1895 issueId 1896 jira(fields: ["key"]) 1897 } 1898 warnings 1899 } 1900 } 1901 """ 1902 variables = { 1903 "testIssueId_list": test_issue_ids, 1904 "projectKey": project_key, 1905 "summary": summary, 1906 "description": description 1907 } 1908 data = self._make_graphql_request(mutation, variables) 1909 if not data: 1910 return None 1911 execution_details = data['createTestExecution']['testExecution'] 1912 # logger.info(f"Created test execution {execution_details['jira']['key']}") 1913 return execution_details 1914 except Exception as e: 1915 logger.error("Failed to create test execution : {e}") 1916 logger.traceback(e) 1917 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
1919 def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]: 1920 """Creates a test execution from a given test plan and associates all tests from the plan with the execution. 1921 This method performs several operations in sequence: 1922 1. Retrieves all tests from the specified test plan 1923 2. Creates a new test execution with those tests 1924 3. Associates the new test execution with the original test plan 1925 4. Creates test runs for each test in the execution 1926 Parameters 1927 ---------- 1928 test_plan : str 1929 The JIRA issue key of the test plan (e.g., "PROJECT-123") 1930 Returns 1931 ------- 1932 Optional[Dict[str, Dict[str, str]]] 1933 A dictionary mapping test case keys to their execution details, or None if the operation fails. 1934 The dictionary structure is:: 1935 { 1936 "TEST-123": { # Test case JIRA key 1937 "test_run_id": "12345", # Unique ID for this test run 1938 "test_execution_key": "TEST-456", # JIRA key of the created test execution 1939 "test_plan_key": "TEST-789" # Original test plan JIRA key 1940 }, 1941 "TEST-124": { 1942 ... 1943 } 1944 } 1945 Returns None in the following cases: 1946 * Test plan parameter is empty or invalid 1947 * No tests found in the test plan 1948 * Test execution creation fails 1949 * API request fails 1950 Examples 1951 -------- 1952 >>> client = XrayGraphQL() 1953 >>> result = client.create_test_execution_from_test_plan("TEST-123") 1954 >>> print(result) 1955 { 1956 "TEST-124": { 1957 "test_run_id": "5f7c3", 1958 "test_execution_key": "TEST-456", 1959 "test_plan_key": "TEST-123" 1960 }, 1961 "TEST-125": { 1962 "test_run_id": "5f7c4", 1963 "test_execution_key": "TEST-456", 1964 "test_plan_key": "TEST-123" 1965 } 1966 } 1967 Notes 1968 ----- 1969 - The test plan must exist and be accessible in Xray 1970 - All tests in the test plan must be valid and accessible 1971 - The method automatically generates a summary and description for the test execution 1972 - The created test execution is automatically linked back to the original test plan 1973 """ 1974 try: 1975 if not test_plan: 1976 logger.error("Test plan is required [ jira key]") 1977 return None 1978 project_key = test_plan.split("-")[0] 1979 summary = f"Test Execution for Test Plan {test_plan}" 1980 retDict = dict() 1981 #Get tests from test plan 1982 tests = self.get_tests_from_test_plan(test_plan) 1983 retDict["tests"] = tests 1984 testIssueId_list = list(tests.values()) 1985 # logger.info(f"Tests: {tests}") 1986 if not testIssueId_list: 1987 logger.error(f"No tests found for {test_plan}") 1988 return None 1989 description = f"Test Execution for {len(tests)} Test cases" 1990 # GraphQL mutation to create test execution 1991 query = """ 1992 mutation CreateTestExecutionForTestPlan( 1993 $testIssueId_list: [String!]!, 1994 $projectKey: String!, 1995 $summary: String!, 1996 $description: String 1997 ) { 1998 createTestExecution( 1999 testIssueIds: $testIssueId_list, 2000 jira: { 2001 fields: { 2002 project: { key: $projectKey }, 2003 summary: $summary, 2004 description: $description, 2005 issuetype: { name: "Test Execution" } 2006 } 2007 } 2008 ) { 2009 testExecution { 2010 issueId 2011 jira(fields: ["key"]) 2012 testRuns(limit: 100) { 2013 results { 2014 id 2015 test { 2016 issueId 2017 jira(fields: ["key"]) 2018 } 2019 } 2020 } 2021 } 2022 warnings 2023 } 2024 } 2025 """ 2026 variables = { 2027 "testIssueId_list": testIssueId_list, 2028 "projectKey": project_key, 2029 "summary": summary, 2030 "description": description or f"Test execution for total of {len(testIssueId_list)} test cases" 2031 } 2032 data = self._make_graphql_request(query, variables) 2033 if not data: 2034 return None 2035 test_exec_key = data['createTestExecution']['testExecution']['jira']['key'] 2036 test_exec_id = data['createTestExecution']['testExecution']['issueId'] 2037 #Add Test execution to test plan 2038 test_exec_dict= self.add_test_execution_to_test_plan(test_plan, test_exec_key) 2039 #Get test runs for test execution 2040 test_runs = data['createTestExecution']['testExecution']['testRuns']['results'] 2041 test_run_dict = dict() 2042 for test_run in test_runs: 2043 test_run_dict[test_run['test']['jira']['key']] = dict() 2044 test_run_dict[test_run['test']['jira']['key']]['test_run_id'] = test_run['id'] 2045 # test_run_dict[test_run['test']['jira']['key']]['test_issue_id'] = test_run['test']['issueId'] 2046 test_run_dict[test_run['test']['jira']['key']]['test_execution_key'] = test_exec_key 2047 # test_run_dict[test_run['test']['jira']['key']]['test_execution_id'] = test_exec_id 2048 test_run_dict[test_run['test']['jira']['key']]['test_plan_key'] = test_plan 2049 return test_run_dict 2050 except requests.exceptions.RequestException as e: 2051 logger.error(f"Error creating test execution: {e}") 2052 logger.traceback(e) 2053 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
2055 def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool: 2056 """ 2057 Update the status of a specific test run in Xray using the GraphQL API. 2058 This method allows updating the execution status of a test run identified by its ID. 2059 The status can be changed to reflect the current state of the test execution 2060 (e.g., "PASS", "FAIL", "TODO", etc.). 2061 Args: 2062 test_run_id (str): The unique identifier of the test run to update. 2063 This is the internal Xray ID for the test run, not the Jira issue key. 2064 test_run_status (str): The new status to set for the test run. 2065 Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. 2066 Returns: 2067 bool: True if the status update was successful, False otherwise. 2068 Returns None if an error occurs during the API request. 2069 Example: 2070 >>> client = XrayGraphQL() 2071 >>> test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") 2072 >>> success = client.update_test_run_status(test_run_id, "PASS") 2073 >>> print(success) 2074 True 2075 Note: 2076 - The test run ID must be valid and accessible with current authentication 2077 - The status value should be one of the valid status values configured in your Xray instance 2078 - Failed updates are logged as errors with details about the failure 2079 Raises: 2080 Exception: If there is an error making the GraphQL request or processing the response. 2081 The exception is caught and logged, and the method returns None. 2082 """ 2083 try: 2084 query = """ 2085 mutation UpdateTestRunStatus($testRunId: String!, $status: String!) { 2086 updateTestRunStatus( 2087 id: $testRunId, 2088 status: $status 2089 ) 2090 } 2091 """ 2092 variables = { 2093 "testRunId": test_run_id, 2094 "status": test_run_status 2095 } 2096 data = self._make_graphql_request(query, variables) 2097 if not data: 2098 logger.error(f"Failed to get test run status for test {data}") 2099 return None 2100 # logger.info(f"Test run status updated: {data}") 2101 return data['updateTestRunStatus'] 2102 except Exception as e: 2103 logger.error(f"Error updating test run status: {str(e)}") 2104 logger.traceback(e) 2105 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.
2107 def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2108 """ 2109 Update the comment of a specific test run in Xray using the GraphQL API. 2110 This method allows adding or updating the comment associated with a test run 2111 identified by its ID. The comment can provide additional context, test results, 2112 or any other relevant information about the test execution. 2113 Args: 2114 test_run_id (str): The unique identifier of the test run to update. 2115 This is the internal Xray ID for the test run, not the Jira issue key. 2116 test_run_comment (str): The new comment text to set for the test run. 2117 This will replace any existing comment on the test run. 2118 Returns: 2119 bool: True if the comment update was successful, False otherwise. 2120 Returns None if an error occurs during the API request. 2121 Example: 2122 >>> client = XrayGraphQL() 2123 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2124 >>> success = client.update_test_run_comment( 2125 ... test_run_id, 2126 ... "Test passed with performance within expected range" 2127 ... ) 2128 >>> print(success) 2129 True 2130 Note: 2131 - The test run ID must be valid and accessible with current authentication 2132 - The comment can include any text content, including newlines and special characters 2133 - Failed updates are logged as errors with details about the failure 2134 - This method will overwrite any existing comment on the test run 2135 Raises: 2136 Exception: If there is an error making the GraphQL request or processing the response. 2137 The exception is caught and logged, and the method returns None. 2138 """ 2139 try: 2140 query = """ 2141 mutation UpdateTestRunStatus($testRunId: String!, $comment: String!) { 2142 updateTestRunComment( 2143 id: $testRunId, 2144 comment: $comment 2145 ) 2146 } 2147 """ 2148 variables = { 2149 "testRunId": test_run_id, 2150 "comment": test_run_comment 2151 } 2152 data = self._make_graphql_request(query, variables) 2153 if not data: 2154 logger.error(f"Failed to get test run comment for test {data}") 2155 return None 2156 # jprint(data) 2157 return data['updateTestRunComment'] 2158 except Exception as e: 2159 logger.error(f"Error updating test run comment: {str(e)}") 2160 logger.traceback(e) 2161 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.
2163 def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool: 2164 """Add evidence (attachments) to a test run in Xray. 2165 This method allows attaching files as evidence to a specific test run. The file is 2166 read, converted to base64, and uploaded to Xray with appropriate MIME type detection. 2167 Parameters 2168 ---------- 2169 test_run_id : str 2170 The unique identifier of the test run to add evidence to 2171 evidence_path : str 2172 The local file system path to the evidence file to be attached 2173 Returns 2174 ------- 2175 bool 2176 True if the evidence was successfully added, None if the operation failed. 2177 Returns None in the following cases: 2178 - Test run ID is not provided 2179 - Evidence path is not provided 2180 - Evidence file does not exist 2181 - GraphQL request fails 2182 - Any other error occurs during processing 2183 Examples 2184 -------- 2185 >>> client = XrayGraphQL() 2186 >>> success = client.add_evidence_to_test_run( 2187 ... test_run_id="10001", 2188 ... evidence_path="/path/to/screenshot.png" 2189 ... ) 2190 >>> print(success) 2191 True 2192 Notes 2193 ----- 2194 - The evidence file must exist and be accessible 2195 - The file is automatically converted to base64 for upload 2196 - MIME type is automatically detected, defaults to "text/plain" if detection fails 2197 - The method supports various file types (images, documents, logs, etc.) 2198 - Failed operations are logged with appropriate error messages 2199 """ 2200 try: 2201 if not test_run_id: 2202 logger.error("Test run ID is required") 2203 return None 2204 if not evidence_path: 2205 logger.error("Evidence path is required") 2206 return None 2207 if not os.path.exists(evidence_path): 2208 logger.error(f"Evidence file not found: {evidence_path}") 2209 return None 2210 #if file exists then read the file in base64 2211 evidence_base64 = None 2212 mime_type = None 2213 filename = os.path.basename(evidence_path) 2214 with open(evidence_path, "rb") as file: 2215 evidence_data = file.read() 2216 evidence_base64 = base64.b64encode(evidence_data).decode('utf-8') 2217 mime_type = mimetypes.guess_type(evidence_path)[0] 2218 logger.info(f"For loop -- Mime type: {mime_type}") 2219 if not mime_type: 2220 mime_type = "text/plain" 2221 query = """ 2222 mutation AddEvidenceToTestRun($testRunId: String!, $filename: String!, $mimeType: String!, $evidenceBase64: String!) { 2223 addEvidenceToTestRun( 2224 id: $testRunId, 2225 evidence: [ 2226 { 2227 filename : $filename, 2228 mimeType : $mimeType, 2229 data : $evidenceBase64 2230 } 2231 ] 2232 ) { 2233 addedEvidence 2234 warnings 2235 } 2236 } 2237 """ 2238 variables = { 2239 "testRunId": test_run_id, 2240 "filename": filename, 2241 "mimeType": mime_type, 2242 "evidenceBase64": evidence_base64 2243 } 2244 data = self._make_graphql_request(query, variables) 2245 if not data: 2246 logger.error(f"Failed to add evidence to test run: {data}") 2247 return None 2248 return data['addEvidenceToTestRun'] 2249 except Exception as e: 2250 logger.error(f"Error adding evidence to test run: {str(e)}") 2251 logger.traceback(e) 2252 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
2254 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]: 2255 """Create a defect from a test run and link it to the test run in Xray. 2256 This method performs two main operations: 2257 1. Creates a new defect in JIRA with the specified summary and description 2258 2. Links the created defect to the specified test run in Xray 2259 Parameters 2260 ---------- 2261 test_run_id : str 2262 The ID of the test run to create defect from 2263 project_key : str 2264 The JIRA project key where the defect should be created. 2265 If not provided, defaults to "EAGVAL" 2266 parent_issue_key : str 2267 The JIRA key of the parent issue to link the defect to 2268 defect_summary : str 2269 Summary/title of the defect. 2270 If not provided, defaults to "Please provide a summary for the defect" 2271 defect_description : str 2272 Description of the defect. 2273 If not provided, defaults to "Please provide a description for the defect" 2274 Returns 2275 ------- 2276 Optional[Dict] 2277 Response data from the GraphQL API if successful, None if failed. 2278 The response includes: 2279 - addedDefects: List of added defects 2280 - warnings: Any warnings from the operation 2281 Examples 2282 -------- 2283 >>> client = XrayGraphQL() 2284 >>> result = client.create_defect_from_test_run( 2285 ... test_run_id="10001", 2286 ... project_key="PROJ", 2287 ... parent_issue_key="PROJ-456", 2288 ... defect_summary="Test failure in login flow", 2289 ... defect_description="The login button is not responding to clicks" 2290 ... ) 2291 >>> print(result) 2292 { 2293 'addedDefects': ['PROJ-123'], 2294 'warnings': [] 2295 } 2296 Notes 2297 ----- 2298 - The project_key will be split on '-' and only the first part will be used 2299 - The defect will be created with issue type 'Bug' 2300 - The method handles missing parameters with default values 2301 - The parent issue must exist and be accessible to create the defect 2302 """ 2303 try: 2304 if not project_key: 2305 project_key = "EAGVAL" 2306 if not defect_summary: 2307 defect_summary = "Please provide a summary for the defect" 2308 if not defect_description: 2309 defect_description = "Please provide a description for the defect" 2310 project_key = project_key.split("-")[0] 2311 # Fix: Correct parameter order for create_issue 2312 defect_key, defect_id = self.create_issue( 2313 project_key=project_key, 2314 parent_issue_key=parent_issue_key, 2315 summary=defect_summary, 2316 description=defect_description, 2317 issue_type='Bug' 2318 ) 2319 if not defect_key: 2320 logger.error("Failed to create defect issue") 2321 return None 2322 # Then add the defect to the test run 2323 add_defect_mutation = """ 2324 mutation AddDefectsToTestRun( 2325 $testRunId: String!, 2326 $defectKey: String! 2327 ) { 2328 addDefectsToTestRun( id: $testRunId, issues: [$defectKey]) { 2329 addedDefects 2330 warnings 2331 } 2332 } 2333 """ 2334 variables = { 2335 "testRunId": test_run_id, 2336 "defectKey": defect_key 2337 } 2338 data = None 2339 retry_count = 0 2340 while retry_count < 3: 2341 data = self._make_graphql_request(add_defect_mutation, variables) 2342 if not data: 2343 logger.error(f"Failed to add defect [{defect_key}] to test run - {test_run_id}.. retrying... {retry_count}") 2344 retry_count += 1 2345 time.sleep(1) 2346 else: 2347 break 2348 return data 2349 except Exception as e: 2350 logger.error(f"Error creating defect from test run: {str(e)}") 2351 logger.traceback(e) 2352 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
2354 def get_test_run_comment(self, test_run_id: str) -> Optional[str]: 2355 """ 2356 Retrieve the comment of a specific test run from Xray using the GraphQL API. 2357 This method allows retrieving the comment associated with a test run 2358 identified by its ID. The comment can provide additional context, test results, 2359 or any other relevant information about the test execution. 2360 Args: 2361 test_run_id (str): The unique identifier of the test run to retrieve comment from. 2362 This is the internal Xray ID for the test run, not the Jira issue key. 2363 Returns: 2364 Optional[str]: The comment text of the test run if successful, None if: 2365 - The test run ID is not found 2366 - The GraphQL request fails 2367 - No comment exists for the test run 2368 - Any other error occurs during the API request 2369 Example: 2370 >>> client = XrayGraphQL() 2371 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2372 >>> comment = client.get_test_run_comment(test_run_id) 2373 >>> print(comment) 2374 "Test passed with performance within expected range" 2375 Note: 2376 - The test run ID must be valid and accessible with current authentication 2377 - If no comment exists for the test run, the method will return None 2378 - Failed requests are logged as errors with details about the failure 2379 - The method returns the raw comment text as stored in Xray 2380 Raises: 2381 Exception: If there is an error making the GraphQL request or processing the response. 2382 The exception is caught and logged, and the method returns None. 2383 """ 2384 try: 2385 # Try the direct ID approach first 2386 query = """ 2387 query GetTestRunComment($testRunId: String!) { 2388 getTestRunById(id: $testRunId) { 2389 id 2390 comment 2391 status { 2392 name 2393 } 2394 } 2395 } 2396 """ 2397 variables = { 2398 "testRunId": test_run_id 2399 } 2400 data = self._make_graphql_request(query, variables) 2401 # jprint(data) 2402 if not data: 2403 logger.error(f"Failed to get test run comment for test run {test_run_id}") 2404 return None 2405 test_run = data.get('getTestRunById', {}) 2406 if not test_run: 2407 logger.warning(f"No test run found with ID {test_run_id}") 2408 return None 2409 comment = test_run.get('comment') 2410 return comment 2411 except Exception as e: 2412 logger.error(f"Error getting test run comment: {str(e)}") 2413 logger.traceback(e) 2414 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.
2416 def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool: 2417 """ 2418 Append the comment of a specific test run in Xray using the GraphQL API. 2419 This method allows appending the comment associated with a test run 2420 identified by its ID. The comment can provide additional context, test results, 2421 or any other relevant information about the test execution. 2422 Args: 2423 test_run_id (str): The unique identifier of the test run to update. 2424 This is the internal Xray ID for the test run, not the Jira issue key. 2425 test_run_comment (str): The comment text to append to the test run. 2426 This will be added to any existing comment on the test run with proper formatting. 2427 Returns: 2428 bool: True if the comment update was successful, False otherwise. 2429 Returns None if an error occurs during the API request. 2430 Example: 2431 >>> client = XrayGraphQL() 2432 >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32" 2433 >>> success = client.append_test_run_comment( 2434 ... test_run_id, 2435 ... "Test passed with performance within expected range" 2436 ... ) 2437 >>> print(success) 2438 True 2439 Note: 2440 - The test run ID must be valid and accessible with current authentication 2441 - The comment can include any text content, including newlines and special characters 2442 - Failed updates are logged as errors with details about the failure 2443 - This method will append to existing comments with proper line breaks 2444 - If no existing comment exists, the new comment will be set as the initial comment 2445 Raises: 2446 Exception: If there is an error making the GraphQL request or processing the response. 2447 The exception is caught and logged, and the method returns None. 2448 """ 2449 try: 2450 # Get existing comment 2451 existing_comment = self.get_test_run_comment(test_run_id) 2452 # Prepare the combined comment with proper formatting 2453 if existing_comment: 2454 # If there's an existing comment, append with double newline for proper separation 2455 combined_comment = f"{existing_comment}\n{test_run_comment}" 2456 else: 2457 # If no existing comment, use the new comment as is 2458 combined_comment = test_run_comment 2459 logger.debug(f"No existing comment found for test run {test_run_id}, setting initial comment") 2460 query = """ 2461 mutation UpdateTestRunComment($testRunId: String!, $comment: String!) { 2462 updateTestRunComment( 2463 id: $testRunId, 2464 comment: $comment 2465 ) 2466 } 2467 """ 2468 variables = { 2469 "testRunId": test_run_id, 2470 "comment": combined_comment 2471 } 2472 data = self._make_graphql_request(query, variables) 2473 if not data: 2474 logger.error(f"Failed to update test run comment for test run {test_run_id}") 2475 return None 2476 return data['updateTestRunComment'] 2477 except Exception as e: 2478 logger.error(f"Error updating test run comment: {str(e)}") 2479 logger.traceback(e) 2480 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.
2482 def download_attachment_by_extension(self, jira_key: str, file_extension: str) -> Optional[Dict]: 2483 """ 2484 Download JIRA attachments by file extension. 2485 2486 Retrieves all attachments from a JIRA issue that match the specified file extension 2487 and downloads their content. This method searches through all attachments on the 2488 issue and filters by filename ending with the provided extension. 2489 2490 Args: 2491 jira_key (str): The JIRA issue key (e.g., 'PROJ-123') 2492 file_extension (str): The file extension to search for (e.g., '.json', '.txt') 2493 Should include the dot prefix 2494 2495 Returns: 2496 Optional[Dict]: A list of dictionaries containing attachment data, where each 2497 dictionary has filename as key and attachment content as value. 2498 Returns None if: 2499 - Issue cannot be retrieved 2500 - No attachments found with the specified extension 2501 - Error occurs during download 2502 2503 Example: 2504 >>> client = XrayGraphQL() 2505 >>> attachments = client.download_attachment_by_extension('PROJ-123', '.json') 2506 >>> # Returns: [{'document.json': {'content': b'...', 'mime_type': 'application/json'}}] 2507 2508 Raises: 2509 Exception: Logged and handled internally, returns None on any error 2510 """ 2511 try: 2512 response = self.make_jira_request(jira_key, 'GET') 2513 2514 if not response or 'fields' not in response: 2515 logger.error(f"Error: Could not retrieve issue {jira_key}") 2516 return None 2517 2518 # Find attachment by filename 2519 attachments = response.get('fields', {}).get('attachment', []) 2520 target_attachment = [att for att in attachments if att.get('filename').endswith(file_extension)] 2521 if not target_attachment: 2522 logger.error(f"No attachment found for {jira_key} with extension {file_extension}") 2523 return None 2524 2525 combined_attachment = [] 2526 fileDetails = dict() 2527 for attachment in target_attachment: 2528 attachment_id = attachment.get('id') 2529 mime_type = attachment.get('mimeType', '') 2530 fileDetails[attachment.get('filename')] = self.download_jira_attachment_by_id(attachment_id, mime_type) 2531 combined_attachment.append(fileDetails) 2532 2533 return combined_attachment 2534 except Exception as e: 2535 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2536 logger.traceback(e) 2537 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
2539 def download_attachment_by_name(self, jira_key: str, file_name: str) -> Optional[Dict]: 2540 """ 2541 Download JIRA attachments by filename prefix. 2542 2543 Retrieves all attachments from a JIRA issue whose filenames start with the 2544 specified name (case-insensitive). This method searches through all attachments 2545 on the issue and filters by filename starting with the provided name. 2546 2547 Args: 2548 jira_key (str): The JIRA issue key (e.g., 'PROJ-123') 2549 file_name (str): The filename prefix to search for (e.g., 'report', 'test') 2550 Case-insensitive matching is performed 2551 2552 Returns: 2553 Optional[Dict]: A list of dictionaries containing attachment data, where each 2554 dictionary has filename as key and attachment content as value. 2555 Returns None if: 2556 - Issue cannot be retrieved 2557 - No attachments found with the specified filename prefix 2558 - Error occurs during download 2559 2560 Example: 2561 >>> client = XrayGraphQL() 2562 >>> attachments = client.download_attachment_by_name('PROJ-123', 'report') 2563 >>> # Returns: [{'report_v1.json': {'content': b'...', 'mime_type': 'application/json'}}, 2564 >>> # {'report_v2.json': {'content': b'...', 'mime_type': 'application/json'}}] 2565 2566 Raises: 2567 Exception: Logged and handled internally, returns None on any error 2568 """ 2569 try: 2570 response = self.make_jira_request(jira_key, 'GET') 2571 2572 if not response or 'fields' not in response: 2573 logger.error(f"Error: Could not retrieve issue {jira_key}") 2574 return None 2575 2576 # Find attachment by filename 2577 attachments = response.get('fields', {}).get('attachment', []) 2578 target_attachment = [att for att in attachments if att.get('filename').lower().startswith(file_name.lower())] 2579 if not target_attachment: 2580 logger.error(f"No attachment found for {jira_key} with extension {file_name}") 2581 return None 2582 2583 combined_attachment = [] 2584 fileDetails = dict() 2585 for attachment in target_attachment: 2586 attachment_id = attachment.get('id') 2587 mime_type = attachment.get('mimeType', '') 2588 fileDetails[attachment.get('filename')] = self.download_jira_attachment_by_id(attachment_id, mime_type) 2589 combined_attachment.append(fileDetails) 2590 2591 return combined_attachment 2592 except Exception as e: 2593 logger.error(f"Error downloading JIRA attachment: {str(e)}") 2594 logger.traceback(e) 2595 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
2597 def generate_json_from_sentence(self, sentence, template_schema, debug=False): 2598 """Extract information using template schema and spaCy components""" 2599 def _ensure_spacy_model(): 2600 """Ensure spaCy model is available, download if needed""" 2601 try: 2602 nlp = spacy.load("en_core_web_md") 2603 return nlp 2604 except OSError: 2605 import subprocess 2606 import sys 2607 logger.info("Downloading required spaCy model...") 2608 try: 2609 subprocess.check_call([ 2610 sys.executable, "-m", "spacy", "download", "en_core_web_md" 2611 ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 2612 return spacy.load("en_core_web_md") 2613 except subprocess.CalledProcessError: 2614 raise RuntimeError( 2615 "Failed to download spaCy model. Please run manually: " 2616 "python -m spacy download en_core_web_md" 2617 ) 2618 2619 def analyze_model_components(nlp): 2620 """Analyze the loaded model's components and capabilities""" 2621 logger.info("=== Model Analysis ===") 2622 logger.info(f"Model: {nlp.meta['name']}") 2623 logger.info(f"Version: {nlp.meta['version']}") 2624 logger.info(f"Pipeline: {nlp.pipe_names}") 2625 logger.info(f"Components: {list(nlp.pipeline)}") 2626 2627 def parse_template_pattern(pattern_value): 2628 """Parse template pattern to extract structure and placeholders""" 2629 if not isinstance(pattern_value, str): 2630 return None 2631 2632 # Extract placeholders like <string> from the pattern 2633 placeholders = re.findall(r'<(\w+)>', pattern_value) 2634 2635 # Create a regex pattern by replacing placeholders with capture groups 2636 regex_pattern = pattern_value 2637 for placeholder in placeholders: 2638 # Replace <string> with a regex that captures word characters 2639 regex_pattern = regex_pattern.replace(f'<{placeholder}>', r'(\w+)') 2640 2641 return { 2642 'original': pattern_value, 2643 'placeholders': placeholders, 2644 'regex_pattern': regex_pattern, 2645 'regex': re.compile(regex_pattern, re.IGNORECASE) 2646 } 2647 2648 def match_pattern_from_template(pattern_value, doc, debug=False): 2649 """Match a pattern based on template value and return the matched text""" 2650 2651 if isinstance(pattern_value, list): 2652 # Handle list of exact values (for environment, region) 2653 for value in pattern_value: 2654 for token in doc: 2655 if token.text.lower() == value.lower(): 2656 if debug: 2657 logger.info(f"✓ Matched list value '{value}' -> {token.text}") 2658 return token.text 2659 return None 2660 2661 elif isinstance(pattern_value, str): 2662 # Parse the template pattern dynamically 2663 pattern_info = parse_template_pattern(pattern_value) 2664 if not pattern_info: 2665 return None 2666 2667 if debug: 2668 logger.info(f"Parsed pattern: {pattern_info['original']}") 2669 logger.info(f"Regex pattern: {pattern_info['regex_pattern']}") 2670 2671 # Look for tokens that match the pattern 2672 for token in doc: 2673 if pattern_info['regex'].match(token.text): 2674 if debug: 2675 logger.info(f"✓ Matched template pattern '{pattern_value}' -> {token.text}") 2676 return token.text 2677 2678 return None 2679 2680 try: 2681 if not sentence: 2682 logger.error("Sentence is required") 2683 return None 2684 if not template_schema or not isinstance(template_schema, dict): 2685 logger.error("Template schema is required") 2686 return None 2687 if not debug: 2688 debug = False 2689 2690 # Fix: Initialize result with all template schema keys 2691 result = {key: None for key in template_schema.keys()} 2692 result["sentences"] = [] # Initialize as empty list instead of string 2693 2694 except Exception as e: 2695 logger.error(f"Error generating JSON from sentence: {str(e)}") 2696 logger.traceback(e) 2697 return None 2698 2699 # Only add debug fields if debug mode is enabled 2700 if debug: 2701 result.update({ 2702 "tokens_analysis": [], 2703 "entities": [], 2704 "dependencies": [] 2705 }) 2706 2707 try: 2708 nlp = _ensure_spacy_model() 2709 if not nlp: 2710 logger.error("Failed to load spaCy model") 2711 return None 2712 if debug: 2713 # Analyze model capabilities 2714 analyze_model_components(nlp) 2715 doc = nlp(sentence) 2716 2717 # Fix: Ensure sentences list exists before appending 2718 if "sentences" not in result: 2719 result["sentences"] = [] 2720 2721 for sent in doc.sents: 2722 result["sentences"].append(sent.text.strip()) 2723 2724 # 2. Tokenize and analyze each token with spaCy components (only in debug mode) 2725 if debug: 2726 for token in doc: 2727 token_info = { 2728 "text": token.text, 2729 "lemma": token.lemma_, 2730 "pos": token.pos_, 2731 "tag": token.tag_, 2732 "dep": token.dep_, 2733 "head": token.head.text, 2734 "is_alpha": token.is_alpha, 2735 "is_digit": token.is_digit, 2736 "is_punct": token.is_punct, 2737 "shape": token.shape_, 2738 "is_stop": token.is_stop 2739 } 2740 result["tokens_analysis"].append(token_info) 2741 2742 # 3. Dynamic pattern matching based on template schema values 2743 for pattern_key, pattern_value in template_schema.items(): 2744 if not result[pattern_key]: # Only search if not already found 2745 matched_value = match_pattern_from_template(pattern_value, doc, debug) 2746 if matched_value: 2747 result[pattern_key] = matched_value 2748 2749 # 4. Use NER (Named Entity Recognition) component (only in debug mode) 2750 if debug: 2751 for ent in doc.ents: 2752 entity_info = { 2753 "text": ent.text, 2754 "label": ent.label_, 2755 "start": ent.start_char, 2756 "end": ent.end_char 2757 } 2758 result["entities"].append(entity_info) 2759 logger.info(f"✓ NER Entity: {ent.text} - {ent.label_}") 2760 2761 # 5. Use dependency parsing to find relationships (only in debug mode) 2762 if debug: 2763 for token in doc: 2764 dep_info = { 2765 "token": token.text, 2766 "head": token.head.text, 2767 "dependency": token.dep_, 2768 "children": [child.text for child in token.children] 2769 } 2770 result["dependencies"].append(dep_info) 2771 2772 # 6. Use lemmatizer to find action verbs and their objects 2773 for token in doc: 2774 if token.lemma_ in ["verify", "validate", "check", "test"]: 2775 if debug: 2776 logger.info(f"✓ Found action verb: {token.text} (lemma: {token.lemma_})") 2777 # Find what's being verified/validated 2778 for child in token.children: 2779 if child.dep_ in ["dobj", "pobj"]: 2780 if debug: 2781 logger.info(f"✓ Found verification target: {child.text}") 2782 2783 return result 2784 except Exception as e: 2785 logger.error(f"Error extracting information from document: {str(e)}") 2786 logger.traceback(e) 2787 return None
Extract information using template schema and spaCy components