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"] 
class JiraHandler:
 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
JiraHandler()
 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

def create_issue( self, project_key: str, summary: str, description: str, issue_type: str = None, priority: str = None, assignee: str = None, labels: List[str] = None, components: List[str] = None, attachments: List[str] = None, parent_issue_key: str = None, linked_issues: List[Dict[str, str]] = None, custom_fields: Dict[str, Any] = None) -> Optional[tuple[str, str]]:
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]}")
def get_issue( self, issue_key: str, fields: List[str] = None) -> Optional[Dict[str, Any]]:
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
def update_issue_summary(self, issue_key: str, new_summary: str) -> bool:
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
def make_jira_request( self, jira_key: str, url: str, method: str = 'GET', payload: Dict = None, api_key: str = None, user: str = None, cookie: str = None) -> Optional[Dict]:
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', ...}
def download_jira_attachment_by_id(self, attachment_id: str, mime_type: str) -> Optional[Dict]:
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.

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

A comprehensive client for interacting with Xray Cloud's GraphQL API. This class extends JiraHandler to provide specialized methods for interacting with Xray Cloud's GraphQL API for test management. It handles authentication, test plans, test executions, test runs, defects, evidence, and other Xray-related operations through GraphQL queries and mutations.

Inherits

JiraHandler Base class providing JIRA client functionality and issue management

Attributes

client_id : str The client ID for Xray authentication client_secret : str The client secret for Xray authentication xray_base_url : str Base URL for Xray Cloud API (defaults to 'https://us.xray.cloud.getxray.app') logger : Logger Logger instance for debugging and error tracking token : str Authentication token obtained from Xray

Methods

Authentication & Setup

__init__() Initialize XrayGraphQL client with authentication and configuration settings. _get_auth_token() -> Optional[str] Authenticate with Xray Cloud API and obtain an authentication token. _make_graphql_request(query: str, variables: Dict) -> Optional[Dict] Makes a GraphQL request to the Xray API with proper authentication. _parse_table(table_str: str) -> Dict[str, Union[List[int], List[float]]] Parse a string representation of a table into a dictionary of numeric values.

Issue ID Management

get_issue_id_from_jira_id(issue_key: str) -> Optional[str] Retrieves the internal Xray issue ID for a given JIRA issue key and type.

Test Plan Operations

get_tests_from_test_plan(test_plan: str) -> Optional[Dict[str, str]] Retrieves all tests associated with a given test plan. get_test_plan_data(test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]] Retrieves and parses tabular data from a test plan's description.

Test Set Operations

get_tests_from_test_set(test_set: str) -> Optional[Dict[str, str]] Retrieves all tests associated with a given test set. filter_test_set_by_test_case(test_key: str) -> Optional[Dict[str, str]] Retrieves all test sets containing a specific test case. filter_tags_by_test_case(test_key: str) -> Optional[List[str]] Extracts and filters tags from test sets associated with a test case.

Test Execution Operations

get_tests_from_test_execution(test_execution: str) -> Optional[Dict[str, str]] Retrieves all tests associated with a given test execution. get_test_execution(test_execution: str) -> Optional[Dict] Retrieve detailed information about a test execution from Xray. create_test_execution(test_issue_keys: List[str], project_key: Optional[str], summary: Optional[str], description: Optional[str]) -> Optional[Dict] Creates a new test execution with specified test cases. create_test_execution_from_test_plan(test_plan: str) -> Optional[Dict[str, Dict[str, str]]] Creates a test execution from a given test plan with all associated tests. add_test_execution_to_test_plan(test_plan: str, test_execution: str) -> Optional[Dict] Add a test execution to an existing test plan in Xray.

Test Run Operations

get_test_runstatus(test_case: str, test_execution: str) -> Optional[Tuple[str, str]] Retrieves the status of a test run for a specific test case. get_test_run_by_id(test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]] Retrieves the test run ID and status using internal Xray IDs. update_test_run_status(test_run_id: str, test_run_status: str) -> bool Updates the status of a specific test run. update_test_run_comment(test_run_id: str, test_run_comment: str) -> bool Updates the comment of a specific test run. get_test_run_comment(test_run_id: str) -> Optional[str] Retrieve the comment of a specific test run from Xray. append_test_run_comment(test_run_id: str, test_run_comment: str) -> bool Append a comment to an existing test run comment.

Evidence & Defect Management

add_evidence_to_test_run(test_run_id: str, evidence_path: str) -> bool Add evidence (attachments) to a test run in Xray. create_defect_from_test_run(test_run_id: str, project_key: str, parent_issue_key: str, defect_summary: str, defect_description: str) -> Optional[Dict] Create a defect from a test run and link it to the test run in Xray.

Examples

>>> client = XrayGraphQL()
>>> test_plan_tests = client.get_tests_from_test_plan("TEST-123")
>>> print(test_plan_tests)
{'TEST-124': '10001', 'TEST-125': '10002'}
>>> test_execution = client.create_test_execution_from_test_plan("TEST-123")
>>> print(test_execution)
{
    'TEST-124': {
        'test_run_id': '5f7c3',
        'test_execution_key': 'TEST-456',
        'test_plan_key': 'TEST-123'
    }
}
>>> # Update test run status
>>> success = client.update_test_run_status("test_run_id", "PASS")
>>> print(success)
True
>>> # Add evidence to test run
>>> evidence_added = client.add_evidence_to_test_run("test_run_id", "/path/to/screenshot.png")
>>> print(evidence_added)
True
<h2 id="notes">Notes</h2>
  • Requires valid Xray Cloud credentials (client_id and client_secret)
  • Uses GraphQL for all API interactions
  • Implements automatic token refresh
  • Handles rate limiting and retries
  • All methods include comprehensive error handling and logging
  • Returns None for failed operations instead of raising exceptions
  • Supports various file types for evidence attachments
  • Integrates with JIRA for defect creation and issue management
  • Provides both synchronous and asynchronous operation patterns
  • Includes retry logic for transient failures
XrayGraphQL()
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:

  1. Loading environment variables from .env file
  2. Reading required environment variables for authentication
  3. Configuring the base URL for Xray Cloud
  4. 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

def get_issue_id_from_jira_id(self, issue_key: str) -> Optional[str]:
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
def get_test_details(self, issue_key: str, issue_type: str) -> Optional[str]:
1045    def get_test_details(self, issue_key: str, issue_type: str) -> Optional[str]:
1046        """
1047        Retrieves the internal Xray issue ID for a given JIRA issue key and type.
1048        This method queries the Xray GraphQL API to find the internal issue ID corresponding
1049        to a JIRA issue key. It supports different types of Xray artifacts including test plans,
1050        test executions, test sets, and tests.
1051        Args:
1052            issue_key (str): The JIRA issue key (e.g., "PROJECT-123")
1053            issue_type (str): The type of Xray artifact. Supported values are:
1054                - "plan" or contains "plan": For Test Plans
1055                - "exec" or contains "exec": For Test Executions
1056                - "set" or contains "set": For Test Sets
1057                - "test" or contains "test": For Tests
1058                If not provided, defaults to "plan"
1059        Returns:
1060            Optional[str]: The internal Xray issue ID if found, None if:
1061                - The issue key doesn't exist
1062                - The GraphQL request fails
1063                - Any other error occurs during processing
1064        Examples:
1065            >>> client.get_issue_id_from_jira_id("TEST-123")
1066            '10000'
1067            >>> client.get_issue_id_from_jira_id("TEST-456")
1068            '10001'
1069            >>> client.get_issue_id_from_jira_id("INVALID-789")
1070            None
1071        Note:
1072            The method performs a case-insensitive comparison when matching issue keys.
1073            The project key is extracted from the issue_key (text before the hyphen)
1074            to filter results by project.
1075        """
1076        try:
1077            parse_project = issue_key.split("-")[0]
1078            function_name = "getTestPlans"
1079            if not issue_type:
1080                issue_type = "plan"
1081            if "plan" in issue_type.lower():
1082                function_name = "getTestPlans"
1083                jira_fields = [
1084                    "key", "summary", "description", "assignee", 
1085                    "status", "priority", "labels", "created", 
1086                    "updated", "dueDate", "components", "versions", 
1087                    "attachments", "comments"
1088                ]
1089                query = """
1090                    query GetDetails($limit: Int!, $jql: String!) {    
1091                        getTestPlans(limit: $limit, jql:$jql) {
1092                            results {
1093                                issueId
1094                                jira(fields: ["key"])
1095                            }
1096                        }
1097                    }
1098                    """
1099            if "exec" in issue_type.lower():
1100                function_name = "getTestExecutions"
1101                jira_fields = [
1102                    "key", "summary", "description", "assignee", 
1103                    "status", "priority", "labels", "created", 
1104                    "updated", "dueDate", "components", "versions", 
1105                    "attachments", "comments"
1106                ]
1107                query = """
1108                    query GetDetails($limit: Int!, $jql: String!) {    
1109                        getTestExecutions(limit: $limit, jql:$jql) {
1110                            results {
1111                                issueId
1112                                jira(fields: ["key"])
1113                            }
1114                        }
1115                    }
1116                    """
1117            if "set" in issue_type.lower():
1118                function_name = "getTestSets"
1119                jira_fields = [
1120                    "key", "summary", "description", "assignee", 
1121                    "status", "priority", "labels", "created", 
1122                    "updated", "dueDate", "components", "versions", 
1123                    "attachments", "comments"
1124                ]
1125                query = """
1126                    query GetDetails($limit: Int!, $jql: String!) {    
1127                        getTestSets(limit: $limit, jql:$jql) {
1128                            results {
1129                                issueId
1130                                jira(fields: ["key"])
1131                            }
1132                        }
1133                    }
1134                    """
1135            if "test" in issue_type.lower():
1136                function_name = "getTests"
1137                jira_fields = [
1138                    "key", "summary", "description", "assignee", 
1139                    "status", "priority", "labels", "created", 
1140                    "updated", "dueDate", "components", "versions", 
1141                    "attachments", "comments"
1142                ]
1143                query = """
1144                    query GetDetails($limit: Int!, $jql: String!, $jiraFields: [String!]!) {    
1145                        getTests(limit: $limit, jql:$jql) {
1146                            results {
1147                                issueId
1148                                jira(fields: $jiraFields)
1149                                steps {
1150                                    id
1151                                    action
1152                                    data
1153                                    result
1154                                    attachments {
1155                                    id
1156                                    filename
1157                                    storedInJira
1158                                    downloadLink
1159                                    }
1160                                }
1161                                
1162                            }
1163                        }
1164                    }
1165                    """
1166            variables = {
1167                "limit": 10,
1168                "jql": f"project = '{parse_project}' AND key = '{issue_key}'",
1169                "jiraFields": jira_fields
1170            }
1171            data = self._make_graphql_request(query, variables)
1172            if not data:
1173                logger.error(f"Failed to get issue ID for {issue_key}")
1174                return None
1175            for issue in data[function_name]['results']:
1176                if str(issue['jira']['key']).lower() == issue_key.lower():
1177                    return issue  # This now includes all metadata
1178            return None
1179        except Exception as e:
1180            logger.error(f"Failed to get issue ID for {issue_key}")
1181            logger.traceback(e)
1182            return None

Retrieves the internal Xray issue ID for a given JIRA issue key and type. This method queries the Xray GraphQL API to find the internal issue ID corresponding to a JIRA issue key. It supports different types of Xray artifacts including test plans, test executions, test sets, and tests. Args: issue_key (str): The JIRA issue key (e.g., "PROJECT-123") issue_type (str): The type of Xray artifact. Supported values are: - "plan" or contains "plan": For Test Plans - "exec" or contains "exec": For Test Executions - "set" or contains "set": For Test Sets - "test" or contains "test": For Tests If not provided, defaults to "plan" Returns: Optional[str]: The internal Xray issue ID if found, None if: - The issue key doesn't exist - The GraphQL request fails - Any other error occurs during processing Examples:

