Repository: PyGithub/PyGithub

Issue:
Adding communications Retry functionality into requests via urllib3 retry object.
This PR is related to #757 and #378

This is an attempt to add a communications retry feature into PyGithub that can be user defined at the top level.

This works directly with requests and uses the low level urllib3 Retry object (already used by requests) to implement the functionality.  It is backwards compatible with the current codeset.

The user can set the retry parameter in the top level `github(<login>, retry = X)` call.  Where X can be a simple `int` to define the number of retrys or it can be an `urllib3.util.retry.Retry(...)` object.  This allows the user very fine grained control over the retry system as defined by urllib3.

The structure for the Retry object can be setup like this example:
```
num_retries=3
backoff_factor=5
status_forcelist=(500, 502, 504)
retry_data = urllib3.util.retry.Retry(
                                      total=num_retries,
                                      read=num_retries,
                                      connect=num_retries,
                                      backoff_factor=backoff_factor,
                                      status_forcelist=status_forcelist
                                     )
gserver = github(<login or token>, retry = retry_data)
```
The urllib3 Retry object has many parameters, that allows the user much flexibility to configure retry's to their heart's content.

The only issue I have at the moment, is that I am completely baffled on how to add tests for this feature given the current testing infrastructure.  Any suggestions would be appreciated.

CC: @mfonville @jrouquie @sfdye 

EDIT: The code for this PR is now stale, and is being moved forward via PR #1002.  However, the discussion is still relevant.  


Discussion:

Resolving patch:
diff --git a/github/MainClass.py b/github/MainClass.py
--- a/github/MainClass.py
+++ b/github/MainClass.py
@@ -55,6 +55,7 @@
 import sys
 import requests
 import jwt
+import urllib3
 
 from Requester import Requester
 import AuthenticatedUser
@@ -95,7 +96,7 @@ class Github(object):
     This is the main class you instantiate to access the Github API v3. Optional parameters allow different authentication methods.
     """
 
-    def __init__(self, login_or_token=None, password=None, jwt=None, base_url=DEFAULT_BASE_URL, timeout=DEFAULT_TIMEOUT, client_id=None, client_secret=None, user_agent='PyGithub/Python', per_page=DEFAULT_PER_PAGE, api_preview=False, verify=True):
+    def __init__(self, login_or_token=None, password=None, jwt=None, base_url=DEFAULT_BASE_URL, timeout=DEFAULT_TIMEOUT, client_id=None, client_secret=None, user_agent='PyGithub/Python', per_page=DEFAULT_PER_PAGE, api_preview=False, verify=True, retry=None):
         """
         :param login_or_token: string
         :param password: string
