# Source code for jira.resilientsession

    import json
    import logging
    import random
    import time
    from typing import Callable, Optional, Union, cast

    from requests import Response, Session
    from requests.exceptions import ConnectionError

    from jira.exceptions import JIRAError

    logging.getLogger("jira").addHandler(logging.NullHandler())

    def raise_on_error(r: Optional[Response], verb="???", **kwargs):
        """Handle errors from a Jira Request

        Args:
            r (Optional[Response]): Response from Jira request
            verb (Optional[str]): Request type, e.g. POST. Defaults to "???".

        Raises:
            JIRAError: If Response is None
            JIRAError: for unhandled 400 status codes.
            JIRAError: for unhandled 200 status codes.
        """
        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
                    # Catching only 'errors' that are dict. See https://github.com/pycontribs/jira/issues/350
                    elif (
                        "errors" in response
                        and len(response["errors"]) > 0
                        and isinstance(response["errors"], dict)
                    ):
                        # 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(
                error,
                status_code=r.status_code,
                url=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(
                status_code=r.status_code, request=request, response=r, **kwargs
            )
        # testing for the 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.max_retry_delay = 60
            self.timeout = timeout
            super().__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: Optional[Union[ConnectionError, Response]],
            url: str,
            request,
            counter: int = 1,
        ):
            msg = str(response)
            if isinstance(response, ConnectionError):
                logging.warning(
                    f"Got ConnectionError [{response}] errno:{response.errno} on {request} {url}\n{vars(response)}\n{response.__dict__}"
                )
            if isinstance(response, Response):
                if response.status_code in [502, 503, 504, 401]:
                    # 401 UNAUTHORIZED still randomly returned by Atlassian Cloud as of 2017-01-16
                    msg = f"{response.status_code} {response.reason}"
                    # 2019-07-25: Disabled recovery for codes above^
                    return False
                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(self.max_retry_delay, 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)
            )
            if isinstance(response, Response):
                logging.debug("response.headers: %s", response.headers)
                logging.debug("response.body: %s", response.content)
            time.sleep(delay)
            return True

        def __verb(
            self, verb: str, url: str, retry_data: Callable = None, **kwargs
        ) -> Response:

            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
            exception = None
            response = None
            while retry_number <= self.max_retries:
                response = None
                exception = None
                try:
                    method = getattr(super(), verb.lower())
                    response = method(url, timeout=self.timeout, **kwargs)
                    if response.status_code >= 200 and response.status_code <= 299:
                        return response
                except ConnectionError as e:
                    logging.warning(f"{e} while doing {verb.upper()} {url}")

                    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)
            # after raise_on_error, only Response objects are allowed through
            response = cast(Response, response)  # tell mypy only Response-like are here
            return response

        def get(self, url: Union[str, bytes], **kwargs) -> Response:  # type: ignore
            return self.__verb("GET", str(url), **kwargs)

        def post(self, url: Union[str, bytes], data=None, json=None, **kwargs) -> Response:  # type: ignore
            return self.__verb("POST", str(url), data=data, json=json, **kwargs)

        def put(self, url: Union[str, bytes], data=None, **kwargs) -> Response:  # type: ignore
            return self.__verb("PUT", str(url), data=data, **kwargs)

        def delete(self, url: Union[str, bytes], **kwargs) -> Response:  # type: ignore
            return self.__verb("DELETE", str(url), **kwargs)

        def head(self, url: Union[str, bytes], **kwargs) -> Response:  # type: ignore
            return self.__verb("HEAD", str(url), **kwargs)

        def patch(self, url: Union[str, bytes], data=None, **kwargs) -> Response:  # type: ignore
            return self.__verb("PATCH", str(url), data=data, **kwargs)

        def options(self, url: Union[str, bytes], **kwargs) -> Response:  # type: ignore
            return self.__verb("OPTIONS", str(url), **kwargs)