client.get_issue_id_from_jira_id("TEST-123") '10000' client.get_issue_id_from_jira_id("TEST-456") '10001' client.get_issue_id_from_jira_id("INVALID-789") None Note: The method performs a case-insensitive comparison when matching issue keys. The project key is extracted from the issue_key (text before the hyphen) to filter results by project.

def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]:
1184    def get_tests_from_test_plan(self, test_plan: str) -> Optional[Dict[str, str]]:
1185        """
1186        Retrieves all tests associated with a given test plan from Xray.
1187        This method queries the Xray GraphQL API to fetch all tests that are part of the specified
1188        test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that
1189        ID to fetch the associated tests.
1190        Args:
1191            test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123")
1192        Returns:
1193            Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs,
1194                or None if the operation fails. For example:
1195                {
1196                    "PROJECT-124": "10001",
1197                    "PROJECT-125": "10002"
1198                }
1199                Returns None in the following cases:
1200                - Test plan ID cannot be found
1201                - GraphQL request fails
1202                - Any other error occurs during processing
1203        Example:
1204            >>> client = XrayGraphQL()
1205            >>> tests = client.get_tests_from_test_plan("PROJECT-123")
1206            >>> print(tests)
1207            {"PROJECT-124": "10001", "PROJECT-125": "10002"}
1208        Note:
1209            - The method is limited to retrieving 99999 tests per test plan
1210            - Test plan must exist in Xray and be accessible with current authentication
1211        """
1212        try:
1213            test_plan_id = self.get_issue_id_from_jira_id(test_plan)
1214            if not test_plan_id:
1215                logger.error(f"Failed to get test plan ID for {test_plan}")
1216                return None
1217            query = """
1218            query GetTestPlanTests($testPlanId: String!) {
1219                getTestPlan(issueId: $testPlanId) {
1220                    tests(limit: 100) {
1221                        results {   
1222                            issueId
1223                            jira(fields: ["key"])
1224                        }
1225                    }
1226                }
1227            }
1228            """
1229            variables = {"testPlanId": test_plan_id}
1230            data = self._make_graphql_request(query, variables)
1231            if not data:
1232                logger.error(f"Failed to get tests for plan {test_plan_id}")
1233                return None
1234            tests = {}
1235            for test in data['getTestPlan']['tests']['results']:
1236                tests[test['jira']['key']] = test['issueId']
1237            return tests
1238        except Exception as e:
1239            logger.error(f"Failed to get tests for plan {test_plan_id}")
1240            logger.traceback(e)
1241            return None

Retrieves all tests associated with a given test plan from Xray. This method queries the Xray GraphQL API to fetch all tests that are part of the specified test plan. It first converts the JIRA test plan key to an internal Xray ID, then uses that ID to fetch the associated tests. Args: test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, or None if the operation fails. For example: { "PROJECT-124": "10001", "PROJECT-125": "10002" } Returns None in the following cases: - Test plan ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:

client = XrayGraphQL() tests = client.get_tests_from_test_plan("PROJECT-123") print(tests) {"PROJECT-124": "10001", "PROJECT-125": "10002"} Note: - The method is limited to retrieving 99999 tests per test plan - Test plan must exist in Xray and be accessible with current authentication

def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]:
1243    def get_tests_from_test_set(self, test_set: str) -> Optional[Dict[str, str]]:
1244        """
1245        Retrieves all tests associated with a given test set from Xray.
1246        This method queries the Xray GraphQL API to fetch all tests that are part of the specified
1247        test set. It first converts the JIRA test set key to an internal Xray ID, then uses that
1248        ID to fetch the associated tests.
1249        Args:
1250            test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123")
1251        Returns:
1252            Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs,
1253                or None if the operation fails. For example:
1254                {
1255                    "PROJECT-124": "10001",
1256                    "PROJECT-125": "10002"
1257                }
1258                Returns None in the following cases:
1259                - Test set ID cannot be found
1260                - GraphQL request fails
1261                - Any other error occurs during processing
1262        Example:
1263            >>> client = XrayGraphQL()
1264            >>> tests = client.get_tests_from_test_set("PROJECT-123")
1265            >>> print(tests)
1266            {"PROJECT-124": "10001", "PROJECT-125": "10002"}
1267        Note:
1268            - The method is limited to retrieving 99999 tests per test set
1269            - Test set must exist in Xray and be accessible with current authentication
1270        """
1271        try:
1272            test_set_id = self.get_issue_id_from_jira_id(test_set)
1273            if not test_set_id:
1274                logger.error(f"Failed to get test set ID for {test_set}")
1275                return None
1276            query = """
1277            query GetTestSetTests($testSetId: String!) {
1278                getTestSet(issueId: $testSetId) {
1279                    tests(limit: 100) {
1280                        results {   
1281                            issueId
1282                            jira(fields: ["key"])
1283                        }
1284                    }
1285                }
1286            }
1287            """
1288            variables = {"testSetId": test_set_id}
1289            data = self._make_graphql_request(query, variables)
1290            if not data:
1291                logger.error(f"Failed to get tests for set {test_set_id}")
1292                return None
1293            tests = {}
1294            for test in data['getTestSet']['tests']['results']:
1295                tests[test['jira']['key']] = test['issueId']
1296            return tests
1297        except Exception as e:
1298            logger.error(f"Failed to get tests for set {test_set_id}")
1299            logger.traceback(e)
1300            return None

Retrieves all tests associated with a given test set from Xray. This method queries the Xray GraphQL API to fetch all tests that are part of the specified test set. It first converts the JIRA test set key to an internal Xray ID, then uses that ID to fetch the associated tests. Args: test_set (str): The JIRA issue key of the test set (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, or None if the operation fails. For example: { "PROJECT-124": "10001", "PROJECT-125": "10002" } Returns None in the following cases: - Test set ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:

client = XrayGraphQL() tests = client.get_tests_from_test_set("PROJECT-123") print(tests) {"PROJECT-124": "10001", "PROJECT-125": "10002"} Note: - The method is limited to retrieving 99999 tests per test set - Test set must exist in Xray and be accessible with current authentication

def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]:
1302    def get_tests_from_test_execution(self, test_execution: str) -> Optional[Dict[str, str]]:
1303        """
1304        Retrieves all tests associated with a given test execution from Xray.
1305        This method queries the Xray GraphQL API to fetch all tests that are part of the specified
1306        test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that
1307        ID to fetch the associated tests.
1308        Args:
1309            test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123")
1310        Returns:
1311            Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs,
1312                or None if the operation fails. For example:
1313                {
1314                    "PROJECT-124": "10001",
1315                    "PROJECT-125": "10002"
1316                }
1317                Returns None in the following cases:
1318                - Test execution ID cannot be found
1319                - GraphQL request fails
1320                - Any other error occurs during processing
1321        Example:
1322            >>> client = XrayGraphQL()
1323            >>> tests = client.get_tests_from_test_execution("PROJECT-123")
1324            >>> print(tests)
1325            {"PROJECT-124": "10001", "PROJECT-125": "10002"}
1326        Note:
1327            - The method is limited to retrieving 99999 tests per test execution
1328            - Test execution must exist in Xray and be accessible with current authentication
1329        """
1330        try:
1331            test_execution_id = self.get_issue_id_from_jira_id(test_execution)
1332            if not test_execution_id:
1333                logger.error(f"Failed to get test execution ID for {test_execution}")
1334                return None
1335            query = """
1336            query GetTestExecutionTests($testExecutionId: String!) {
1337                getTestExecution(issueId: $testExecutionId) {
1338                    tests(limit: 100) {
1339                        results {   
1340                            issueId
1341                            jira(fields: ["key"])
1342                        }
1343                    }
1344                }
1345            }
1346            """
1347            variables = {"testExecutionId": test_execution_id}
1348            data = self._make_graphql_request(query, variables)
1349            if not data:
1350                logger.error(f"Failed to get tests for execution {test_execution_id}")
1351                return None
1352            tests = {}
1353            for test in data['getTestExecution']['tests']['results']:
1354                tests[test['jira']['key']] = test['issueId']
1355            return tests
1356        except Exception as e:
1357            logger.error(f"Failed to get tests for execution {test_execution_id}")
1358            logger.traceback(e)
1359            return None

Retrieves all tests associated with a given test execution from Xray. This method queries the Xray GraphQL API to fetch all tests that are part of the specified test execution. It first converts the JIRA test execution key to an internal Xray ID, then uses that ID to fetch the associated tests. Args: test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test JIRA keys to their Xray issue IDs, or None if the operation fails. For example: { "PROJECT-124": "10001", "PROJECT-125": "10002" } Returns None in the following cases: - Test execution ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:

client = XrayGraphQL() tests = client.get_tests_from_test_execution("PROJECT-123") print(tests) {"PROJECT-124": "10001", "PROJECT-125": "10002"} Note: - The method is limited to retrieving 99999 tests per test execution - Test execution must exist in Xray and be accessible with current authentication

def get_test_plan_data( self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]:
1361    def get_test_plan_data(self, test_plan: str) -> Optional[Dict[str, Union[List[int], List[float]]]]:
1362        """
1363        Retrieve and parse tabular data from a test plan's description field in Xray.
1364        This method fetches a test plan's description from Xray and parses any tables found within it.
1365        The tables in the description are expected to be in a specific format that can be parsed by
1366        the _parse_table method. The parsed data is returned as a dictionary containing numeric values
1367        and lists extracted from the table.
1368        Args:
1369            test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123")
1370        Returns:
1371            Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data
1372                where keys are derived from the first column of the table and values are lists of numeric
1373                values. Returns None if:
1374                - The test plan ID cannot be found
1375                - The GraphQL request fails
1376                - The description cannot be parsed
1377                - Any other error occurs during processing
1378        Example:
1379            >>> client = XrayGraphQL()
1380            >>> data = client.get_test_plan_data("TEST-123")
1381            >>> print(data)
1382            {
1383                'temperature': [20, 25, 30],
1384                'pressure': [1.0, 1.5, 2.0],
1385                'measurements': [[1, 2, 3], [4, 5, 6]]
1386            }
1387        Note:
1388            - The test plan must exist in Xray and be accessible with current authentication
1389            - The description must contain properly formatted tables for parsing
1390            - Table values are converted to numeric types (int or float) where possible
1391            - Lists in table cells should be formatted as [value1, value2, ...]
1392            - Failed operations are logged as errors with relevant details
1393        """
1394        try:
1395            test_plan_id = self.get_issue_id_from_jira_id(test_plan)
1396            if not test_plan_id:
1397                logger.error(f"Failed to get test plan ID for {test_plan}")
1398                return None
1399            query = """
1400            query GetTestPlanTests($testPlanId: String!) {
1401                getTestPlan(issueId: $testPlanId) {
1402                    issueId
1403                    jira(fields: ["key","description"])
1404                }
1405            }
1406            """
1407            variables = {"testPlanId": test_plan_id}
1408            data = self._make_graphql_request(query, variables)
1409            if not data:
1410                logger.error(f"Failed to get tests for plan {test_plan_id}")
1411                return None
1412            description = data['getTestPlan']['jira']['description']
1413            test_plan_data = self._parse_table(description)
1414            return test_plan_data            
1415        except Exception as e:
1416            logger.error(f"Failed to get tests for plan {test_plan_id}")
1417            logger.traceback(e)
1418            return None