@@ -106,6 +107,7 @@ def __init__(self, login_or_token=None, password=None, jwt=None, base_url=DEFAUL
         :param user_agent: string
         :param per_page: int
         :param verify: boolean or string
+        :param retry: int or urllib3.util.retry.Retry object
         """
 
         assert login_or_token is None or isinstance(login_or_token, (str, unicode)), login_or_token
@@ -117,7 +119,8 @@ def __init__(self, login_or_token=None, password=None, jwt=None, base_url=DEFAUL
         assert client_secret is None or isinstance(client_secret, (str, unicode)), client_secret
         assert user_agent is None or isinstance(user_agent, (str, unicode)), user_agent
         assert isinstance(api_preview, (bool))
-        self.__requester = Requester(login_or_token, password, jwt, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify)
+        assert retry is None or isinstance(retry, (int)) or isinstance(retry, (urllib3.util.Retry))
+        self.__requester = Requester(login_or_token, password, jwt, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify, retry)
 
     def __get_FIX_REPO_GET_GIT_REF(self):
         """
diff --git a/github/Requester.py b/github/Requester.py
--- a/github/Requester.py
+++ b/github/Requester.py
@@ -89,13 +89,18 @@ def read(self):
 
 class HTTPSRequestsConnectionClass(object):
     # mimic the httplib connection object
-    def __init__(self, host, port=None, strict=False, timeout=None, **kwargs):
+    def __init__(self, host, port=None, strict=False, timeout=None, retry=None, **kwargs):
         self.port = port if port else 443
         self.host = host
         self.protocol = "https"
         self.timeout = timeout
         self.verify = kwargs.get("verify", True)
         self.session = requests.Session()
+        # Code to support retries
+        if retry:
+            self.retry = retry
+            self.adapter = requests.adapters.HTTPAdapter(max_retries=self.retry)
+            self.session.mount('https://', self.adapter)
 
     def request(self, verb, url, input, headers):
         self.verb = verb
@@ -115,13 +120,18 @@ def close(self):
 
 class HTTPRequestsConnectionClass(object):
     # mimic the httplib connection object
-    def __init__(self, host, port=None, strict=False, timeout=None, **kwargs):
+    def __init__(self, host, port=None, strict=False, timeout=None, retry=None, **kwargs):
         self.port = port if port else 80
         self.host = host
         self.protocol = "http"
         self.timeout = timeout
         self.verify = kwargs.get("verify", True)
         self.session = requests.Session()
+        # Code to support retries
+        if retry:
+            self.retry = retry
+            self.adapter = requests.adapters.HTTPAdapter(max_retries=self.retry)
+            self.session.mount('http://', self.adapter)
 
     def request(self, verb, url, input, headers):
         self.verb = verb
@@ -214,7 +224,7 @@ def _initializeDebugFeature(self):
 
     #############################################################
 
-    def __init__(self, login_or_token, password, jwt, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify):
+    def __init__(self, login_or_token, password, jwt, base_url, timeout, client_id, client_secret, user_agent, per_page, api_preview, verify, retry):
         self._initializeDebugFeature()
 
         if password is not None:
@@ -237,6 +247,7 @@ def __init__(self, login_or_token, password, jwt, base_url, timeout, client_id,
         self.__port = o.port
         self.__prefix = o.path
         self.__timeout = timeout
+        self.__retry = retry  # NOTE: retry can be either int or an urllib3 Retry object
         self.__scheme = o.scheme
         if o.scheme == "https":
             self.__connectionClass = self.__httpsConnectionClass
@@ -283,9 +294,9 @@ def __customConnection(self, url):
                (o.port and o.port != self.__port) or \
                (o.scheme != self.__scheme and not (o.scheme == "https" and self.__scheme == "http")):  # issue80
                 if o.scheme == 'http':
-                    cnx = self.__httpConnectionClass(o.hostname, o.port)
+                    cnx = self.__httpConnectionClass(o.hostname, o.port, retry = self.__retry)
                 elif o.scheme == 'https':
-                    cnx = self.__httpsConnectionClass(o.hostname, o.port)
+                    cnx = self.__httpsConnectionClass(o.hostname, o.port, retry = self.__retry)
         return cnx
 
     def __createException(self, status, headers, output):
@@ -459,7 +470,7 @@ def __createConnection(self):
         if self.__persist and self.__connection is not None:
             return self.__connection
 
-        self.__connection = self.__connectionClass(self.__hostname, self.__port, **kwds)
+        self.__connection = self.__connectionClass(self.__hostname, self.__port, retry = self.__retry, **kwds)
 
         return self.__connection
 
@@ -475,4 +486,4 @@ def __log(self, verb, url, requestHeaders, input, status, responseHeaders, outpu
                     requestHeaders["Authorization"] = "Bearer (jwt removed)"
                 else:  # pragma no cover (Cannot happen, but could if we add an authentication method => be prepared)
                     requestHeaders["Authorization"] = "(unknown auth removed)"  # pragma no cover (Cannot happen, but could if we add an authentication method => be prepared)
-            logger.debug("%s %s://%s%s %s %s ==> %i %s %s", str(verb), self.__scheme, self.__hostname, str(url), str(requestHeaders), str(input), status, str(responseHeaders), str(output))
+            logger.debug("%s %s://%s%s %s %s ==> %i %s %s", verb, self.__scheme, self.__hostname, url, requestHeaders, input, status, responseHeaders, output)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -113,6 +113,7 @@
             "integrations": ["cryptography"]
         },
         tests_require=[
-            "cryptography"
+            "cryptography",
+            "httpretty==0.9.6"
         ]
     )


Test patch:
diff --git a/github/tests/AllTests.py b/github/tests/AllTests.py
--- a/github/tests/AllTests.py
+++ b/github/tests/AllTests.py
@@ -105,6 +105,7 @@
 from BadAttributes import *
 from Equality import *
 from Search import *
+from Retry import *
 
 from Issue33 import *
 from Issue50 import *
diff --git a/github/tests/Framework.py b/github/tests/Framework.py
--- a/github/tests/Framework.py
+++ b/github/tests/Framework.py
@@ -41,6 +41,9 @@
 import sys
 import traceback
 import unittest
+import httpretty
+from requests.structures import CaseInsensitiveDict
+from urllib3.util import Url
 
 import github
 
@@ -163,14 +166,28 @@ def __init__(self, testCase, file, protocol, host, port, *args, **kwds):
         self.__file = file
         self.__protocol = protocol
         self.__host = host
-        self.__port = str(port)
+        self.__port = port
+        self.response_headers = CaseInsensitiveDict()
+
+        self.__cnx = self._realConnection(host, port, *args, **kwds)
 
     def request(self, verb, url, input, headers):
+        full_url = Url(scheme=self.__protocol, host=self.__host, port=self.__port, path=url)
+
+        httpretty.register_uri(
+            verb,
+            full_url.url,
+            body=self.__request_callback
+        )
+
+        self.__cnx.request(verb, url, input, headers)
+
+    def __readNextRequest(self, verb, url, input, headers):
         fixAuthorizationHeader(headers)
         self.__testCase.assertEqual(self.__protocol, readLine(self.__file))
         self.__testCase.assertEqual(verb, readLine(self.__file))
         self.__testCase.assertEqual(self.__host, readLine(self.__file))
-        self.__testCase.assertEqual(self.__port, readLine(self.__file))
+        self.__testCase.assertEqual(str(self.__port), readLine(self.__file))
         self.__testCase.assertEqual(self.__splitUrl(url), self.__splitUrl(readLine(self.__file)))
         self.__testCase.assertEqual(headers, eval(readLine(self.__file)))
         expectedInput = readLine(self.__file)
@@ -193,29 +210,58 @@ def __splitUrl(self, url):
         base, qs = splitedUrl
         return (base, sorted(qs.split("&")))
 
-    def getresponse(self):
+    def __request_callback(self, request, uri, response_headers):
+        self.__readNextRequest(self.__cnx.verb, self.__cnx.url, self.__cnx.input, self.__cnx.headers)
+
         status = int(readLine(self.__file))
-        headers = eval(readLine(self.__file))
+        self.response_headers = CaseInsensitiveDict(eval(readLine(self.__file)))
         output = readLine(self.__file)
+        readLine(self.__file)
 
-        return FakeHttpResponse(status, headers, output)
+        if atLeastPython3:
+            output = bytes(output, 'utf-8')
+
+        # make a copy of the headers and remove the ones that interfere with the response handling
+        adding_headers = CaseInsensitiveDict(self.response_headers)
+        adding_headers.pop('content-length', None)
+        adding_headers.pop('transfer-encoding', None)
+        adding_headers.pop('content-encoding', None)
+
+        response_headers.update(adding_headers)
+        return [status, response_headers, output]
+
+    def getresponse(self):
+        # call original connection, this will go all the way down to the python socket and will be intercepted by httpretty
+        response = self.__cnx.getresponse()
+
+        # restore original headers to the response
+        response.headers = self.response_headers
+
+        return response
 
     def close(self):
-        readLine(self.__file)
+        self.__cnx.close()
 
 
-def ReplayingHttpConnection(testCase, file, *args, **kwds):
-    return ReplayingConnection(testCase, file, "http", *args, **kwds)
+class ReplayingHttpConnection(ReplayingConnection):
+    _realConnection = github.Requester.HTTPRequestsConnectionClass
 
+    def __init__(self, testCase, file, *args, **kwds):
+        ReplayingConnection.__init__(self, testCase, file, "http", *args, **kwds)
 
-def ReplayingHttpsConnection(testCase, file, *args, **kwds):
-    return ReplayingConnection(testCase, file, "https", *args, **kwds)
+
+class ReplayingHttpsConnection(ReplayingConnection):
+    _realConnection = github.Requester.HTTPSRequestsConnectionClass
+
+    def __init__(self, testCase, file, *args, **kwds):
+        ReplayingConnection.__init__(self, testCase, file, "https", *args, **kwds)
 
 
 class BasicTestCase(unittest.TestCase):
     recordMode = False
     tokenAuthMode = False
     jwtAuthMode = False
+    retry = None
     replayDataFolder = os.path.join(os.path.dirname(__file__), "ReplayData")
 
     def setUp(self):
@@ -247,8 +293,12 @@ def setUp(self):
             self.client_secret = "client_secret"
             self.jwt = "jwt"
 
+            httpretty.enable(allow_net_connect=False)
+
     def tearDown(self):
         unittest.TestCase.tearDown(self)
+        httpretty.disable()
+        httpretty.reset()
         self.__closeReplayFileIfNeeded()
         github.Requester.Requester.resetConnectionClasses()
 
@@ -298,11 +348,11 @@ def setUp(self):
         github.Requester.Requester.setOnCheckMe(self.getFrameChecker())
 
         if self.tokenAuthMode:
-            self.g = github.Github(self.oauth_token)
+            self.g = github.Github(self.oauth_token, retry=self.retry)
         elif self.jwtAuthMode:
-            self.g = github.Github(jwt=self.jwt)
+            self.g = github.Github(jwt=self.jwt, retry=self.retry)
         else:
-            self.g = github.Github(self.login, self.password)
+            self.g = github.Github(self.login, self.password, retry=self.retry)
 
 
 def activateRecordMode():  # pragma no cover (Function useful only when recording new tests, not used during automated tests)
@@ -315,3 +365,7 @@ def activateTokenAuthMode():  # pragma no cover (Function useful only when recor
 
 def activateJWTAuthMode():  # pragma no cover (Function useful only when recording new tests, not used during automated tests)
     BasicTestCase.jwtAuthMode = True
+
+
+def enableRetry(retry):
+    BasicTestCase.retry = retry
diff --git a/github/tests/ReplayData/Release.testUploadAsset.txt b/github/tests/ReplayData/Release.testUploadAsset.txt
--- a/github/tests/ReplayData/Release.testUploadAsset.txt
+++ b/github/tests/ReplayData/Release.testUploadAsset.txt
@@ -38,6 +38,6 @@ None
 /repos/edhollandAL/PyGithub/releases/1210837/assets?label=unit+test+artifact&name=archive.zip
 {'Authorization': 'Basic login_and_password_removed', 'Content-Length': '140', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/zip'}
 None
-204
-[('content-length', '150'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-served-by', '139317cebd6caf9cd03889139437f00b'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('etag', '"26708914904a29b8c9fd2ac006beb9db"'), ('access-control-allow-credentials', 'true'), ('status', '200 OK'), ('x-ratelimit-remaining', '4942'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', '56BCFFD3:5677:5265FA1:553A03F5'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('last-modified', 'Fri, 24 Apr 2015 08:43:32 GMT'), ('date', 'Fri, 24 Apr 2015 08:51:01 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/zip'), ('x-ratelimit-reset', '1429867683')]
+201
+[('content-length', '155'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-served-by', '139317cebd6caf9cd03889139437f00b'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('etag', '"26708914904a29b8c9fd2ac006beb9db"'), ('access-control-allow-credentials', 'true'), ('status', '201 CREATED'), ('x-ratelimit-remaining', '4942'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', '56BCFFD3:5677:5265FA1:553A03F5'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('last-modified', 'Fri, 24 Apr 2015 08:43:32 GMT'), ('date', 'Fri, 24 Apr 2015 08:51:01 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/zip'), ('x-ratelimit-reset', '1429867683')]
 {"url":"/repos/edhollandAL/PyGithub/releases/1210837/assets/xxx","id":"xxx","name":"archive.zip","label":"unit test artifact","uploader":"unittests"}
diff --git a/github/tests/ReplayData/Retry.testRaisesRetryErrorAfterMaxRetries.txt b/github/tests/ReplayData/Retry.testRaisesRetryErrorAfterMaxRetries.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Retry.testRaisesRetryErrorAfterMaxRetries.txt
@@ -0,0 +1,44 @@
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
diff --git a/github/tests/ReplayData/Retry.testReturnsRepoAfter1Retry.txt b/github/tests/ReplayData/Retry.testReturnsRepoAfter1Retry.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Retry.testReturnsRepoAfter1Retry.txt
@@ -0,0 +1,22 @@
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+500
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '500 Internal Server Error'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+200
+[('Server', 'GitHub.com'), ('Date', 'Fri, 14 Dec 2018 17:20:59 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '200 OK'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4998'), ('X-RateLimit-Reset', '1544811658'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"6edb0f751d877e7d63f6accba11f7cbd"'), ('Last-Modified', 'Thu, 13 Dec 2018 15:19:18 GMT'), ('X-GitHub-Media-Type', 'github.v3; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'E656:4CFB:2F753A4:61982D2:5C13E67A')]
+{"id":3544490,"node_id":"MDEwOlJlcG9zaXRvcnkzNTQ0NDkw","name":"PyGithub","full_name":"PyGithub/PyGithub","private":false,"owner":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars0.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/PyGithub/PyGithub","description":"Typed interactions with the GitHub API v3","fork":false,"url":"https://api.github.com/repos/PyGithub/PyGithub","forks_url":"https://api.github.com/repos/PyGithub/PyGithub/forks","keys_url":"https://api.github.com/repos/PyGithub/PyGithub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/PyGithub/PyGithub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/PyGithub/PyGithub/teams","hooks_url":"https://api.github.com/repos/PyGithub/PyGithub/hooks","issue_events_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/events{/number}","events_url":"https://api.github.com/repos/PyGithub/PyGithub/events","assignees_url":"https://api.github.com/repos/PyGithub/PyGithub/assignees{/user}","branches_url":"https://api.github.com/repos/PyGithub/PyGithub/branches{/branch}","tags_url":"https://api.github.com/repos/PyGithub/PyGithub/tags","blobs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/PyGithub/PyGithub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/refs{/sha}","trees_url":"https://api.github.com/repos/PyGithub/PyGithub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/PyGithub/PyGithub/statuses/{sha}","languages_url":"https://api.github.com/repos/PyGithub/PyGithub/languages","stargazers_url":"https://api.github.com/repos/PyGithub/PyGithub/stargazers","contributors_url":"https://api.github.com/repos/PyGithub/PyGithub/contributors","subscribers_url":"https://api.github.com/repos/PyGithub/PyGithub/subscribers","subscription_url":"https://api.github.com/repos/PyGithub/PyGithub/subscription","commits_url":"https://api.github.com/repos/PyGithub/PyGithub/commits{/sha}","git_commits_url":"https://api.github.com/repos/PyGithub/PyGithub/git/commits{/sha}","comments_url":"https://api.github.com/repos/PyGithub/PyGithub/comments{/number}","issue_comment_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/comments{/number}","contents_url":"https://api.github.com/repos/PyGithub/PyGithub/contents/{+path}","compare_url":"https://api.github.com/repos/PyGithub/PyGithub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/PyGithub/PyGithub/merges","archive_url":"https://api.github.com/repos/PyGithub/PyGithub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/PyGithub/PyGithub/downloads","issues_url":"https://api.github.com/repos/PyGithub/PyGithub/issues{/number}","pulls_url":"https://api.github.com/repos/PyGithub/PyGithub/pulls{/number}","milestones_url":"https://api.github.com/repos/PyGithub/PyGithub/milestones{/number}","notifications_url":"https://api.github.com/repos/PyGithub/PyGithub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/PyGithub/PyGithub/labels{/name}","releases_url":"https://api.github.com/repos/PyGithub/PyGithub/releases{/id}","deployments_url":"https://api.github.com/repos/PyGithub/PyGithub/deployments","created_at":"2012-02-25T12:53:47Z","updated_at":"2018-12-13T15:19:18Z","pushed_at":"2018-12-13T22:50:18Z","git_url":"git://github.com/PyGithub/PyGithub.git","ssh_url":"git@github.com:PyGithub/PyGithub.git","clone_url":"https://github.com/PyGithub/PyGithub.git","svn_url":"https://github.com/PyGithub/PyGithub","homepage":"https://pygithub.readthedocs.io/","size":11559,"stargazers_count":2256,"watchers_count":2256,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":765,"mirror_url":null,"archived":false,"open_issues_count":67,"license":{"key":"lgpl-3.0","name":"GNU Lesser General Public License v3.0","spdx_id":"LGPL-3.0","url":"https://api.github.com/licenses/lgpl-3.0","node_id":"MDc6TGljZW5zZTEy"},"forks":765,"open_issues":67,"watchers":2256,"default_branch":"master","permissions":{"admin":false,"push":false,"pull":true},"organization":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars0.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"network_count":765,"subscribers_count":86}
+
diff --git a/github/tests/ReplayData/Retry.testReturnsRepoAfter3Retries.txt b/github/tests/ReplayData/Retry.testReturnsRepoAfter3Retries.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Retry.testReturnsRepoAfter3Retries.txt
@@ -0,0 +1,44 @@
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+502
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '502 Bad Gateway'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Server Error"}
+
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+200
+[('Server', 'GitHub.com'), ('Date', 'Fri, 14 Dec 2018 17:20:59 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '200 OK'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4998'), ('X-RateLimit-Reset', '1544811658'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"6edb0f751d877e7d63f6accba11f7cbd"'), ('Last-Modified', 'Thu, 13 Dec 2018 15:19:18 GMT'), ('X-GitHub-Media-Type', 'github.v3; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'E656:4CFB:2F753A4:61982D2:5C13E67A')]
+{"id":3544490,"node_id":"MDEwOlJlcG9zaXRvcnkzNTQ0NDkw","name":"PyGithub","full_name":"PyGithub/PyGithub","private":false,"owner":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars0.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/PyGithub/PyGithub","description":"Typed interactions with the GitHub API v3","fork":false,"url":"https://api.github.com/repos/PyGithub/PyGithub","forks_url":"https://api.github.com/repos/PyGithub/PyGithub/forks","keys_url":"https://api.github.com/repos/PyGithub/PyGithub/keys{/key_id}","collaborators_url":"https://api.github.com/repos/PyGithub/PyGithub/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/PyGithub/PyGithub/teams","hooks_url":"https://api.github.com/repos/PyGithub/PyGithub/hooks","issue_events_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/events{/number}","events_url":"https://api.github.com/repos/PyGithub/PyGithub/events","assignees_url":"https://api.github.com/repos/PyGithub/PyGithub/assignees{/user}","branches_url":"https://api.github.com/repos/PyGithub/PyGithub/branches{/branch}","tags_url":"https://api.github.com/repos/PyGithub/PyGithub/tags","blobs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/PyGithub/PyGithub/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/PyGithub/PyGithub/git/refs{/sha}","trees_url":"https://api.github.com/repos/PyGithub/PyGithub/git/trees{/sha}","statuses_url":"https://api.github.com/repos/PyGithub/PyGithub/statuses/{sha}","languages_url":"https://api.github.com/repos/PyGithub/PyGithub/languages","stargazers_url":"https://api.github.com/repos/PyGithub/PyGithub/stargazers","contributors_url":"https://api.github.com/repos/PyGithub/PyGithub/contributors","subscribers_url":"https://api.github.com/repos/PyGithub/PyGithub/subscribers","subscription_url":"https://api.github.com/repos/PyGithub/PyGithub/subscription","commits_url":"https://api.github.com/repos/PyGithub/PyGithub/commits{/sha}","git_commits_url":"https://api.github.com/repos/PyGithub/PyGithub/git/commits{/sha}","comments_url":"https://api.github.com/repos/PyGithub/PyGithub/comments{/number}","issue_comment_url":"https://api.github.com/repos/PyGithub/PyGithub/issues/comments{/number}","contents_url":"https://api.github.com/repos/PyGithub/PyGithub/contents/{+path}","compare_url":"https://api.github.com/repos/PyGithub/PyGithub/compare/{base}...{head}","merges_url":"https://api.github.com/repos/PyGithub/PyGithub/merges","archive_url":"https://api.github.com/repos/PyGithub/PyGithub/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/PyGithub/PyGithub/downloads","issues_url":"https://api.github.com/repos/PyGithub/PyGithub/issues{/number}","pulls_url":"https://api.github.com/repos/PyGithub/PyGithub/pulls{/number}","milestones_url":"https://api.github.com/repos/PyGithub/PyGithub/milestones{/number}","notifications_url":"https://api.github.com/repos/PyGithub/PyGithub/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/PyGithub/PyGithub/labels{/name}","releases_url":"https://api.github.com/repos/PyGithub/PyGithub/releases{/id}","deployments_url":"https://api.github.com/repos/PyGithub/PyGithub/deployments","created_at":"2012-02-25T12:53:47Z","updated_at":"2018-12-13T15:19:18Z","pushed_at":"2018-12-13T22:50:18Z","git_url":"git://github.com/PyGithub/PyGithub.git","ssh_url":"git@github.com:PyGithub/PyGithub.git","clone_url":"https://github.com/PyGithub/PyGithub.git","svn_url":"https://github.com/PyGithub/PyGithub","homepage":"https://pygithub.readthedocs.io/","size":11559,"stargazers_count":2256,"watchers_count":2256,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":765,"mirror_url":null,"archived":false,"open_issues_count":67,"license":{"key":"lgpl-3.0","name":"GNU Lesser General Public License v3.0","spdx_id":"LGPL-3.0","url":"https://api.github.com/licenses/lgpl-3.0","node_id":"MDc6TGljZW5zZTEy"},"forks":765,"open_issues":67,"watchers":2256,"default_branch":"master","permissions":{"admin":false,"push":false,"pull":true},"organization":{"login":"PyGithub","id":11288996,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMjg4OTk2","avatar_url":"https://avatars0.githubusercontent.com/u/11288996?v=4","gravatar_id":"","url":"https://api.github.com/users/PyGithub","html_url":"https://github.com/PyGithub","followers_url":"https://api.github.com/users/PyGithub/followers","following_url":"https://api.github.com/users/PyGithub/following{/other_user}","gists_url":"https://api.github.com/users/PyGithub/gists{/gist_id}","starred_url":"https://api.github.com/users/PyGithub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PyGithub/subscriptions","organizations_url":"https://api.github.com/users/PyGithub/orgs","repos_url":"https://api.github.com/users/PyGithub/repos","events_url":"https://api.github.com/users/PyGithub/events{/privacy}","received_events_url":"https://api.github.com/users/PyGithub/received_events","type":"Organization","site_admin":false},"network_count":765,"subscribers_count":86}
+
diff --git a/github/tests/ReplayData/Retry.testShouldNotRetryWhenStatusNotOnList.txt b/github/tests/ReplayData/Retry.testShouldNotRetryWhenStatusNotOnList.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Retry.testShouldNotRetryWhenStatusNotOnList.txt
@@ -0,0 +1,11 @@
+https
+GET
+api.github.com
+None
+/repos/PyGithub/PyGithub
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+400
+[('Server', 'GitHub.com'), ('Date', 'Fri, 26 Oct 2018 06:02:42 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Status', '400 Bad Request'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4970'), ('X-RateLimit-Reset', '1540536267'), ('Cache-Control', 'private, max-age=60, s-maxage=60'), ('Vary', 'Accept, Authorization, Cookie, X-GitHub-OTP'), ('ETag', 'W/"3ffd69d4f4e7383eae948812b1eb72e5"'), ('X-OAuth-Scopes', 'repo'), ('X-Accepted-OAuth-Scopes', ''), ('X-GitHub-Media-Type', 'github.v3; param=star; format=json'), ('Access-Control-Expose-Headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'F9D6:235E:3C00DD:772BC6:5BD2AE02')]
+{"error": "Bad Request"}
+
diff --git a/github/tests/Retry.py b/github/tests/Retry.py
new file mode 100644
--- /dev/null
+++ b/github/tests/Retry.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+
+############################ Copyrights and license ############################
+#                                                                              #
+# Copyright 2018 Justin Kufro <jkufro@andrew.cmu.edu>                          #
+# Copyright 2018 Ivan Minno <iminno@andrew.cmu.edu>                            #
+# Copyright 2018 Zilei Gu <zileig@andrew.cmu.edu>                              #
+# Copyright 2018 Yves Zumbach <yzumbach@andrew.cmu.edu>                        #
+# Copyright 2018 Leying Chen <leyingc@andrew.cmu.edu>                          #
+#                                                                              #
+# This file is part of PyGithub.                                               #
+# http://pygithub.readthedocs.io/                                              #
+#                                                                              #
+# PyGithub is free software: you can redistribute it and/or modify it under    #
+# the terms of the GNU Lesser General Public License as published by the Free  #
+# Software Foundation, either version 3 of the License, or (at your option)    #
+# any later version.                                                           #
+#                                                                              #
+# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY  #
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    #
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
+# details.                                                                     #
+#                                                                              #
+# You should have received a copy of the GNU Lesser General Public License     #
+# along with PyGithub. If not, see <http://www.gnu.org/licenses/>.             #
+#                                                                              #
+################################################################################
+import urllib3
+from httpretty import httpretty
+
+import Framework
+
+import requests
+
+from github import GithubException
+from github.Repository import Repository
+
+REPO_NAME = 'PyGithub/PyGithub'
+
+
+class Retry(Framework.TestCase):
+    def setUp(self):
+        # status codes returned on random github server errors
+        status_forcelist = (500, 502, 504)
+        retry = urllib3.Retry(
+            total=3,
+            read=3,
+            connect=3,
+            status_forcelist=status_forcelist
+        )
+
+        Framework.enableRetry(retry)
+        Framework.TestCase.setUp(self)
+
+    def testShouldNotRetryWhenStatusNotOnList(self):
+        try:
+            self.g.get_repo(REPO_NAME)
+        except GithubException:
+            self.assertEquals(len(httpretty.latest_requests), 1)
+
+    def testReturnsRepoAfter3Retries(self):
+        repository = self.g.get_repo(REPO_NAME)
+        self.assertEquals(len(httpretty.latest_requests), 4)
+        for request in httpretty.latest_requests:
+            self.assertEquals(request.path, '/repos/' + REPO_NAME)
+
+        self.assertIsInstance(repository, Repository)
+        self.assertEquals(repository.full_name, REPO_NAME)
+
+    def testReturnsRepoAfter1Retry(self):
+        repository = self.g.get_repo(REPO_NAME)
+        self.assertEquals(len(httpretty.latest_requests), 2)
+        for request in httpretty.latest_requests:
+            self.assertEquals(request.path, '/repos/' + REPO_NAME)
+
+        self.assertIsInstance(repository, Repository)
+        self.assertEquals(repository.full_name, REPO_NAME)
+
+    def testRaisesRetryErrorAfterMaxRetries(self):
+        try:
+            response = self.g.get_repo('PyGithub/PyGithub')
+            self.fail("RetryError should have been raised")
+        except requests.exceptions.RetryError:
+            self.assertEquals(len(httpretty.latest_requests), 4)
+            for request in httpretty.latest_requests:
+                self.assertEquals(request.path, '/repos/PyGithub/PyGithub')