Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Enhancing Reliability: A Python Script for Retrying Jira Cloud API Requests

Hi Atlassian Community!

I'm now sharing a simple Python script designed to help you efficiently handle retries for requests to the Jira Cloud REST API. This is particularly useful when dealing with intermittent issues. This script attempts to resend the request if it fails due to temporary issues, ensuring more reliable interactions with the Jira Cloud API.

The Solution

Using REST API's, such as such as "Get Issue", this script retries failed requests due to network errors or rate limiting. It uses an exponential backoff strategy to wait before retrying, which helps manage API rate limits and network reliability.

The Script

import requests
import time
from requests.exceptions import RequestException
def get_issue(issue_key, domain, email, api_token, max_retries=3):
    url = f"https://{domain}/rest/api/2/issue/{issue_key}"
    headers = {
        "Accept": "application/json"
    }
    auth = (email, api_token)
    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers, auth=auth)
            if response.status_code == 200:
                return response.json()
            else:
                print(f"Attempt {attempt + 1}: Failed to retrieve issue {issue_key}. Status code: {response.status_code}")
                if response.status_code == 429:  # Rate limiting error
                    retry_after = int(response.headers.get("Retry-After", 1))
                    time.sleep(retry_after)
                else:
                    time.sleep(2 ** attempt)  # Exponential backoff
        except RequestException as e:
            print(f"Attempt {attempt + 1}: Request failed with exception: {e}")
            time.sleep(2 ** attempt)  # Exponential backoff
    return None
# Example usage domain = "your-domain.atlassian.net" email = "your-email@example.com" api_token = "your-api-token" issue_key = "PROJ-001" issue_data = get_issue(issue_key, domain, email, api_token) if issue_data: print("Issue data retrieved successfully:", issue_data) else: print("Failed to retrieve issue data after multiple attempts.")

Preparation

  1. Ensure the user running the script has "Browse Projects" and "View Issues" permissions in the relevant Jira projects.
  2. Install the requests library: pip install requests
  3. Prepare your Jira Cloud domain (your-domain.atlassian.net), email address, and API token.
  4. Update the domain, email, api_token, and issue_key variables in the script with your own details.

Disclaimer

This script is provided "as is" and "as available" without warranties and is not officially supported or endorsed by Atlassian. Use at your own risk. Jira API changes may impact functionality.

Cheers!

2 comments

Andrew Culver
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
June 13, 2025
Like Barışcan Elkama likes this
David Foote
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
June 13, 2025

I found it very helpful to wrap all my Jira calls in a class that has the retry logic built in. I am not a Python expert, but I found referencing Atlassian's own API wrapper to be very helpful https://github.com/atlassian-api/atlassian-python-api 

(When I first looked at it a few years ago, I decided to write my own stuff, not sure that was a great decision or not!)

Here is an example version of a class that interacts with Jira:

jira.py

import requests
import logging
from urllib3.util.retry import Retry
from requests.auth import HTTPBasicAuth

class Jira:

    def __init__(self, baseURL, username, password):
        self.baseURL = baseURL
        self.username = username
        self.password = password
        self.http = self.get_requests_session()

    def get_requests_session(self):
        retry_strategy = Retry(
        total=3,
            backoff_factor=1,
            status_forcelist=[429, 500, 502, 503, 504],
            respect_retry_after_header=True,
            allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"]
        )
        adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
        http = requests.Session()
        http.auth = HTTPBasicAuth(self.username, self.password)
        http.mount("https://", adapter)       

        return http

    def get(self, url, payload=None, headers = None):
        try:
            r = self.http.get(url, params = payload, headers = headers)
            r.raise_for_status()
        except requests.exceptions.RequestException as e:
            logging.error(e)
            return None
        return r.json()  
 

    def get_issue(self, issueKey):
        url = f"{self.baseURL}/rest/api/2/issue/{issueKey}?expand=names&properties=*all"
        return self.get(url)


And here is an example file that uses that class:

 

import jira
import logging
from pprint import pprint

logger = logging.getLogger()
AEJ = jira.Jira("myURL.atlassian.net","myusername", "mypassword")

if __name__  == "__main__":
    pprint(AEJ.get_issue("AB-1234"))

 

(In our actual scripts, I have a helper class that sets up logging and grabs URL, username and pass from a config file, I just simplified it here)

But this set up of separating out the actual GET/POST, whatever calls helps simplify the actual API calls, so I don't have to keep replicating some of the basic request logic.

Putting it all in a class also helps when I need to chain multiple calls together; for example, sometimes it's useful to have a simple call to get the total results of an JQL search without having to parse it out of a normal search call, so you can do stuff like this in a class

 

def search_issues(self, jql, fieldlist=['created'], maxResults = 50, nextPageToken = None):
url = f"{self.baseURL}/rest/api/3/search/jql"


data_payload = {
"maxResults": maxResults,
"jql": jql,
"fields": fieldlist,
"nextPageToken": nextPageToken,

}

return self.post(url, data_payload)

def total_results_from_search(self, jql):
p = self.search_issues(jql, ['created'], 1, 0)

if 'total' in p:
return p['total']
else:
return -1

 So in a script I can just call total_results_from_search() to get counts; very helpful when collecting stats or just verifying how much data you might be looking at if it's over 1000 and the UI won't actually give you the total number.

 

 

Like # people like this

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events