/usr/lib/python3/dist-packages/jira/resilientsession.py is in python3-jira 1.0.10-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import json
import logging
try: # Python 2.7+
from logging import NullHandler
except ImportError:
class NullHandler(logging.Handler):
def emit(self, record):
pass
import random
from requests.exceptions import ConnectionError
from requests import Session
import time
from jira.exceptions import JIRAError
logging.getLogger('jira').addHandler(NullHandler())
def raise_on_error(r, verb='???', **kwargs):
request = kwargs.get('request', None)
# headers = kwargs.get('headers', None)
if r is None:
raise JIRAError(None, **kwargs)
if r.status_code >= 400:
error = ''
if r.status_code == 403 and "x-authentication-denied-reason" in r.headers:
error = r.headers["x-authentication-denied-reason"]
elif r.text:
try:
response = json.loads(r.text)
if 'message' in response:
# JIRA 5.1 errors
error = response['message']
elif 'errorMessages' in response and len(response['errorMessages']) > 0:
# JIRA 5.0.x error messages sometimes come wrapped in this array
# Sometimes this is present but empty
errorMessages = response['errorMessages']
if isinstance(errorMessages, (list, tuple)):
error = errorMessages[0]
else:
error = errorMessages
elif 'errors' in response and len(response['errors']) > 0:
# JIRA 6.x error messages are found in this array.
error_list = response['errors'].values()
error = ", ".join(error_list)
else:
error = r.text
except ValueError:
error = r.text
raise JIRAError(
r.status_code, error, r.url, request=request, response=r, **kwargs)
# for debugging weird errors on CI
if r.status_code not in [200, 201, 202, 204]:
raise JIRAError(r.status_code, request=request, response=r, **kwargs)
# testing for the WTH bug exposed on
# https://answers.atlassian.com/questions/11457054/answers/11975162
if r.status_code == 200 and len(r.content) == 0 \
and 'X-Seraph-LoginReason' in r.headers \
and 'AUTHENTICATED_FAILED' in r.headers['X-Seraph-LoginReason']:
pass
class ResilientSession(Session):
"""This class is supposed to retry requests that do return temporary errors.
At this moment it supports: 502, 503, 504
"""
def __init__(self, timeout=None):
self.max_retries = 3
self.timeout = timeout
super(ResilientSession, self).__init__()
# Indicate our preference for JSON to avoid https://bitbucket.org/bspeakmon/jira-python/issue/46 and https://jira.atlassian.com/browse/JRA-38551
self.headers.update({"Accept": "application/json,*.*;q=0.9"})
def __recoverable(self, response, url, request, counter=1):
msg = response
if isinstance(response, ConnectionError):
logging.warning("Got ConnectionError [%s] errno:%s on %s %s\n%s\%s" % (
response, response.errno, request, url, vars(response), response.__dict__))
if hasattr(response, 'status_code'):
if response.status_code in [502, 503, 504, 401]:
# 401 UNAUTHORIZED still randomly returned by Atlassian Cloud as of 2017-01-16
msg = "%s %s" % (response.status_code, response.reason)
elif not (response.status_code == 200 and
len(response.content) == 0 and
'X-Seraph-LoginReason' in response.headers and
'AUTHENTICATED_FAILED' in response.headers['X-Seraph-LoginReason']):
return False
else:
msg = "Atlassian's bug https://jira.atlassian.com/browse/JRA-41559"
# Exponential backoff with full jitter.
delay = min(60, 10 * 2 ** counter) * random.random()
logging.warning("Got recoverable error from %s %s, will retry [%s/%s] in %ss. Err: %s" % (
request, url, counter, self.max_retries, delay, msg))
time.sleep(delay)
return True
def __verb(self, verb, url, retry_data=None, **kwargs):
d = self.headers.copy()
d.update(kwargs.get('headers', {}))
kwargs['headers'] = d
# if we pass a dictionary as the 'data' we assume we want to send json
# data
data = kwargs.get('data', {})
if isinstance(data, dict):
data = json.dumps(data)
retry_number = 0
while retry_number <= self.max_retries:
response = None
exception = None
try:
method = getattr(super(ResilientSession, self), verb.lower())
response = method(url, timeout=self.timeout, **kwargs)
if response.status_code == 200:
return response
except ConnectionError as e:
logging.warning(
"%s while doing %s %s [%s]" % (e, verb.upper(), url, kwargs))
exception = e
retry_number += 1
if retry_number <= self.max_retries:
response_or_exception = response if response is not None else exception
if self.__recoverable(response_or_exception, url, verb.upper(), retry_number):
if retry_data:
# if data is a stream, we cannot just read again from it,
# retry_data() will give us a new stream with the data
kwargs['data'] = retry_data()
continue
else:
break
if exception is not None:
raise exception
raise_on_error(response, verb=verb, **kwargs)
return response
def get(self, url, **kwargs):
return self.__verb('GET', url, **kwargs)
def post(self, url, **kwargs):
return self.__verb('POST', url, **kwargs)
def put(self, url, **kwargs):
return self.__verb('PUT', url, **kwargs)
def delete(self, url, **kwargs):
return self.__verb('DELETE', url, **kwargs)
def head(self, url, **kwargs):
return self.__verb('HEAD', url, **kwargs)
def patch(self, url, **kwargs):
return self.__verb('PATCH', url, **kwargs)
def options(self, url, **kwargs):
return self.__verb('OPTIONS', url, **kwargs)
|