# Jira in Python

Create, query and update Jira tickets from Python services.

Your services work with Jira tickets in two ways:

- **Jira cloud connector:** a high-level client through `self.cloud.jira` - use it for JQL queries and other common operations
- **Direct REST calls:** for operations the connector does not cover, such as Service Desk endpoints

## Create a connection {#create-a-connection}

To create a connection, go to **Cloud** > **Atlassian** > **Jira** in the Dashboard, click **Create a new connection** and fill in the form:

1. **Name:** My Jira
2. **Address:** your Jira instance's address, e.g. `https://example.atlassian.net`
3. **Username:** the account the connection works as, e.g. `api-user@example.com`
4. Click **OK**

[Video: New Jira connection](https://zatosource-production.b-cdn.net/docs/gfx/cloud/jira-create.webm?v=1790440801)

Dashboard menu: Cloud > Atlassian > Jira

## Query tickets {#query-tickets}

Use JQL (Jira Query Language) to search for issues. The connector returns structured data you can process in Python.

```python
# -*- coding: utf-8 -*-

from zato.server.service import Service

class GetOpenTickets(Service):
    name = 'my.jira.get-open-tickets'

    def handle(self):

        # Name of your Jira connection defined in Dashboard
        conn_name = 'My Jira'

        # Fields to retrieve
        fields = ['key', 'summary', 'status', 'assignee', 'created']

        # Get a reference to the Jira connection
        jira = self.cloud.jira[conn_name]

        # Obtain a client
        with jira.conn.client() as client:

            # Build and execute JQL query
            query = 'status="Open" AND project="MYPROJECT"'
            result = client.jql(jql=query, fields=fields)

            # Process results
            issues = result['issues']
            for issue in issues:
                key = issue['key']
                summary = issue['fields']['summary']
                self.logger.info(f'Found issue: {key} - {summary}')

        self.response.payload = {'count': len(issues)}
```

## Transition issues {#transition-issues}

To move an issue through its workflow, post the transition's ID to the issue's transitions endpoint:

```python
# -*- coding: utf-8 -*-

import os
from json import dumps
import requests
from zato.server.service import Service

class TransitionIssue(Service):
    name = 'my.jira.transition-issue'

    input = 'ticket_id', 'transition_id'

    def handle(self):

        # Jira credentials
        base_url = 'https://yourcompany.atlassian.net'
        username = 'your.email@company.com'
        password = os.environ['JIRA_API_TOKEN']
        auth = (username, password)

        # Build the transition request
        url = f'{base_url}/rest/api/2/issue/{self.request.input.ticket_id}/transitions'
        data = {'transition': {'id': self.request.input.transition_id}}
        headers = {'Content-Type': 'application/json'}

        # Execute the transition
        response = requests.post(url, data=dumps(data), auth=auth, headers=headers)

        if not str(response.status_code).startswith('2'):
            raise Exception(f'Transition failed: {response.status_code} {response.text}')

        self.response.payload = {'status': 'transitioned'}
```

## Add comments {#add-comments}

To comment on an issue, post the text to the issue's comment endpoint - the `public` flag decides whether Service Desk customers see the comment or only agents do:

```python
# -*- coding: utf-8 -*-

import os
from json import dumps
import requests
from zato.server.service import Service

class AddComment(Service):
    name = 'my.jira.add-comment'

    input = 'ticket_id', 'comment', '-public'

    def handle(self):

        # Jira credentials
        base_url = 'https://yourcompany.atlassian.net'
        username = 'your.email@company.com'
        password = os.environ['JIRA_API_TOKEN']
        auth = (username, password)

        # For Service Desk, use the service desk API
        url = f'{base_url}/rest/servicedeskapi/request/{self.request.input.ticket_id}/comment'

        # An unset optional flag means an internal comment
        public = self.request.input.public
        if public == '':
            public = False

        data = {
            'body': self.request.input.comment,
            'public': public
        }

        headers = {'Content-Type': 'application/json'}

        response = requests.post(url, data=dumps(data), auth=auth, headers=headers)

        if not str(response.status_code).startswith('2'):
            raise Exception(f'Comment failed: {response.status_code} {response.text}')

        self.response.payload = {'status': 'commented'}
```

## Download attachments {#download-attachments}

To download an attachment, request its content endpoint - Jira replies with a redirect whose Location header points to the file's actual URL:

```python
# -*- coding: utf-8 -*-

import os
import requests
from zato.server.service import Service

class GetAttachment(Service):
    name = 'my.jira.get-attachment'

    input = 'attachment_id'

    def handle(self):

        # Jira credentials
        base_url = 'https://yourcompany.atlassian.net'
        username = 'your.email@company.com'
        password = os.environ['JIRA_API_TOKEN']
        auth = (username, password)

        # Get attachment metadata and download URL
        url = f'{base_url}/rest/api/3/attachment/content/{self.request.input.attachment_id}'

        # Don't follow redirects - we want the Location header
        response = requests.get(url, auth=auth, allow_redirects=False)

        # The actual file is at the redirect location
        download_url = response.headers['Location']

        # Now download the file
        file_response = requests.get(download_url)

        self.response.payload = {
            'content': file_response.content,
            'size': len(file_response.content)
        }
```

## Use the Jira cloud connector {#use-the-jira-cloud-connector}

The connector's client covers common operations directly - single issues by key, JQL searches and the rest of Jira's API:

```python
# -*- coding: utf-8 -*-

from zato.server.service import Service

class JiraConnectorExample(Service):
    name = 'my.jira.connector-example'

    def handle(self):

        # Get the Jira connection
        jira = self.cloud.jira['My Jira']

        with jira.conn.client() as client:

            # Get a single issue
            issue = client.get_issue('PROJ-123', fields=['summary', 'status'])

            # Search with JQL
            results = client.jql(
                jql='project=PROJ AND status=Open',
                fields=['key', 'summary']
            )

            # The client provides direct access to Jira's API
            self.response.payload = {
                'issue': issue,
                'search_count': len(results['issues'])
            }
```

## See also {#see-also}

- [REST outgoing connections](https://zato.io/docs/dev/rest/outconns.html) - Call Jira REST endpoints the connector does not cover
- [Slack](https://zato.io/docs/dev/examples/slack.html) - Notify people when tickets change
- [Scheduler](https://zato.io/docs/dev/examples/scheduler.html) - Poll Jira for changes on an interval

## Learn more {#learn-more}

- [Development documentation](https://zato.io/docs/dev/) - The complete reference for writing services
- [Requests and responses](https://zato.io/docs/dev/request-response/) - What a service receives, what it returns and how both are structured
- [Integration examples](https://zato.io/docs/dev/examples/) - Ready-made code for the systems you are likely to connect to
- [IDE and debugging](https://zato.io/docs/dev/ide/) - Write services in the Dashboard or in your own editor
- [Data models](https://zato.io/docs/dev/model/) - Declare inputs and outputs and have them validated for you
- [In-depth API tutorial](https://zato.io/tutorials/main/01.html) - The full platform tutorial, from installation to production patterns