Retrieve and parse tabular data from a test plan's description field in Xray. This method fetches a test plan's description from Xray and parses any tables found within it. The tables in the description are expected to be in a specific format that can be parsed by the _parse_table method. The parsed data is returned as a dictionary containing numeric values and lists extracted from the table. Args: test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") Returns: Optional[Dict[str, Union[List[int], List[float]]]]: A dictionary containing the parsed table data where keys are derived from the first column of the table and values are lists of numeric values. Returns None if: - The test plan ID cannot be found - The GraphQL request fails - The description cannot be parsed - Any other error occurs during processing Example:

client = XrayGraphQL() data = client.get_test_plan_data("TEST-123") print(data) { 'temperature': [20, 25, 30], 'pressure': [1.0, 1.5, 2.0], 'measurements': [[1, 2, 3], [4, 5, 6]] } Note: - The test plan must exist in Xray and be accessible with current authentication - The description must contain properly formatted tables for parsing - Table values are converted to numeric types (int or float) where possible - Lists in table cells should be formatted as [value1, value2, ...] - Failed operations are logged as errors with relevant details

def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]:
1420    def filter_test_set_by_test_case(self, test_key: str) -> Optional[Dict[str, str]]:
1421        """
1422        Retrieves all test sets that contain a specific test case from Xray.
1423        This method queries the Xray GraphQL API to find all test sets that include the specified
1424        test case. It first converts the JIRA test case key to an internal Xray ID, then uses that
1425        ID to fetch all associated test sets.
1426        Args:
1427            test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123")
1428        Returns:
1429            Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries,
1430                or None if the operation fails. For example:
1431                {
1432                    "PROJECT-124": "Test Set for Feature A",
1433                    "PROJECT-125": "Regression Test Set"
1434                }
1435                Returns None in the following cases:
1436                - Test case ID cannot be found
1437                - GraphQL request fails
1438                - Any other error occurs during processing
1439        Example:
1440            >>> client = XrayGraphQL()
1441            >>> test_sets = client.filter_test_set_by_test_case("PROJECT-123")
1442            >>> print(test_sets)
1443            {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"}
1444        Note:
1445            - The method is limited to retrieving 99999 test sets per test case
1446            - Test case must exist in Xray and be accessible with current authentication
1447        """
1448        try:
1449            test_id = self.get_issue_id_from_jira_id(test_key)
1450            if not test_id:
1451                logger.error(f"Failed to get test ID for  Test Case ({test_key})")
1452                return None
1453            query = """
1454            query GetTestDetails($testId: String!) {
1455                getTest(issueId: $testId) {
1456                    testSets(limit: 100) {
1457                        results {   
1458                            issueId
1459                            jira(fields: ["key","summary"])
1460                        }
1461                    }
1462                }
1463            }   
1464            """
1465            variables = {
1466                "testId": test_id
1467            }
1468            data = self._make_graphql_request(query, variables)
1469            if not data:
1470                logger.error(f"Failed to get tests for plan {test_id}")
1471                return None
1472            retDict = {}
1473            for test in data['getTest']['testSets']['results']:
1474                retDict[test['jira']['key']] = test['jira']['summary']
1475            return retDict
1476        except Exception as e:
1477            logger.error(f"Error in getting test set by test id: {e}")
1478            logger.traceback(e)
1479            return None 

Retrieves all test sets that contain a specific test case from Xray. This method queries the Xray GraphQL API to find all test sets that include the specified test case. It first converts the JIRA test case key to an internal Xray ID, then uses that ID to fetch all associated test sets. Args: test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") Returns: Optional[Dict[str, str]]: A dictionary mapping test set JIRA keys to their summaries, or None if the operation fails. For example: { "PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set" } Returns None in the following cases: - Test case ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:

client = XrayGraphQL() test_sets = client.filter_test_set_by_test_case("PROJECT-123") print(test_sets) {"PROJECT-124": "Test Set for Feature A", "PROJECT-125": "Regression Test Set"} Note: - The method is limited to retrieving 99999 test sets per test case - Test case must exist in Xray and be accessible with current authentication

def filter_tags_by_test_case(self, test_key: str) -> Optional[List[str]]:
1481    def filter_tags_by_test_case(self, test_key: str) -> Optional[List[str]]:
1482        """
1483        Extract and filter tags from test sets associated with a specific test case in Xray.
1484        This method queries the Xray GraphQL API to find all test sets associated with the given
1485        test case and extracts tags from their summaries. Tags are identified from test set summaries
1486        that start with either 'tag' or 'benchtype' prefixes.
1487        Args:
1488            test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123")
1489        Returns:
1490            Optional[List[str]]: A list of unique tags extracted from test set summaries,
1491                or None if no tags are found or an error occurs. Tags are:
1492                - Extracted from summaries starting with 'tag:' or 'benchtype:'
1493                - Split on commas, semicolons, double pipes, or whitespace
1494                - Converted to lowercase and stripped of whitespace
1495        Example:
1496            >>> client = XrayGraphQL()
1497            >>> tags = client.filter_tags_by_test_case("TEST-123")
1498            >>> print(tags)
1499            ['regression', 'smoke', 'performance']
1500        Note:
1501            - Test sets must have summaries in the format "tag: tag1, tag2" or "benchtype: type1, type2"
1502            - Tags are extracted only from summaries with the correct prefix
1503            - All tags are converted to lowercase for consistency
1504            - Duplicate tags are automatically removed via set conversion
1505            - Returns None if no valid tags are found or if an error occurs
1506        """
1507        try:
1508            test_id = self.get_issue_id_from_jira_id(test_key)
1509            if not test_id:
1510                logger.error(f"Failed to get test ID for  Test Case ({test_key})")
1511                return None
1512            query = """
1513            query GetTestDetails($testId: String!) {
1514                getTest(issueId: $testId) {
1515                    testSets(limit: 100) {
1516                        results {   
1517                            issueId
1518                            jira(fields: ["key","summary"])
1519                        }
1520                    }
1521                }
1522            }   
1523            """
1524            variables = {
1525                "testId": test_id
1526            }
1527            data = self._make_graphql_request(query, variables)
1528            if not data:
1529                logger.error(f"Failed to get tests for plan {test_id}")
1530                return None
1531            tags = set()
1532            for test in data['getTest']['testSets']['results']:
1533                summary = str(test['jira']['summary']).strip().lower()
1534                if summary.startswith(('tag', 'benchtype')):
1535                    summary = summary.split(':', 1)[-1].strip()  # Split only once and take last part
1536                    tags.update(tag for tag in re.split(r'[,|;|\|\|]\s*|\s+', summary) if tag)
1537            if tags:
1538                return list(tags)
1539            else:
1540                return None
1541        except Exception as e:
1542            logger.error(f"Error in getting test set by test id: {e}")
1543            logger.traceback(e)
1544            return None 

Extract and filter tags from test sets associated with a specific test case in Xray. This method queries the Xray GraphQL API to find all test sets associated with the given test case and extracts tags from their summaries. Tags are identified from test set summaries that start with either 'tag' or 'benchtype' prefixes. Args: test_key (str): The JIRA issue key of the test case (e.g., "PROJECT-123") Returns: Optional[List[str]]: A list of unique tags extracted from test set summaries, or None if no tags are found or an error occurs. Tags are: - Extracted from summaries starting with 'tag:' or 'benchtype:' - Split on commas, semicolons, double pipes, or whitespace - Converted to lowercase and stripped of whitespace Example:

client = XrayGraphQL() tags = client.filter_tags_by_test_case("TEST-123") print(tags) ['regression', 'smoke', 'performance'] Note: - Test sets must have summaries in the format "tag: tag1, tag2" or "benchtype: type1, type2" - Tags are extracted only from summaries with the correct prefix - All tags are converted to lowercase for consistency - Duplicate tags are automatically removed via set conversion - Returns None if no valid tags are found or if an error occurs

def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]:
1546    def get_test_runstatus(self, test_case: str, test_execution: str) -> Optional[Tuple[str, str]]:
1547        """
1548        Retrieve the status of a test run for a specific test case within a test execution.
1549        This method queries the Xray GraphQL API to get the current status of a test run,
1550        which represents the execution status of a specific test case within a test execution.
1551        It first converts both the test case and test execution JIRA keys to their internal
1552        Xray IDs, then uses these to fetch the test run status.
1553        Args:
1554            test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123")
1555            test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456")
1556        Returns:
1557            Tuple[Optional[str], Optional[str]]: A tuple containing:
1558                - test_run_id: The unique identifier of the test run (or None if not found)
1559                - test_run_status: The current status of the test run (or None if not found)
1560                Returns (None, None) if any error occurs during the process.
1561        Example:
1562            >>> client = XrayGraphQL()
1563            >>> run_id, status = client.get_test_runstatus("TEST-123", "TEST-456")
1564            >>> print(f"Test Run ID: {run_id}, Status: {status}")
1565            Test Run ID: 10001, Status: PASS
1566        Note:
1567            - Both the test case and test execution must exist in Xray and be accessible
1568            - The test case must be associated with the test execution
1569            - The method performs two ID lookups before querying the test run status
1570            - Failed operations are logged as errors with relevant details
1571        """
1572        try:
1573            test_case_id = self.get_issue_id_from_jira_id(test_case)
1574            if not test_case_id:
1575                logger.error(f"Failed to get test ID for Test Case ({test_case})")
1576                return None
1577            test_exec_id = self.get_issue_id_from_jira_id(test_execution)
1578            if not test_exec_id:
1579                logger.error(f"Failed to get test execution ID for Test Execution ({test_execution})")
1580                return None
1581            query = """
1582            query GetTestRunStatus($testId: String!, $testExecutionId: String!) {
1583                getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) {
1584                    id
1585                    status {
1586                        name
1587                    }
1588                }
1589            }
1590            """
1591            variables = {
1592                "testId": test_case_id,
1593                "testExecutionId": test_exec_id,
1594            }
1595            # Add debug loggerging
1596            logger.debug(f"Getting test run status for test {test_case_id} in execution {test_exec_id}")
1597            data = self._make_graphql_request(query, variables)
1598            if not data:
1599                logger.error(f"Failed to get test run status for test {test_case_id}")
1600                return None
1601            # jprint(data)
1602            test_run_id = data['getTestRun']['id']
1603            test_run_status = data['getTestRun']['status']['name']
1604            return (test_run_id, test_run_status)
1605        except Exception as e:
1606            logger.error(f"Error getting test run status: {str(e)}")
1607            logger.traceback(e)
1608            return (None, None)

Retrieve the status of a test run for a specific test case within a test execution. This method queries the Xray GraphQL API to get the current status of a test run, which represents the execution status of a specific test case within a test execution. It first converts both the test case and test execution JIRA keys to their internal Xray IDs, then uses these to fetch the test run status. Args: test_case (str): The JIRA issue key of the test case (e.g., "PROJECT-123") test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-456") Returns: Tuple[Optional[str], Optional[str]]: A tuple containing: - test_run_id: The unique identifier of the test run (or None if not found) - test_run_status: The current status of the test run (or None if not found) Returns (None, None) if any error occurs during the process. Example:

client = XrayGraphQL() run_id, status = client.get_test_runstatus("TEST-123", "TEST-456") print(f"Test Run ID: {run_id}, Status: {status}") Test Run ID: 10001, Status: PASS Note: - Both the test case and test execution must exist in Xray and be accessible - The test case must be associated with the test execution - The method performs two ID lookups before querying the test run status - Failed operations are logged as errors with relevant details

