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

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

Inherits

JiraHandler Base class providing JIRA client functionality and issue management

Attributes

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

Methods

Authentication & Setup

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

Issue ID Management

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

Test Plan Operations

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

Test Set Operations

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

Test Execution Operations

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

Test Run Operations

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

Evidence & Defect Management

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

Examples

>>> client = XrayGraphQL()
>>> test_plan_tests = client.get_tests_from_test_plan("TEST-123")
>>> print(test_plan_tests)
{'TEST-124': '10001', 'TEST-125': '10002'}
>>> test_execution = client.create_test_execution_from_test_plan("TEST-123")
>>> print(test_execution)
{
    'TEST-124': {
        'test_run_id': '5f7c3',
        'test_execution_key': 'TEST-456',
        'test_plan_key': 'TEST-123'
    }
}
>>> # Update test run status
>>> success = client.update_test_run_status("test_run_id", "PASS")
>>> print(success)
True
>>> # Add evidence to test run
>>> evidence_added = client.add_evidence_to_test_run("test_run_id", "/path/to/screenshot.png")
>>> print(evidence_added)
True
<h2 id="notes">Notes</h2>
  • Requires valid Xray Cloud credentials (client_id and client_secret)
  • Uses GraphQL for all API interactions
  • Implements automatic token refresh
  • Handles rate limiting and retries
  • All methods include comprehensive error handling and logging
  • Returns None for failed operations instead of raising exceptions
  • Supports various file types for evidence attachments
  • Integrates with JIRA for defect creation and issue management
  • Provides both synchronous and asynchronous operation patterns
  • Includes retry logic for transient failures
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                                    result
1153                                    attachments {
1154                                    id
1155                                    filename
1156                                    storedInJira
1157                                    downloadLink
1158                                    }
1159                                }
1160                                
1161                            }
1162                        }
1163                    }
1164                    """
1165            variables = {
1166                "limit": 10,
1167                "jql": f"project = '{parse_project}' AND key = '{issue_key}'",
1168                "jiraFields": jira_fields
1169            }
1170            data = self._make_graphql_request(query, variables)
1171            if not data:
1172                logger.error(f"Failed to get issue ID for {issue_key}")
1173                return None
1174            for issue in data[function_name]['results']:
1175                if str(issue['jira']['key']).lower() == issue_key.lower():
1176                    return issue  # This now includes all metadata
1177            return None
1178        except Exception as e:
1179            logger.error(f"Failed to get issue ID for {issue_key}")
1180            logger.traceback(e)
1181            return None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Parameters

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

Returns

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

Examples

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

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

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

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

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

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

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

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

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

Download JIRA attachments by file extension.

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

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

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

Example:

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

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

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

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

Download JIRA attachments by filename prefix.

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

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

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

Example:

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

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

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

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

def generate_json_from_sentence(self, sentence, template_schema, debug=False):
2597    def generate_json_from_sentence(self, sentence, template_schema, debug=False):
2598        """Extract information using template schema and spaCy components"""
2599        def _ensure_spacy_model():
2600            """Ensure spaCy model is available, download if needed"""
2601            try:
2602                nlp = spacy.load("en_core_web_md")
2603                return nlp
2604            except OSError:
2605                import subprocess
2606                import sys
2607                logger.info("Downloading required spaCy model...")
2608                try:
2609                    subprocess.check_call([
2610                        sys.executable, "-m", "spacy", "download", "en_core_web_md"
2611                    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2612                    return spacy.load("en_core_web_md")
2613                except subprocess.CalledProcessError:
2614                    raise RuntimeError(
2615                        "Failed to download spaCy model. Please run manually: "
2616                        "python -m spacy download en_core_web_md"
2617                    )
2618
2619        def analyze_model_components(nlp):
2620            """Analyze the loaded model's components and capabilities"""
2621            logger.info("=== Model Analysis ===")
2622            logger.info(f"Model: {nlp.meta['name']}")
2623            logger.info(f"Version: {nlp.meta['version']}")
2624            logger.info(f"Pipeline: {nlp.pipe_names}")
2625            logger.info(f"Components: {list(nlp.pipeline)}")
2626            
2627        def parse_template_pattern(pattern_value):
2628            """Parse template pattern to extract structure and placeholders"""
2629            if not isinstance(pattern_value, str):
2630                return None
2631            
2632            # Extract placeholders like <string> from the pattern
2633            placeholders = re.findall(r'<(\w+)>', pattern_value)
2634            
2635            # Create a regex pattern by replacing placeholders with capture groups
2636            regex_pattern = pattern_value
2637            for placeholder in placeholders:
2638                # Replace <string> with a regex that captures word characters
2639                regex_pattern = regex_pattern.replace(f'<{placeholder}>', r'(\w+)')
2640            
2641            return {
2642                'original': pattern_value,
2643                'placeholders': placeholders,
2644                'regex_pattern': regex_pattern,
2645                'regex': re.compile(regex_pattern, re.IGNORECASE)
2646            }
2647
2648        def match_pattern_from_template(pattern_value, doc, debug=False):
2649            """Match a pattern based on template value and return the matched text"""
2650            
2651            if isinstance(pattern_value, list):
2652                # Handle list of exact values (for environment, region)
2653                for value in pattern_value:
2654                    for token in doc:
2655                        if token.text.lower() == value.lower():
2656                            if debug:
2657                                logger.info(f"✓ Matched list value '{value}' -> {token.text}")
2658                            return token.text
2659                return None
2660            
2661            elif isinstance(pattern_value, str):
2662                # Parse the template pattern dynamically
2663                pattern_info = parse_template_pattern(pattern_value)
2664                if not pattern_info:
2665                    return None
2666                
2667                if debug:
2668                    logger.info(f"Parsed pattern: {pattern_info['original']}")
2669                    logger.info(f"Regex pattern: {pattern_info['regex_pattern']}")
2670                
2671                # Look for tokens that match the pattern
2672                for token in doc:
2673                    if pattern_info['regex'].match(token.text):
2674                        if debug:
2675                            logger.info(f"✓ Matched template pattern '{pattern_value}' -> {token.text}")
2676                        return token.text
2677                
2678                return None
2679
2680        try:
2681            if not sentence:
2682                logger.error("Sentence is required")
2683                return None
2684            if not template_schema or not isinstance(template_schema, dict):
2685                logger.error("Template schema is required")
2686                return None
2687            if not debug:
2688                debug = False
2689            
2690            # Fix: Initialize result with all template schema keys
2691            result = {key: None for key in template_schema.keys()}
2692            result["sentences"] = []  # Initialize as empty list instead of string
2693            
2694        except Exception as e:
2695            logger.error(f"Error generating JSON from sentence: {str(e)}")
2696            logger.traceback(e)
2697            return None
2698        
2699        # Only add debug fields if debug mode is enabled
2700        if debug:
2701            result.update({
2702                "tokens_analysis": [],
2703                "entities": [],
2704                "dependencies": []
2705            })
2706        
2707        try:
2708            nlp = _ensure_spacy_model()
2709            if not nlp:
2710                logger.error("Failed to load spaCy model")
2711                return None
2712            if debug:
2713                # Analyze model capabilities
2714                analyze_model_components(nlp)
2715            doc = nlp(sentence)
2716
2717            # Fix: Ensure sentences list exists before appending
2718            if "sentences" not in result:
2719                result["sentences"] = []
2720                
2721            for sent in doc.sents:
2722                result["sentences"].append(sent.text.strip())
2723            
2724            # 2. Tokenize and analyze each token with spaCy components (only in debug mode)
2725            if debug:
2726                for token in doc:
2727                    token_info = {
2728                        "text": token.text,
2729                        "lemma": token.lemma_,
2730                        "pos": token.pos_,
2731                        "tag": token.tag_,
2732                        "dep": token.dep_,
2733                        "head": token.head.text,
2734                        "is_alpha": token.is_alpha,
2735                        "is_digit": token.is_digit,
2736                        "is_punct": token.is_punct,
2737                        "shape": token.shape_,
2738                        "is_stop": token.is_stop
2739                    }
2740                    result["tokens_analysis"].append(token_info)
2741            
2742            # 3. Dynamic pattern matching based on template schema values
2743            for pattern_key, pattern_value in template_schema.items():
2744                if not result[pattern_key]:  # Only search if not already found
2745                    matched_value = match_pattern_from_template(pattern_value, doc, debug)
2746                    if matched_value:
2747                        result[pattern_key] = matched_value
2748            
2749            # 4. Use NER (Named Entity Recognition) component (only in debug mode)
2750            if debug:
2751                for ent in doc.ents:
2752                    entity_info = {
2753                        "text": ent.text,
2754                        "label": ent.label_,
2755                        "start": ent.start_char,
2756                        "end": ent.end_char
2757                    }
2758                    result["entities"].append(entity_info)
2759                    logger.info(f"✓ NER Entity: {ent.text} - {ent.label_}")
2760            
2761            # 5. Use dependency parsing to find relationships (only in debug mode)
2762            if debug:
2763                for token in doc:
2764                    dep_info = {
2765                        "token": token.text,
2766                        "head": token.head.text,
2767                        "dependency": token.dep_,
2768                        "children": [child.text for child in token.children]
2769                    }
2770                    result["dependencies"].append(dep_info)
2771            
2772            # 6. Use lemmatizer to find action verbs and their objects
2773            for token in doc:
2774                if token.lemma_ in ["verify", "validate", "check", "test"]:
2775                    if debug:
2776                        logger.info(f"✓ Found action verb: {token.text} (lemma: {token.lemma_})")
2777                    # Find what's being verified/validated
2778                    for child in token.children:
2779                        if child.dep_ in ["dobj", "pobj"]:
2780                            if debug:
2781                                logger.info(f"✓ Found verification target: {child.text}")
2782            
2783            return result
2784        except Exception as e:
2785            logger.error(f"Error extracting information from document: {str(e)}")
2786            logger.traceback(e)
2787            return None

Extract information using template schema and spaCy components