diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b7b2fc2..2dd83d77 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,7 +53,12 @@ def get_protected_branch(self): ## Automated tests -You can run the tests through `python -m github.tests`. +First you need to install the test dependencies: +```bash +pip install -r test-requirements.txt +``` + +Then you can run the tests through `python -m github.tests`. Run a specific test with `python -m github.tests TestCase` or `python -m github.tests TestCase.testMethod`. If you add a new test, for example `Issue139.testCompletion`, you must add an import in `github/tests/AllTests.py`. diff --git a/github/MainClass.py b/github/MainClass.py index 1e86f517..f75c7f77 100644 --- a/github/MainClass.py +++ b/github/MainClass.py @@ -55,6 +55,7 @@ import time import sys import requests import jwt +import urllib3 from Requester import Requester import AuthenticatedUser @@ -93,7 +94,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 @@ -104,6 +105,7 @@ class Github(object): :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 @@ -115,7 +117,8 @@ class Github(object): 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 index 3ac8a9fa..fe6fee9c 100644 --- a/github/Requester.py +++ b/github/Requester.py @@ -89,13 +89,18 @@ class RequestsResponse: 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 @@ class HTTPSRequestsConnectionClass(object): 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 @@ class Requester: ############################################################# - 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 @@ class Requester: 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 @@ class Requester: (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 @@ class Requester: 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 @@ class Requester: 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/github/tests/AllTests.py b/github/tests/AllTests.py index b7f84784..9122d225 100644 --- a/github/tests/AllTests.py +++ b/github/tests/AllTests.py @@ -104,6 +104,7 @@ from ExposeAllAttributes import * 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 index 8b61433e..b334d358 100644 --- a/github/tests/Framework.py +++ b/github/tests/Framework.py @@ -41,6 +41,9 @@ import os import sys import traceback import unittest +import httpretty +from requests.structures import CaseInsensitiveDict +from urllib3.util import Url import github @@ -163,14 +166,28 @@ class ReplayingConnection: 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 @@ class ReplayingConnection: 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) - - return FakeHttpResponse(status, headers, output) - - def close(self): readLine(self.__file) + if atLeastPython3: + output = bytes(output, 'utf-8') -def ReplayingHttpConnection(testCase, file, *args, **kwds): - return ReplayingConnection(testCase, file, "http", *args, **kwds) + # 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): + self.__cnx.close() -def ReplayingHttpsConnection(testCase, file, *args, **kwds): - return ReplayingConnection(testCase, file, "https", *args, **kwds) +class ReplayingHttpConnection(ReplayingConnection): + _realConnection = github.Requester.HTTPRequestsConnectionClass + + def __init__(self, testCase, file, *args, **kwds): + ReplayingConnection.__init__(self, testCase, file, "http", *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 @@ class BasicTestCase(unittest.TestCase): 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 @@ class TestCase(BasicTestCase): 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 index 05858a62..33828902 100644 --- 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 index 00000000..643222a0 --- /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 index 00000000..22bb11d8 --- /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 index 00000000..0fc65c45 --- /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 index 00000000..e3fadf71 --- /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 index 00000000..bcf9890c --- /dev/null +++ b/github/tests/Retry.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +############################ Copyrights and license ############################ +# # +# Copyright 2018 Justin Kufro # +# Copyright 2018 Ivan Minno # +# Copyright 2018 Zilei Gu # +# Copyright 2018 Yves Zumbach # +# Copyright 2018 Leying Chen # +# # +# 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 . # +# # +################################################################################ +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') diff --git a/setup.py b/setup.py index cea0bd62..205d3936 100755 --- a/setup.py +++ b/setup.py @@ -113,6 +113,7 @@ if __name__ == "__main__": "integrations": ["cryptography"] }, tests_require=[ - "cryptography" + "cryptography", + "httpretty==0.9.6" ] ) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..dba76c0b --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +httpretty==0.9.6