def get_test_run_by_id( self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]:
1610    def get_test_run_by_id(self, test_case_id: str, test_execution_id: str) -> Optional[Tuple[str, str]]:
1611        """
1612        Retrieves the test run ID and status for a specific test case within a test execution using GraphQL.
1613        Args:
1614            test_case_id (str): The ID of the test case to query
1615            test_execution_id (str): The ID of the test execution containing the test run
1616        Returns:
1617            tuple[Optional[str], Optional[str]]: A tuple containing:
1618                - test_run_id: The ID of the test run if found, None if not found or on error
1619                - test_run_status: The status name of the test run if found, None if not found or on error
1620        Note:
1621            The function makes a GraphQL request to fetch the test run information. If the request fails
1622            or encounters any errors, it will log the error and return (None, None).
1623        """
1624        try:
1625            query = """
1626            query GetTestRunStatus($testId: String!, $testExecutionId: String!) {
1627                getTestRun( testIssueId: $testId, testExecIssueId: $testExecutionId) {
1628                    id
1629                    status {
1630                        name
1631                    }
1632                }
1633            }
1634            """
1635            variables = {
1636                "testId": test_case_id,
1637                "testExecutionId": test_execution_id,
1638            }
1639            # Add debug loggerging
1640            logger.debug(f"Getting test run status for test {test_case_id} in execution {test_execution_id}")
1641            data = self._make_graphql_request(query, variables)
1642            if not data:
1643                logger.error(f"Failed to get test run status for test {test_case_id}")
1644                return None
1645            test_run_id = data['getTestRun']['id']
1646            test_run_status = data['getTestRun']['status']['name']
1647            return (test_run_id, test_run_status)
1648        except Exception as e:
1649            logger.error(f"Error getting test run status: {str(e)}")
1650            logger.traceback(e)
1651            return (None, None)

Retrieves the test run ID and status for a specific test case within a test execution using GraphQL. Args: test_case_id (str): The ID of the test case to query test_execution_id (str): The ID of the test execution containing the test run Returns: tuple[Optional[str], Optional[str]]: A tuple containing: - test_run_id: The ID of the test run if found, None if not found or on error - test_run_status: The status name of the test run if found, None if not found or on error Note: The function makes a GraphQL request to fetch the test run information. If the request fails or encounters any errors, it will log the error and return (None, None).

def get_test_execution(self, test_execution: str) -> Optional[Dict]:
1653    def get_test_execution(self, test_execution: str) -> Optional[Dict]:
1654        """
1655        Retrieve detailed information about a test execution from Xray.
1656        This method queries the Xray GraphQL API to fetch information about a specific test execution,
1657        including its ID and associated tests. It first converts the JIRA test execution key to an
1658        internal Xray ID, then uses that ID to fetch the execution details.
1659        Args:
1660            test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123")
1661        Returns:
1662            Optional[Dict]: A dictionary containing test execution details if successful, None if failed.
1663                The dictionary has the following structure:
1664                {
1665                    'id': str,          # The internal Xray ID of the test execution
1666                    'tests': {          # Dictionary mapping test keys to their IDs
1667                        'TEST-124': '10001',
1668                        'TEST-125': '10002',
1669                        ...
1670                    }
1671                }
1672                Returns None in the following cases:
1673                - Test execution ID cannot be found
1674                - GraphQL request fails
1675                - No test execution found with the given ID
1676                - No tests found in the test execution
1677                - Any other error occurs during processing
1678        Example:
1679            >>> client = XrayGraphQL()
1680            >>> execution = client.get_test_execution("TEST-123")
1681            >>> print(execution)
1682            {
1683                'id': '10000',
1684                'tests': {
1685                    'TEST-124': '10001',
1686                    'TEST-125': '10002'
1687                }
1688            }
1689        Note:
1690            - The method is limited to retrieving 99999 tests per test execution
1691            - Test execution must exist in Xray and be accessible with current authentication
1692            - Failed operations are logged with appropriate error or warning messages
1693        """
1694        try:
1695            test_execution_id = self.get_issue_id_from_jira_id(test_execution)
1696            if not test_execution_id:
1697                logger.error(f"Failed to get test execution ID for {test_execution}")
1698                return None
1699            query = """
1700            query GetTestExecution($testExecutionId: String!) {
1701                getTestExecution(issueId: $testExecutionId) {
1702                    issueId
1703                    projectId
1704                    jira(fields: ["key", "summary", "description", "status"])
1705                    tests(limit: 100) {
1706                        total
1707                        start
1708                        limit
1709                        results {
1710                            issueId
1711                            jira(fields: ["key"])
1712                        }
1713                    }
1714                }
1715            }
1716            """
1717            variables = {
1718                "testExecutionId": test_execution_id
1719            }
1720            # Add debug loggerging
1721            logger.debug(f"Getting test execution details for {test_execution_id}")
1722            data = self._make_graphql_request(query, variables)
1723            # jprint(data)
1724            if not data:
1725                logger.error(f"Failed to get test execution details for {test_execution_id}")
1726                return None
1727            test_execution = data.get('getTestExecution',{})
1728            if not test_execution:
1729                logger.warning(f"No test execution found with ID {test_execution_id}")
1730                return None
1731            tests = test_execution.get('tests',{})
1732            if not tests:
1733                logger.warning(f"No tests found for test execution {test_execution_id}")
1734                return None
1735            tests_details = dict()
1736            for test in tests['results']:
1737                tests_details[test['jira']['key']] = test['issueId']
1738            formatted_response = {
1739                'id': test_execution['issueId'],
1740                'tests': tests_details
1741            }
1742            return formatted_response
1743        except Exception as e:
1744            logger.error(f"Error getting test execution details: {str(e)}")
1745            logger.traceback(e)
1746            return None

Retrieve detailed information about a test execution from Xray. This method queries the Xray GraphQL API to fetch information about a specific test execution, including its ID and associated tests. It first converts the JIRA test execution key to an internal Xray ID, then uses that ID to fetch the execution details. Args: test_execution (str): The JIRA issue key of the test execution (e.g., "PROJECT-123") Returns: Optional[Dict]: A dictionary containing test execution details if successful, None if failed. The dictionary has the following structure: { 'id': str, # The internal Xray ID of the test execution 'tests': { # Dictionary mapping test keys to their IDs 'TEST-124': '10001', 'TEST-125': '10002', ... } } Returns None in the following cases: - Test execution ID cannot be found - GraphQL request fails - No test execution found with the given ID - No tests found in the test execution - Any other error occurs during processing Example:

client = XrayGraphQL() execution = client.get_test_execution("TEST-123") print(execution) { 'id': '10000', 'tests': { 'TEST-124': '10001', 'TEST-125': '10002' } } Note: - The method is limited to retrieving 99999 tests per test execution - Test execution must exist in Xray and be accessible with current authentication - Failed operations are logged with appropriate error or warning messages

def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]:
1748    def add_test_execution_to_test_plan(self, test_plan: str, test_execution: str) -> Optional[Dict]:
1749        """
1750        Add a test execution to an existing test plan in Xray.
1751        This method associates a test execution with a test plan using the Xray GraphQL API.
1752        It first converts both the test plan and test execution JIRA keys to their internal
1753        Xray IDs, then creates the association between them.
1754        Args:
1755            test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123")
1756            test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456")
1757        Returns:
1758            Optional[Dict]: A dictionary containing the response data if successful, None if failed.
1759                The dictionary has the following structure:
1760                {
1761                    'addTestExecutionsToTestPlan': {
1762                        'addedTestExecutions': [str],  # List of added test execution IDs
1763                        'warning': str                 # Any warnings from the operation
1764                    }
1765                }
1766                Returns None in the following cases:
1767                - Test plan ID cannot be found
1768                - Test execution ID cannot be found
1769                - GraphQL request fails
1770                - Any other error occurs during processing
1771        Example:
1772            >>> client = XrayGraphQL()
1773            >>> result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456")
1774            >>> print(result)
1775            {
1776                'addTestExecutionsToTestPlan': {
1777                    'addedTestExecutions': ['10001'],
1778                    'warning': None
1779                }
1780            }
1781        Note:
1782            - Both the test plan and test execution must exist in Xray and be accessible
1783            - The method performs two ID lookups before creating the association
1784            - Failed operations are logged as errors with relevant details
1785        """
1786        try:
1787            test_plan_id = self.get_issue_id_from_jira_id(test_plan)
1788            if not test_plan_id:
1789                logger.error(f"Test plan ID is required")
1790                return None
1791            test_exec_id = self.get_issue_id_from_jira_id(test_execution)
1792            if not test_exec_id:
1793                logger.error(f"Test execution ID is required")
1794                return None
1795            query = """
1796            mutation AddTestExecutionToTestPlan($testPlanId: String!, $testExecutionIds: [String!]!) {
1797                addTestExecutionsToTestPlan(issueId: $testPlanId, testExecIssueIds: $testExecutionIds) {
1798                    addedTestExecutions 
1799                    warning
1800                }
1801            }
1802            """
1803            variables = {
1804                "testPlanId": test_plan_id,
1805                "testExecutionIds": [test_exec_id]
1806            }
1807            data = self._make_graphql_request(query, variables)
1808            return data
1809        except Exception as e:
1810            logger.error(f"Error adding test execution to test plan: {str(e)}")
1811            logger.traceback(e)
1812            return None

Add a test execution to an existing test plan in Xray. This method associates a test execution with a test plan using the Xray GraphQL API. It first converts both the test plan and test execution JIRA keys to their internal Xray IDs, then creates the association between them. Args: test_plan (str): The JIRA issue key of the test plan (e.g., "PROJECT-123") test_execution (str): The JIRA issue key of the test execution to add (e.g., "PROJECT-456") Returns: Optional[Dict]: A dictionary containing the response data if successful, None if failed. The dictionary has the following structure: { 'addTestExecutionsToTestPlan': { 'addedTestExecutions': [str], # List of added test execution IDs 'warning': str # Any warnings from the operation } } Returns None in the following cases: - Test plan ID cannot be found - Test execution ID cannot be found - GraphQL request fails - Any other error occurs during processing Example:

client = XrayGraphQL() result = client.add_test_execution_to_test_plan("TEST-123", "TEST-456") print(result) { 'addTestExecutionsToTestPlan': { 'addedTestExecutions': ['10001'], 'warning': None } } Note: - Both the test plan and test execution must exist in Xray and be accessible - The method performs two ID lookups before creating the association - Failed operations are logged as errors with relevant details

def create_test_execution( self, test_issue_keys: List[str], project_key: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None) -> Optional[Dict]:
1814    def create_test_execution(self, 
1815                            test_issue_keys: List[str], 
1816                            project_key: Optional[str] = None, 
1817                            summary: Optional[str] = None, 
1818                            description: Optional[str] = None) -> Optional[Dict]:
1819        """
1820        Create a new test execution in Xray with specified test cases.
1821        This method creates a test execution ticket in JIRA/Xray that includes the specified test cases.
1822        It handles validation of test issue keys, automatically derives project information if not provided,
1823        and creates appropriate default values for summary and description if not specified.
1824        Args:
1825            test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution
1826                (e.g., ["TEST-123", "TEST-124"])
1827            project_key (Optional[str]): The JIRA project key where the test execution should be created.
1828                If not provided, it will be derived from the first test issue key.
1829            summary (Optional[str]): The summary/title for the test execution ticket.
1830                If not provided, a default summary will be generated using the test issue keys.
1831            description (Optional[str]): The description for the test execution ticket.
1832                If not provided, a default description will be generated using the test issue keys.
1833        Returns:
1834            Optional[Dict]: A dictionary containing the created test execution details if successful,
1835                None if the creation fails. The dictionary has the following structure:
1836                {
1837                    'issueId': str,      # The internal Xray ID of the created test execution
1838                    'jira': {
1839                        'key': str       # The JIRA issue key of the created test execution
1840                    }
1841                }
1842        Example:
1843            >>> client = XrayGraphQL()
1844            >>> test_execution = client.create_test_execution(
1845            ...     test_issue_keys=["TEST-123", "TEST-124"],
1846            ...     project_key="TEST",
1847            ...     summary="Sprint 1 Regression Tests"
1848            ... )
1849            >>> print(test_execution)
1850            {'issueId': '10001', 'jira': {'key': 'TEST-125'}}
1851        Note:
1852            - Invalid test issue keys are logged as warnings but don't prevent execution creation
1853            - At least one valid test issue key is required
1854            - The method validates each test issue key before creating the execution
1855            - Project key is automatically derived from the first test issue key if not provided
1856        """
1857        try:
1858            invalid_keys = []
1859            test_issue_ids = []
1860            for key in test_issue_keys:
1861                test_issue_id = self.get_issue_id_from_jira_id(key)
1862                if test_issue_id:
1863                    test_issue_ids.append(test_issue_id)
1864                else:
1865                    invalid_keys.append(key)
1866            if len(test_issue_ids) == 0:
1867                logger.error(f"No valid test issue keys provided: {invalid_keys}")
1868                return None
1869            if len(invalid_keys) > 0:
1870                logger.warning(f"Invalid test issue keys: {invalid_keys}")
1871            if not project_key:
1872                project_key = test_issue_keys[0].split("-")[0]
1873            if not summary:
1874                summary = f"Test Execution for Test Plan {test_issue_keys}"
1875            if not description:
1876                description = f"Test Execution for Test Plan {test_issue_keys}"
1877            mutation = """
1878            mutation CreateTestExecutionForTestPlan(
1879                $testIssueId_list: [String!]!,
1880                $projectKey: String!,
1881                $summary: String!,
1882                $description: String
1883            ) {
1884                createTestExecution(
1885                    testIssueIds: $testIssueId_list,
1886                    jira: {
1887                        fields: {
1888                            project: { key: $projectKey },
1889                            summary: $summary,
1890                            description: $description,
1891                            issuetype: { name: "Test Execution" }
1892                        }
1893                    }
1894                ) {
1895                    testExecution {
1896                        issueId
1897                        jira(fields: ["key"])
1898                    }
1899                    warnings
1900                }
1901            }
1902            """
1903            variables = {
1904                "testIssueId_list": test_issue_ids,
1905                "projectKey": project_key,
1906                "summary": summary,
1907                "description": description
1908            }
1909            data = self._make_graphql_request(mutation, variables)
1910            if not data:
1911                return None
1912            execution_details = data['createTestExecution']['testExecution']
1913            # logger.info(f"Created test execution {execution_details['jira']['key']}")
1914            return execution_details
1915        except Exception as e:
1916            logger.error("Failed to create test execution : {e}")
1917            logger.traceback(e)
1918            return None

Create a new test execution in Xray with specified test cases. This method creates a test execution ticket in JIRA/Xray that includes the specified test cases. It handles validation of test issue keys, automatically derives project information if not provided, and creates appropriate default values for summary and description if not specified. Args: test_issue_keys (List[str]): List of JIRA issue keys for test cases to include in the execution (e.g., ["TEST-123", "TEST-124"]) project_key (Optional[str]): The JIRA project key where the test execution should be created. If not provided, it will be derived from the first test issue key. summary (Optional[str]): The summary/title for the test execution ticket. If not provided, a default summary will be generated using the test issue keys. description (Optional[str]): The description for the test execution ticket. If not provided, a default description will be generated using the test issue keys. Returns: Optional[Dict]: A dictionary containing the created test execution details if successful, None if the creation fails. The dictionary has the following structure: { 'issueId': str, # The internal Xray ID of the created test execution 'jira': { 'key': str # The JIRA issue key of the created test execution } } Example:

client = XrayGraphQL() test_execution = client.create_test_execution( ... test_issue_keys=["TEST-123", "TEST-124"], ... project_key="TEST", ... summary="Sprint 1 Regression Tests" ... ) print(test_execution) {'issueId': '10001', 'jira': {'key': 'TEST-125'}} Note: - Invalid test issue keys are logged as warnings but don't prevent execution creation - At least one valid test issue key is required - The method validates each test issue key before creating the execution - Project key is automatically derived from the first test issue key if not provided

def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]:
1920    def create_test_execution_from_test_plan(self, test_plan: str) -> Optional[Dict[str, Dict[str, str]]]:
1921        """Creates a test execution from a given test plan and associates all tests from the plan with the execution.
1922        This method performs several operations in sequence:
1923        1. Retrieves all tests from the specified test plan
1924        2. Creates a new test execution with those tests
1925        3. Associates the new test execution with the original test plan
1926        4. Creates test runs for each test in the execution
1927        Parameters
1928        ----------
1929        test_plan : str
1930            The JIRA issue key of the test plan (e.g., "PROJECT-123")
1931        Returns
1932        -------
1933        Optional[Dict[str, Dict[str, str]]]
1934            A dictionary mapping test case keys to their execution details, or None if the operation fails.
1935            The dictionary structure is::
1936                {
1937                    "TEST-123": {                    # Test case JIRA key
1938                        "test_run_id": "12345",      # Unique ID for this test run
1939                        "test_execution_key": "TEST-456",  # JIRA key of the created test execution
1940                        "test_plan_key": "TEST-789"  # Original test plan JIRA key
1941                    },
1942                    "TEST-124": {
1943                        ...
1944                    }
1945                }
1946            Returns None in the following cases:
1947            * Test plan parameter is empty or invalid
1948            * No tests found in the test plan
1949            * Test execution creation fails
1950            * API request fails
1951        Examples
1952        --------
1953        >>> client = XrayGraphQL()
1954        >>> result = client.create_test_execution_from_test_plan("TEST-123")
1955        >>> print(result)
1956        {
1957            "TEST-124": {
1958                "test_run_id": "5f7c3",
1959                "test_execution_key": "TEST-456",
1960                "test_plan_key": "TEST-123"
1961            },
1962            "TEST-125": {
1963                "test_run_id": "5f7c4",
1964                "test_execution_key": "TEST-456",
1965                "test_plan_key": "TEST-123"
1966            }
1967        }
1968        Notes
1969        -----
1970        - The test plan must exist and be accessible in Xray
1971        - All tests in the test plan must be valid and accessible
1972        - The method automatically generates a summary and description for the test execution
1973        - The created test execution is automatically linked back to the original test plan
1974        """
1975        try:
1976            if not test_plan:
1977                logger.error("Test plan is required [ jira key]")
1978                return None
1979            project_key = test_plan.split("-")[0]
1980            summary = f"Test Execution for Test Plan {test_plan}"
1981            retDict = dict()
1982            #Get tests from test plan
1983            tests = self.get_tests_from_test_plan(test_plan)
1984            retDict["tests"] = tests
1985            testIssueId_list = list(tests.values())
1986            # logger.info(f"Tests: {tests}")
1987            if not testIssueId_list:
1988                logger.error(f"No tests found for {test_plan}")
1989                return None
1990            description = f"Test Execution for {len(tests)} Test cases"
1991            # GraphQL mutation to create test execution
1992            query = """
1993                mutation CreateTestExecutionForTestPlan(
1994                    $testIssueId_list: [String!]!,
1995                    $projectKey: String!,
1996                    $summary: String!,
1997                    $description: String
1998                ) {
1999                    createTestExecution(
2000                        testIssueIds: $testIssueId_list,
2001                        jira: {
2002                            fields: {
2003                                project: { key: $projectKey },
2004                                summary: $summary,
2005                                description: $description,
2006                                issuetype: { name: "Test Execution" }
2007                            }
2008                        }
2009                    ) {
2010                        testExecution {
2011                            issueId
2012                            jira(fields: ["key"])
2013                            testRuns(limit: 100) {
2014                                results {
2015                                    id
2016                                    test {
2017                                        issueId
2018                                        jira(fields: ["key"])
2019                                    }
2020                                }
2021                            }
2022                        }
2023                        warnings
2024                    }
2025                }
2026            """
2027            variables = {
2028                "testIssueId_list": testIssueId_list,
2029                "projectKey": project_key,
2030                "summary": summary,
2031                "description": description or f"Test execution for total of {len(testIssueId_list)} test cases"
2032            }
2033            data = self._make_graphql_request(query, variables)
2034            if not data:
2035                return None
2036            test_exec_key = data['createTestExecution']['testExecution']['jira']['key']
2037            test_exec_id = data['createTestExecution']['testExecution']['issueId']
2038            #Add Test execution to test plan
2039            test_exec_dict= self.add_test_execution_to_test_plan(test_plan, test_exec_key)
2040            #Get test runs for test execution
2041            test_runs = data['createTestExecution']['testExecution']['testRuns']['results']
2042            test_run_dict = dict()
2043            for test_run in test_runs:
2044                test_run_dict[test_run['test']['jira']['key']] = dict()
2045                test_run_dict[test_run['test']['jira']['key']]['test_run_id'] = test_run['id']
2046                # test_run_dict[test_run['test']['jira']['key']]['test_issue_id'] = test_run['test']['issueId']
2047                test_run_dict[test_run['test']['jira']['key']]['test_execution_key'] = test_exec_key
2048                # test_run_dict[test_run['test']['jira']['key']]['test_execution_id'] = test_exec_id
2049                test_run_dict[test_run['test']['jira']['key']]['test_plan_key'] = test_plan
2050            return test_run_dict
2051        except requests.exceptions.RequestException as e:
2052            logger.error(f"Error creating test execution: {e}")
2053            logger.traceback(e)
2054        return None

Creates a test execution from a given test plan and associates all tests from the plan with the execution. This method performs several operations in sequence:

  1. Retrieves all tests from the specified test plan
  2. Creates a new test execution with those tests
  3. Associates the new test execution with the original test plan
  4. 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
def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool:
2056    def update_test_run_status(self, test_run_id: str, test_run_status: str) -> bool:
2057        """
2058        Update the status of a specific test run in Xray using the GraphQL API.
2059        This method allows updating the execution status of a test run identified by its ID.
2060        The status can be changed to reflect the current state of the test execution
2061        (e.g., "PASS", "FAIL", "TODO", etc.).
2062        Args:
2063            test_run_id (str): The unique identifier of the test run to update.
2064                This is the internal Xray ID for the test run, not the Jira issue key.
2065            test_run_status (str): The new status to set for the test run.
2066                Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc.
2067        Returns:
2068            bool: True if the status update was successful, False otherwise.
2069                Returns None if an error occurs during the API request.
2070        Example:
2071            >>> client = XrayGraphQL()
2072            >>> test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY")
2073            >>> success = client.update_test_run_status(test_run_id, "PASS")
2074            >>> print(success)
2075            True
2076        Note:
2077            - The test run ID must be valid and accessible with current authentication
2078            - The status value should be one of the valid status values configured in your Xray instance
2079            - Failed updates are logged as errors with details about the failure
2080        Raises:
2081            Exception: If there is an error making the GraphQL request or processing the response.
2082                The exception is caught and logged, and the method returns None.
2083        """
2084        try:
2085            query = """
2086            mutation UpdateTestRunStatus($testRunId: String!, $status: String!) {
2087                updateTestRunStatus(
2088                    id: $testRunId, 
2089                    status: $status 
2090                ) 
2091            }                       
2092            """
2093            variables = {
2094                "testRunId": test_run_id,
2095                "status": test_run_status
2096            }
2097            data = self._make_graphql_request(query, variables)
2098            if not data:
2099                logger.error(f"Failed to get test run status for test {data}")
2100                return None
2101            # logger.info(f"Test run status updated: {data}")
2102            return data['updateTestRunStatus']
2103        except Exception as e:
2104            logger.error(f"Error updating test run status: {str(e)}")
2105            logger.traceback(e)
2106            return None

Update the status of a specific test run in Xray using the GraphQL API. This method allows updating the execution status of a test run identified by its ID. The status can be changed to reflect the current state of the test execution (e.g., "PASS", "FAIL", "TODO", etc.). Args: test_run_id (str): The unique identifier of the test run to update. This is the internal Xray ID for the test run, not the Jira issue key. test_run_status (str): The new status to set for the test run. Common values include: "PASS", "FAIL", "TODO", "EXECUTING", etc. Returns: bool: True if the status update was successful, False otherwise. Returns None if an error occurs during the API request. Example:

client = XrayGraphQL() test_run_id = client.get_test_run_status("TEST-CASE-KEY", "TEST-EXEC-KEY") success = client.update_test_run_status(test_run_id, "PASS") print(success) True Note: - The test run ID must be valid and accessible with current authentication - The status value should be one of the valid status values configured in your Xray instance - Failed updates are logged as errors with details about the failure Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.

def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool:
2108    def update_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool:
2109        """
2110        Update the comment of a specific test run in Xray using the GraphQL API.
2111        This method allows adding or updating the comment associated with a test run
2112        identified by its ID. The comment can provide additional context, test results,
2113        or any other relevant information about the test execution.
2114        Args:
2115            test_run_id (str): The unique identifier of the test run to update.
2116                This is the internal Xray ID for the test run, not the Jira issue key.
2117            test_run_comment (str): The new comment text to set for the test run.
2118                This will replace any existing comment on the test run.
2119        Returns:
2120            bool: True if the comment update was successful, False otherwise.
2121                Returns None if an error occurs during the API request.
2122        Example:
2123            >>> client = XrayGraphQL()
2124            >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32"
2125            >>> success = client.update_test_run_comment(
2126            ...     test_run_id,
2127            ...     "Test passed with performance within expected range"
2128            ... )
2129            >>> print(success)
2130            True
2131        Note:
2132            - The test run ID must be valid and accessible with current authentication
2133            - The comment can include any text content, including newlines and special characters
2134            - Failed updates are logged as errors with details about the failure
2135            - This method will overwrite any existing comment on the test run
2136        Raises:
2137            Exception: If there is an error making the GraphQL request or processing the response.
2138                The exception is caught and logged, and the method returns None.
2139        """
2140        try:
2141            query = """
2142            mutation UpdateTestRunStatus($testRunId: String!, $comment: String!) {
2143                updateTestRunComment(
2144                    id: $testRunId, 
2145                    comment: $comment 
2146                ) 
2147            }                       
2148            """
2149            variables = {
2150                "testRunId": test_run_id,
2151                "comment": test_run_comment
2152            }
2153            data = self._make_graphql_request(query, variables)
2154            if not data:
2155                logger.error(f"Failed to get test run comment for test {data}")
2156                return None
2157            # jprint(data)
2158            return data['updateTestRunComment']
2159        except Exception as e:
2160            logger.error(f"Error updating test run comment: {str(e)}")
2161            logger.traceback(e)
2162            return None

Update the comment of a specific test run in Xray using the GraphQL API. This method allows adding or updating the comment associated with a test run identified by its ID. The comment can provide additional context, test results, or any other relevant information about the test execution. Args: test_run_id (str): The unique identifier of the test run to update. This is the internal Xray ID for the test run, not the Jira issue key. test_run_comment (str): The new comment text to set for the test run. This will replace any existing comment on the test run. Returns: bool: True if the comment update was successful, False otherwise. Returns None if an error occurs during the API request. Example:

client = XrayGraphQL() test_run_id = "67fcfd4b9e6d63d4c1d57b32" success = client.update_test_run_comment( ... test_run_id, ... "Test passed with performance within expected range" ... ) print(success) True Note: - The test run ID must be valid and accessible with current authentication - The comment can include any text content, including newlines and special characters - Failed updates are logged as errors with details about the failure - This method will overwrite any existing comment on the test run Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.

def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool:
2164    def add_evidence_to_test_run(self, test_run_id: str, evidence_path: str) -> bool:
2165        """Add evidence (attachments) to a test run in Xray.
2166        This method allows attaching files as evidence to a specific test run. The file is
2167        read, converted to base64, and uploaded to Xray with appropriate MIME type detection.
2168        Parameters
2169        ----------
2170        test_run_id : str
2171            The unique identifier of the test run to add evidence to
2172        evidence_path : str
2173            The local file system path to the evidence file to be attached
2174        Returns
2175        -------
2176        bool
2177            True if the evidence was successfully added, None if the operation failed.
2178            Returns None in the following cases:
2179            - Test run ID is not provided
2180            - Evidence path is not provided
2181            - Evidence file does not exist
2182            - GraphQL request fails
2183            - Any other error occurs during processing
2184        Examples
2185        --------
2186        >>> client = XrayGraphQL()
2187        >>> success = client.add_evidence_to_test_run(
2188        ...     test_run_id="10001",
2189        ...     evidence_path="/path/to/screenshot.png"
2190        ... )
2191        >>> print(success)
2192        True
2193        Notes
2194        -----
2195        - The evidence file must exist and be accessible
2196        - The file is automatically converted to base64 for upload
2197        - MIME type is automatically detected, defaults to "text/plain" if detection fails
2198        - The method supports various file types (images, documents, logs, etc.)
2199        - Failed operations are logged with appropriate error messages
2200        """
2201        try:
2202            if not test_run_id:
2203                logger.error("Test run ID is required")
2204                return None
2205            if not evidence_path:
2206                logger.error("Evidence path is required")
2207                return None
2208            if not os.path.exists(evidence_path):
2209                logger.error(f"Evidence file not found: {evidence_path}")
2210                return None
2211            #if file exists then read the file in base64
2212            evidence_base64 = None
2213            mime_type = None
2214            filename = os.path.basename(evidence_path)
2215            with open(evidence_path, "rb") as file:
2216                evidence_data = file.read()
2217                evidence_base64 = base64.b64encode(evidence_data).decode('utf-8')
2218                mime_type = mimetypes.guess_type(evidence_path)[0]
2219                logger.info(f"For loop -- Mime type: {mime_type}")
2220                if not mime_type:
2221                    mime_type = "text/plain"
2222            query = """
2223            mutation AddEvidenceToTestRun($testRunId: String!, $filename: String!, $mimeType: String!, $evidenceBase64: String!) {
2224                addEvidenceToTestRun(
2225                    id: $testRunId, 
2226                    evidence: [
2227                        {
2228                            filename : $filename,
2229                            mimeType : $mimeType,
2230                            data : $evidenceBase64
2231                        }
2232                    ]
2233                ) {
2234                    addedEvidence
2235                    warnings
2236                }
2237            }
2238            """
2239            variables = {
2240                "testRunId": test_run_id,
2241                "filename": filename,
2242                "mimeType": mime_type,
2243                "evidenceBase64": evidence_base64
2244            }
2245            data = self._make_graphql_request(query, variables) 
2246            if not data:
2247                logger.error(f"Failed to add evidence to test run: {data}")
2248                return None
2249            return data['addEvidenceToTestRun'] 
2250        except Exception as e:
2251            logger.error(f"Error adding evidence to test run: {str(e)}")
2252            logger.traceback(e)
2253            return None

Add evidence (attachments) to a test run in Xray. This method allows attaching files as evidence to a specific test run. The file is read, converted to base64, and uploaded to Xray with appropriate MIME type detection.

Parameters

test_run_id : str The unique identifier of the test run to add evidence to evidence_path : str The local file system path to the evidence file to be attached

Returns

bool True if the evidence was successfully added, None if the operation failed. Returns None in the following cases: - Test run ID is not provided - Evidence path is not provided - Evidence file does not exist - GraphQL request fails - Any other error occurs during processing

Examples

>>> client = XrayGraphQL()
>>> success = client.add_evidence_to_test_run(
...     test_run_id="10001",
...     evidence_path="/path/to/screenshot.png"
... )
>>> print(success)
True
<h2 id="notes">Notes</h2>
  • The evidence file must exist and be accessible
  • The file is automatically converted to base64 for upload
  • MIME type is automatically detected, defaults to "text/plain" if detection fails
  • The method supports various file types (images, documents, logs, etc.)
  • Failed operations are logged with appropriate error messages
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    def create_defect_from_test_run(self, test_run_id: str, project_key: str, parent_issue_key: str, defect_summary: str, defect_description: str) -> Optional[Dict]:
2256        """Create a defect from a test run and link it to the test run in Xray.
2257        This method performs two main operations:
2258        1. Creates a new defect in JIRA with the specified summary and description
2259        2. Links the created defect to the specified test run in Xray
2260        Parameters
2261        ----------
2262        test_run_id : str
2263            The ID of the test run to create defect from
2264        project_key : str
2265            The JIRA project key where the defect should be created.
2266            If not provided, defaults to "EAGVAL"
2267        parent_issue_key : str
2268            The JIRA key of the parent issue to link the defect to
2269        defect_summary : str
2270            Summary/title of the defect.
2271            If not provided, defaults to "Please provide a summary for the defect"
2272        defect_description : str
2273            Description of the defect.
2274            If not provided, defaults to "Please provide a description for the defect"
2275        Returns
2276        -------
2277        Optional[Dict]
2278            Response data from the GraphQL API if successful, None if failed.
2279            The response includes:
2280            - addedDefects: List of added defects
2281            - warnings: Any warnings from the operation
2282        Examples
2283        --------
2284        >>> client = XrayGraphQL()
2285        >>> result = client.create_defect_from_test_run(
2286        ...     test_run_id="10001",
2287        ...     project_key="PROJ",
2288        ...     parent_issue_key="PROJ-456",
2289        ...     defect_summary="Test failure in login flow",
2290        ...     defect_description="The login button is not responding to clicks"
2291        ... )
2292        >>> print(result)
2293        {
2294            'addedDefects': ['PROJ-123'],
2295            'warnings': []
2296        }
2297        Notes
2298        -----
2299        - The project_key will be split on '-' and only the first part will be used
2300        - The defect will be created with issue type 'Bug'
2301        - The method handles missing parameters with default values
2302        - The parent issue must exist and be accessible to create the defect
2303        """
2304        try:
2305            if not project_key:
2306                project_key = "EAGVAL"
2307            if not defect_summary:
2308                defect_summary = "Please provide a summary for the defect"
2309            if not defect_description:
2310                defect_description = "Please provide a description for the defect"
2311            project_key = project_key.split("-")[0]
2312            # Fix: Correct parameter order for create_issue
2313            defect_key, defect_id = self.create_issue(
2314                project_key=project_key,
2315                parent_issue_key=parent_issue_key,
2316                summary=defect_summary,
2317                description=defect_description,
2318                issue_type='Bug'
2319            )
2320            if not defect_key:
2321                logger.error("Failed to create defect issue")
2322                return None
2323            # Then add the defect to the test run
2324            add_defect_mutation = """
2325            mutation AddDefectsToTestRun(
2326                $testRunId: String!,
2327                $defectKey: String!
2328            ) {
2329                addDefectsToTestRun( id: $testRunId, issues: [$defectKey]) {
2330                    addedDefects
2331                    warnings
2332                }
2333            }
2334            """
2335            variables = {
2336                "testRunId": test_run_id,
2337                "defectKey": defect_key
2338            }
2339            data = None
2340            retry_count = 0
2341            while retry_count < 3:
2342                data = self._make_graphql_request(add_defect_mutation, variables)
2343                if not data:
2344                    logger.error(f"Failed to add defect [{defect_key}] to test run - {test_run_id}.. retrying... {retry_count}")
2345                    retry_count += 1
2346                    time.sleep(1)
2347                else:
2348                    break
2349            return data
2350        except Exception as e:
2351            logger.error(f"Error creating defect from test run: {str(e)}")
2352            logger.traceback(e)
2353            return None

Create a defect from a test run and link it to the test run in Xray. This method performs two main operations:

  1. Creates a new defect in JIRA with the specified summary and description
  2. 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
def get_test_run_comment(self, test_run_id: str) -> Optional[str]:
2355    def get_test_run_comment(self, test_run_id: str) -> Optional[str]:
2356        """
2357        Retrieve the comment of a specific test run from Xray using the GraphQL API.
2358        This method allows retrieving the comment associated with a test run
2359        identified by its ID. The comment can provide additional context, test results,
2360        or any other relevant information about the test execution.
2361        Args:
2362            test_run_id (str): The unique identifier of the test run to retrieve comment from.
2363                This is the internal Xray ID for the test run, not the Jira issue key.
2364        Returns:
2365            Optional[str]: The comment text of the test run if successful, None if:
2366                - The test run ID is not found
2367                - The GraphQL request fails
2368                - No comment exists for the test run
2369                - Any other error occurs during the API request
2370        Example:
2371            >>> client = XrayGraphQL()
2372            >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32"
2373            >>> comment = client.get_test_run_comment(test_run_id)
2374            >>> print(comment)
2375            "Test passed with performance within expected range"
2376        Note:
2377            - The test run ID must be valid and accessible with current authentication
2378            - If no comment exists for the test run, the method will return None
2379            - Failed requests are logged as errors with details about the failure
2380            - The method returns the raw comment text as stored in Xray
2381        Raises:
2382            Exception: If there is an error making the GraphQL request or processing the response.
2383                The exception is caught and logged, and the method returns None.
2384        """
2385        try:
2386            # Try the direct ID approach first
2387            query = """
2388            query GetTestRunComment($testRunId: String!) {
2389                getTestRunById(id: $testRunId) {
2390                    id
2391                    comment
2392                    status {
2393                        name
2394                    }
2395                }
2396            }                       
2397            """
2398            variables = {
2399                "testRunId": test_run_id
2400            }
2401            data = self._make_graphql_request(query, variables)
2402            # jprint(data)
2403            if not data:
2404                logger.error(f"Failed to get test run comment for test run {test_run_id}")
2405                return None
2406            test_run = data.get('getTestRunById', {})
2407            if not test_run:
2408                logger.warning(f"No test run found with ID {test_run_id}")
2409                return None
2410            comment = test_run.get('comment')
2411            return comment
2412        except Exception as e:
2413            logger.error(f"Error getting test run comment: {str(e)}")
2414            logger.traceback(e)
2415            return None

Retrieve the comment of a specific test run from Xray using the GraphQL API. This method allows retrieving the comment associated with a test run identified by its ID. The comment can provide additional context, test results, or any other relevant information about the test execution. Args: test_run_id (str): The unique identifier of the test run to retrieve comment from. This is the internal Xray ID for the test run, not the Jira issue key. Returns: Optional[str]: The comment text of the test run if successful, None if: - The test run ID is not found - The GraphQL request fails - No comment exists for the test run - Any other error occurs during the API request Example:

client = XrayGraphQL() test_run_id = "67fcfd4b9e6d63d4c1d57b32" comment = client.get_test_run_comment(test_run_id) print(comment) "Test passed with performance within expected range" Note: - The test run ID must be valid and accessible with current authentication - If no comment exists for the test run, the method will return None - Failed requests are logged as errors with details about the failure - The method returns the raw comment text as stored in Xray Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.

def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool:
2417    def append_test_run_comment(self, test_run_id: str, test_run_comment: str) -> bool:
2418        """
2419        Append the comment of a specific test run in Xray using the GraphQL API.
2420        This method allows appending the comment associated with a test run
2421        identified by its ID. The comment can provide additional context, test results,
2422        or any other relevant information about the test execution.
2423        Args:
2424            test_run_id (str): The unique identifier of the test run to update.
2425                This is the internal Xray ID for the test run, not the Jira issue key.
2426            test_run_comment (str): The comment text to append to the test run.
2427                This will be added to any existing comment on the test run with proper formatting.
2428        Returns:
2429            bool: True if the comment update was successful, False otherwise.
2430                Returns None if an error occurs during the API request.
2431        Example:
2432            >>> client = XrayGraphQL()
2433            >>> test_run_id = "67fcfd4b9e6d63d4c1d57b32"
2434            >>> success = client.append_test_run_comment(
2435            ...     test_run_id,
2436            ...     "Test passed with performance within expected range"
2437            ... )
2438            >>> print(success)
2439            True
2440        Note:
2441            - The test run ID must be valid and accessible with current authentication
2442            - The comment can include any text content, including newlines and special characters
2443            - Failed updates are logged as errors with details about the failure
2444            - This method will append to existing comments with proper line breaks
2445            - If no existing comment exists, the new comment will be set as the initial comment
2446        Raises:
2447            Exception: If there is an error making the GraphQL request or processing the response.
2448                The exception is caught and logged, and the method returns None.
2449        """
2450        try:
2451            # Get existing comment
2452            existing_comment = self.get_test_run_comment(test_run_id)
2453            # Prepare the combined comment with proper formatting
2454            if existing_comment:
2455                # If there's an existing comment, append with double newline for proper separation
2456                combined_comment = f"{existing_comment}\n{test_run_comment}"
2457            else:
2458                # If no existing comment, use the new comment as is
2459                combined_comment = test_run_comment
2460                logger.debug(f"No existing comment found for test run {test_run_id}, setting initial comment")
2461            query = """
2462            mutation UpdateTestRunComment($testRunId: String!, $comment: String!) {
2463                updateTestRunComment(
2464                    id: $testRunId, 
2465                    comment: $comment 
2466                ) 
2467            }                       
2468            """
2469            variables = {
2470                "testRunId": test_run_id,
2471                "comment": combined_comment
2472            }
2473            data = self._make_graphql_request(query, variables)
2474            if not data:
2475                logger.error(f"Failed to update test run comment for test run {test_run_id}")
2476                return None
2477            return data['updateTestRunComment']
2478        except Exception as e:
2479            logger.error(f"Error updating test run comment: {str(e)}")
2480            logger.traceback(e)
2481            return None

Append the comment of a specific test run in Xray using the GraphQL API. This method allows appending the comment associated with a test run identified by its ID. The comment can provide additional context, test results, or any other relevant information about the test execution. Args: test_run_id (str): The unique identifier of the test run to update. This is the internal Xray ID for the test run, not the Jira issue key. test_run_comment (str): The comment text to append to the test run. This will be added to any existing comment on the test run with proper formatting. Returns: bool: True if the comment update was successful, False otherwise. Returns None if an error occurs during the API request. Example:

client = XrayGraphQL() test_run_id = "67fcfd4b9e6d63d4c1d57b32" success = client.append_test_run_comment( ... test_run_id, ... "Test passed with performance within expected range" ... ) print(success) True Note: - The test run ID must be valid and accessible with current authentication - The comment can include any text content, including newlines and special characters - Failed updates are logged as errors with details about the failure - This method will append to existing comments with proper line breaks - If no existing comment exists, the new comment will be set as the initial comment Raises: Exception: If there is an error making the GraphQL request or processing the response. The exception is caught and logged, and the method returns None.

def download_attachment_by_extension(self, jira_key: str, file_extension: str) -> Optional[Dict]:
2483    def download_attachment_by_extension(self, jira_key: str, file_extension: str) -> Optional[Dict]:
2484        """
2485        Download JIRA attachments by file extension.
2486        
2487        Retrieves all attachments from a JIRA issue that match the specified file extension
2488        and downloads their content. This method searches through all attachments on the
2489        issue and filters by filename ending with the provided extension.
2490        
2491        Args:
2492            jira_key (str): The JIRA issue key (e.g., 'PROJ-123')
2493            file_extension (str): The file extension to search for (e.g., '.json', '.txt')
2494                                 Should include the dot prefix
2495                                 
2496        Returns:
2497            Optional[Dict]: A list of dictionaries containing attachment data, where each
2498                           dictionary has filename as key and attachment content as value.
2499                           Returns None if:
2500                           - Issue cannot be retrieved
2501                           - No attachments found with the specified extension
2502                           - Error occurs during download
2503                           
2504        Example:
2505            >>> client = XrayGraphQL()
2506            >>> attachments = client.download_attachment_by_extension('PROJ-123', '.json')
2507            >>> # Returns: [{'document.json': {'content': b'...', 'mime_type': 'application/json'}}]
2508            
2509        Raises:
2510            Exception: Logged and handled internally, returns None on any error
2511        """
2512        try:
2513            response = self.make_jira_request(jira_key, 'GET')
2514            
2515            if not response or 'fields' not in response:
2516                logger.error(f"Error: Could not retrieve issue {jira_key}")
2517                return None
2518            
2519            # Find attachment by filename
2520            attachments = response.get('fields', {}).get('attachment', [])
2521            target_attachment = [att for att in attachments if att.get('filename').endswith(file_extension)]
2522            if not target_attachment:
2523                logger.error(f"No attachment found for {jira_key} with extension {file_extension}")
2524                return None
2525            
2526            combined_attachment = []
2527            fileDetails = dict()
2528            for attachment in target_attachment:
2529                attachment_id = attachment.get('id')
2530                mime_type = attachment.get('mimeType', '')
2531                fileDetails[attachment.get('filename')] = self.download_jira_attachment_by_id(attachment_id, mime_type)
2532                combined_attachment.append(fileDetails)
2533            
2534            return combined_attachment
2535        except Exception as e:
2536            logger.error(f"Error downloading JIRA attachment: {str(e)}")
2537            logger.traceback(e)
2538            return None

Download JIRA attachments by file extension.

Retrieves all attachments from a JIRA issue that match the specified file extension and downloads their content. This method searches through all attachments on the issue and filters by filename ending with the provided extension.

Args: jira_key (str): The JIRA issue key (e.g., 'PROJ-123') file_extension (str): The file extension to search for (e.g., '.json', '.txt') Should include the dot prefix

Returns: Optional[Dict]: A list of dictionaries containing attachment data, where each dictionary has filename as key and attachment content as value. Returns None if: - Issue cannot be retrieved - No attachments found with the specified extension - Error occurs during download

Example:

client = XrayGraphQL() attachments = client.download_attachment_by_extension('PROJ-123', '.json')

Returns: [{'document.json': {'content': b'...', 'mime_type': 'application/json'}}]

Raises: Exception: Logged and handled internally, returns None on any error

def download_attachment_by_name(self, jira_key: str, file_name: str) -> Optional[Dict]:
2540    def download_attachment_by_name(self, jira_key: str, file_name: str) -> Optional[Dict]:
2541        """
2542        Download JIRA attachments by filename prefix.
2543        
2544        Retrieves all attachments from a JIRA issue whose filenames start with the
2545        specified name (case-insensitive). This method searches through all attachments
2546        on the issue and filters by filename starting with the provided name.
2547        
2548        Args:
2549            jira_key (str): The JIRA issue key (e.g., 'PROJ-123')
2550            file_name (str): The filename prefix to search for (e.g., 'report', 'test')
2551                            Case-insensitive matching is performed
2552                            
2553        Returns:
2554            Optional[Dict]: A list of dictionaries containing attachment data, where each
2555                           dictionary has filename as key and attachment content as value.
2556                           Returns None if:
2557                           - Issue cannot be retrieved
2558                           - No attachments found with the specified filename prefix
2559                           - Error occurs during download
2560                           
2561        Example:
2562            >>> client = XrayGraphQL()
2563            >>> attachments = client.download_attachment_by_name('PROJ-123', 'report')
2564            >>> # Returns: [{'report_v1.json': {'content': b'...', 'mime_type': 'application/json'}},
2565            >>> #          {'report_v2.json': {'content': b'...', 'mime_type': 'application/json'}}]
2566            
2567        Raises:
2568            Exception: Logged and handled internally, returns None on any error
2569        """
2570        try:
2571            response = self.make_jira_request(jira_key, 'GET')
2572            
2573            if not response or 'fields' not in response:
2574                logger.error(f"Error: Could not retrieve issue {jira_key}")
2575                return None
2576            
2577            # Find attachment by filename
2578            attachments = response.get('fields', {}).get('attachment', [])
2579            target_attachment = [att for att in attachments if att.get('filename').lower().startswith(file_name.lower())]
2580            if not target_attachment:
2581                logger.error(f"No attachment found for {jira_key} with extension {file_name}")
2582                return None
2583            
2584            combined_attachment = []
2585            fileDetails = dict()
2586            for attachment in target_attachment:
2587                attachment_id = attachment.get('id')
2588                mime_type = attachment.get('mimeType', '')
2589                fileDetails[attachment.get('filename')] = self.download_jira_attachment_by_id(attachment_id, mime_type)
2590                combined_attachment.append(fileDetails)
2591            
2592            return combined_attachment
2593        except Exception as e:
2594            logger.error(f"Error downloading JIRA attachment: {str(e)}")
2595            logger.traceback(e)
2596            return None

Download JIRA attachments by filename prefix.

Retrieves all attachments from a JIRA issue whose filenames start with the specified name (case-insensitive). This method searches through all attachments on the issue and filters by filename starting with the provided name.

Args: jira_key (str): The JIRA issue key (e.g., 'PROJ-123') file_name (str): The filename prefix to search for (e.g., 'report', 'test') Case-insensitive matching is performed

Returns: Optional[Dict]: A list of dictionaries containing attachment data, where each dictionary has filename as key and attachment content as value. Returns None if: - Issue cannot be retrieved - No attachments found with the specified filename prefix - Error occurs during download

Example:

client = XrayGraphQL() attachments = client.download_attachment_by_name('PROJ-123', 'report')

Returns: [{'report_v1.json': {'content': b'...', 'mime_type': 'application/json'}},

{'report_v2.json': {'content': b'...', 'mime_type': 'application/json'}}]

Raises: Exception: Logged and handled internally, returns None on any error

def download_xray_attachment_by_id(self, attachment_id: str, mime_type: str) -> Optional[Dict]:
2598    def download_xray_attachment_by_id(self, attachment_id: str, mime_type: str) -> Optional[Dict]:
2599        '''
2600        Download an Xray attachment by its ID using Xray API authentication.
2601        
2602        This method downloads attachments from Xray Cloud using the proper Xray API
2603        endpoint and Bearer token authentication, unlike JIRA attachments which use
2604        Basic authentication.
2605        
2606        Args:
2607            attachment_id (str): The Xray attachment ID
2608            mime_type (str): The MIME type of the attachment
2609            
2610        Returns:
2611            Optional[Dict]: A dictionary containing the attachment content and metadata,
2612                           or None if the download fails
2613        '''
2614        try:
2615            # Use the Xray API endpoint for attachments
2616            CONTENT_URL = f"{self.xray_base_url}/api/v2/attachments/{attachment_id}"
2617            if not CONTENT_URL:
2618                logger.error(f"No content URL found for attachment '{attachment_id}'")
2619                return None
2620            
2621            # Use Xray Bearer token authentication
2622            headers = {
2623                "Authorization": f"Bearer {self.token}",
2624                "Content-Type": "application/json"
2625            }
2626            
2627            download_response = requests.get(CONTENT_URL, headers=headers)
2628            download_response.raise_for_status()
2629            content = download_response.content
2630            
2631            # Process content based on type
2632            result = {
2633                'content': content,
2634                'mime_type': mime_type,
2635                'text_content': None,
2636                'json_content': None
2637            }
2638            
2639            # Handle text-based files
2640            if mime_type.startswith(('text/', 'application/json', 'application/xml', 'json')):
2641                try:
2642                    text_content = content.decode('utf-8')
2643                    result['text_content'] = text_content
2644                    
2645                    # Try to parse as JSON
2646                    if 'json' in mime_type:
2647                        try:
2648                            result['json_content'] = json.loads(text_content)
2649                        except json.JSONDecodeError:
2650                            pass
2651                except UnicodeDecodeError:
2652                    logger.error(f"Warning: Could not decode text content for {attachment_id}")
2653                    logger.traceback(e)
2654            
2655            return result
2656        except Exception as e:
2657            logger.error(f"Error downloading Xray attachment: {str(e)}")
2658            logger.traceback(e)
2659            return None

Download an Xray attachment by its ID using Xray API authentication.

This method downloads attachments from Xray Cloud using the proper Xray API endpoint and Bearer token authentication, unlike JIRA attachments which use Basic authentication.

Args: attachment_id (str): The Xray attachment ID mime_type (str): The MIME type of the attachment

Returns: Optional[Dict]: A dictionary containing the attachment content and metadata, or None if the download fails

def generate_json_from_sentence(self, sentence, template_schema, debug=False):
2662    def generate_json_from_sentence(self, sentence, template_schema, debug=False):
2663        """Extract information using template schema and spaCy components"""
2664        def _ensure_spacy_model():
2665            """Ensure spaCy model is available, download if needed"""
2666            try:
2667                nlp = spacy.load("en_core_web_md")
2668                return nlp
2669            except OSError:
2670                import subprocess
2671                import sys
2672                logger.info("Downloading required spaCy model...")
2673                try:
2674                    subprocess.check_call([
2675                        sys.executable, "-m", "spacy", "download", "en_core_web_md"
2676                    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2677                    return spacy.load("en_core_web_md")
2678                except subprocess.CalledProcessError:
2679                    raise RuntimeError(
2680                        "Failed to download spaCy model. Please run manually: "
2681                        "python -m spacy download en_core_web_md"
2682                    )
2683
2684        def analyze_model_components(nlp):
2685            """Analyze the loaded model's components and capabilities"""
2686            logger.info("=== Model Analysis ===")
2687            logger.info(f"Model: {nlp.meta['name']}")
2688            logger.info(f"Version: {nlp.meta['version']}")
2689            logger.info(f"Pipeline: {nlp.pipe_names}")
2690            logger.info(f"Components: {list(nlp.pipeline)}")
2691            
2692        def parse_template_pattern(pattern_value):
2693            """Parse template pattern to extract structure and placeholders"""
2694            if not isinstance(pattern_value, str):
2695                return None
2696            
2697            # Extract placeholders like <string> from the pattern
2698            placeholders = re.findall(r'<(\w+)>', pattern_value)
2699            
2700            # Create a regex pattern by replacing placeholders with capture groups
2701            regex_pattern = pattern_value
2702            for placeholder in placeholders:
2703                # Replace <string> with a regex that captures word characters
2704                regex_pattern = regex_pattern.replace(f'<{placeholder}>', r'(\w+)')
2705            
2706            return {
2707                'original': pattern_value,
2708                'placeholders': placeholders,
2709                'regex_pattern': regex_pattern,
2710                'regex': re.compile(regex_pattern, re.IGNORECASE)
2711            }
2712
2713        def match_pattern_from_template(pattern_value, doc, debug=False):
2714            """Match a pattern based on template value and return the matched text"""
2715            
2716            if isinstance(pattern_value, list):
2717                # Handle list of exact values (for environment, region)
2718                for value in pattern_value:
2719                    for token in doc:
2720                        if token.text.lower() == value.lower():
2721                            if debug:
2722                                logger.info(f"✓ Matched list value '{value}' -> {token.text}")
2723                            return token.text
2724                return None
2725            
2726            elif isinstance(pattern_value, str):
2727                # Parse the template pattern dynamically
2728                pattern_info = parse_template_pattern(pattern_value)
2729                if not pattern_info:
2730                    return None
2731                
2732                if debug:
2733                    logger.info(f"Parsed pattern: {pattern_info['original']}")
2734                    logger.info(f"Regex pattern: {pattern_info['regex_pattern']}")
2735                
2736                # Look for tokens that match the pattern
2737                for token in doc:
2738                    if pattern_info['regex'].match(token.text):
2739                        if debug:
2740                            logger.info(f"✓ Matched template pattern '{pattern_value}' -> {token.text}")
2741                        return token.text
2742                
2743                return None
2744
2745        try:
2746            if not sentence:
2747                logger.error("Sentence is required")
2748                return None
2749            if not template_schema or not isinstance(template_schema, dict):
2750                logger.error("Template schema is required")
2751                return None
2752            if not debug:
2753                debug = False
2754            
2755            # Fix: Initialize result with all template schema keys
2756            result = {key: None for key in template_schema.keys()}
2757            result["sentences"] = []  # Initialize as empty list instead of string
2758            
2759        except Exception as e:
2760            logger.error(f"Error generating JSON from sentence: {str(e)}")
2761            logger.traceback(e)
2762            return None
2763        
2764        # Only add debug fields if debug mode is enabled
2765        if debug:
2766            result.update({
2767                "tokens_analysis": [],
2768                "entities": [],
2769                "dependencies": []
2770            })
2771        
2772        try:
2773            nlp = _ensure_spacy_model()
2774            if not nlp:
2775                logger.error("Failed to load spaCy model")
2776                return None
2777            if debug:
2778                # Analyze model capabilities
2779                analyze_model_components(nlp)
2780            doc = nlp(sentence)
2781
2782            # Fix: Ensure sentences list exists before appending
2783            if "sentences" not in result:
2784                result["sentences"] = []
2785                
2786            for sent in doc.sents:
2787                result["sentences"].append(sent.text.strip())
2788            
2789            # 2. Tokenize and analyze each token with spaCy components (only in debug mode)
2790            if debug:
2791                for token in doc:
2792                    token_info = {
2793                        "text": token.text,
2794                        "lemma": token.lemma_,
2795                        "pos": token.pos_,
2796                        "tag": token.tag_,
2797                        "dep": token.dep_,
2798                        "head": token.head.text,
2799                        "is_alpha": token.is_alpha,
2800                        "is_digit": token.is_digit,
2801                        "is_punct": token.is_punct,
2802                        "shape": token.shape_,
2803                        "is_stop": token.is_stop
2804                    }
2805                    result["tokens_analysis"].append(token_info)
2806            
2807            # 3. Dynamic pattern matching based on template schema values
2808            for pattern_key, pattern_value in template_schema.items():
2809                if not result[pattern_key]:  # Only search if not already found
2810                    matched_value = match_pattern_from_template(pattern_value, doc, debug)
2811                    if matched_value:
2812                        result[pattern_key] = matched_value
2813            
2814            # 4. Use NER (Named Entity Recognition) component (only in debug mode)
2815            if debug:
2816                for ent in doc.ents:
2817                    entity_info = {
2818                        "text": ent.text,
2819                        "label": ent.label_,
2820                        "start": ent.start_char,
2821                        "end": ent.end_char
2822                    }
2823                    result["entities"].append(entity_info)
2824                    logger.info(f"✓ NER Entity: {ent.text} - {ent.label_}")
2825            
2826            # 5. Use dependency parsing to find relationships (only in debug mode)
2827            if debug:
2828                for token in doc:
2829                    dep_info = {
2830                        "token": token.text,
2831                        "head": token.head.text,
2832                        "dependency": token.dep_,
2833                        "children": [child.text for child in token.children]
2834                    }
2835                    result["dependencies"].append(dep_info)
2836            
2837            # 6. Use lemmatizer to find action verbs and their objects
2838            for token in doc:
2839                if token.lemma_ in ["verify", "validate", "check", "test"]:
2840                    if debug:
2841                        logger.info(f"✓ Found action verb: {token.text} (lemma: {token.lemma_})")
2842                    # Find what's being verified/validated
2843                    for child in token.children:
2844                        if child.dep_ in ["dobj", "pobj"]:
2845                            if debug:
2846                                logger.info(f"✓ Found verification target: {child.text}")
2847            
2848            return result
2849        except Exception as e:
2850            logger.error(f"Error extracting information from document: {str(e)}")
2851            logger.traceback(e)
2852            return None

Extract information using template schema and spaCy components