Blame rewrite-httppy-pycurl-requests-0.30.patch

aa7aaa1
From 22936cd8ed0f0d5b0c3ece75752e2877f46af455 Mon Sep 17 00:00:00 2001
2a0688a
From: Patrick Uiterwijk <puiterwijk@redhat.com>
2a0688a
Date: Wed, 21 Sep 2016 13:48:07 +0000
2a0688a
Subject: [PATCH 1/3] Rewrite http.py to use python-requests
2a0688a
2a0688a
This will work around an issue when pycurl is used in the same context as
2a0688a
rpm is, as used by koji-containerbuild.
2a0688a
2a0688a
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>
2a0688a
---
aa7aaa1
 osbs/http.py | 290 +++++++++--------------------------------------------------
aa7aaa1
 1 file changed, 42 insertions(+), 248 deletions(-)
2a0688a
2a0688a
diff --git a/osbs/http.py b/osbs/http.py
aa7aaa1
index 9955c55..5f39141 100644
2a0688a
--- a/osbs/http.py
2a0688a
+++ b/osbs/http.py
2a0688a
@@ -24,77 +24,17 @@
2a0688a
 import logging
2a0688a
 from io import BytesIO
2a0688a
 
2a0688a
-import pycurl
2a0688a
-
2a0688a
 from osbs.exceptions import OsbsException, OsbsNetworkException, OsbsResponseException
2a0688a
 
2a0688a
+import requests
2a0688a
 try:
2a0688a
-    # py2
2a0688a
-    import httplib
2a0688a
+    from requests_kerberos import HTTPKerberosAuth
2a0688a
 except ImportError:
2a0688a
-    # py3
2a0688a
-    import http.client as httplib
2a0688a
+    HTTPKerberosAuth = None
2a0688a
 
2a0688a
 
2a0688a
-logger = logging.getLogger(__name__)
2a0688a
 
2a0688a
-SELECT_TIMEOUT = 9999
2a0688a
-PYCURL_NETWORK_CODES = [pycurl.E_BAD_CONTENT_ENCODING,
2a0688a
-                        pycurl.E_BAD_DOWNLOAD_RESUME,
2a0688a
-                        pycurl.E_CONV_FAILED,
2a0688a
-                        pycurl.E_CONV_REQD,
2a0688a
-                        pycurl.E_COULDNT_CONNECT,
2a0688a
-                        pycurl.E_COULDNT_RESOLVE_HOST,
2a0688a
-                        pycurl.E_COULDNT_RESOLVE_PROXY,
2a0688a
-                        pycurl.E_FILESIZE_EXCEEDED,
2a0688a
-                        pycurl.E_HTTP_POST_ERROR,
2a0688a
-                        pycurl.E_HTTP_RANGE_ERROR,
2a0688a
-                        pycurl.E_HTTP_RETURNED_ERROR,
2a0688a
-                        pycurl.E_LOGIN_DENIED,
2a0688a
-                        # old pycurl: E_OPERATION_TIMEOUTED, new pycurl: E_OPERATION_TIMEDOUT
2a0688a
-                        getattr(pycurl, "E_OPERATION_TIMEDOUT", "E_OPERATION_TIMEOUTED"),
2a0688a
-                        pycurl.E_PARTIAL_FILE,
2a0688a
-                        pycurl.E_READ_ERROR,
2a0688a
-                        pycurl.E_RECV_ERROR,
2a0688a
-                        pycurl.E_REMOTE_FILE_NOT_FOUND,
2a0688a
-                        pycurl.E_SEND_ERROR,
2a0688a
-                        pycurl.E_SSL_CACERT,
2a0688a
-                        pycurl.E_SSL_CERTPROBLEM,
2a0688a
-                        pycurl.E_SSL_CIPHER,
2a0688a
-                        pycurl.E_SSL_CONNECT_ERROR,
2a0688a
-                        pycurl.E_SSL_PEER_CERTIFICATE,
2a0688a
-                        pycurl.E_SSL_SHUTDOWN_FAILED,
2a0688a
-                        pycurl.E_TOO_MANY_REDIRECTS,
2a0688a
-                        pycurl.E_UNSUPPORTED_PROTOCOL,
2a0688a
-                        pycurl.E_WRITE_ERROR]
2a0688a
-
2a0688a
-PYCURL_NETWORK_CODES = [x for x in PYCURL_NETWORK_CODES if x is not None]
2a0688a
-
2a0688a
-
2a0688a
-def parse_headers(all_headers):
2a0688a
-    # all_headers contains headers from all responses - even without FOLLOWLOCATION there
2a0688a
-    # might be multiple sets of headers due to 401 Unauthorized. We only care about the last
2a0688a
-    # response.
2a0688a
-    try:
2a0688a
-        raw_headers = all_headers.split(b"\r\n\r\n")[-2]
2a0688a
-    except IndexError:
2a0688a
-        logger.warning('Incorrectly terminated http headers')
2a0688a
-        raw_headers = all_headers
2a0688a
-
2a0688a
-    logger.debug("raw headers: " + repr(raw_headers))
2a0688a
-
2a0688a
-    # http://stackoverflow.com/questions/24728088/python-parse-http-response-string/24729316#24729316
2a0688a
-    class FakeSocket(object):
2a0688a
-        def __init__(self, response_str):
2a0688a
-            self._file = BytesIO(response_str)
2a0688a
-
2a0688a
-        def makefile(self, *args, **kwargs):
2a0688a
-            return self._file
2a0688a
-
2a0688a
-    response = httplib.HTTPResponse(FakeSocket(raw_headers))
2a0688a
-    response.begin()
2a0688a
-    header_list = [(k.lower(), v) for (k, v) in response.getheaders()]
2a0688a
-    return dict(header_list)
2a0688a
+logger = logging.getLogger(__name__)
2a0688a
 
2a0688a
 
2a0688a
 class HttpSession(object):
2a0688a
@@ -120,22 +60,9 @@ def request(self, url, *args, **kwargs):
2a0688a
                 return stream
2a0688a
 
2a0688a
             with stream as s:
2a0688a
-                # joining at once is much faster than doing += in a loop
2a0688a
-                all_chunks = list(s.iter_chunks())
2a0688a
-                content = ''.join(all_chunks)
2a0688a
+                content = s.req.content
2a0688a
                 return HttpResponse(s.status_code, s.headers, content)
2a0688a
-        except pycurl.error as ex:
2a0688a
-            code = ex.args[0]
2a0688a
-            try:
2a0688a
-                message = ex.args[1]
2a0688a
-            except IndexError:
2a0688a
-                # happened on rhel 6
2a0688a
-                message = ""
2a0688a
-            if code in PYCURL_NETWORK_CODES:
2a0688a
-                raise OsbsNetworkException(url, message, code, *ex.args[2:],
2a0688a
-                                           cause=ex,
2a0688a
-                                           traceback=sys.exc_info()[2])
2a0688a
-
2a0688a
+        except Exception as ex:
2a0688a
             raise OsbsException(cause=ex, traceback=sys.exc_info()[2])
2a0688a
 
2a0688a
 
aa7aaa1
@@ -160,203 +87,68 @@ def __init__(self, url, method, data=None, kerberos_auth=False,
2a0688a
 
2a0688a
         self.status_code = 0
2a0688a
         self.headers = None
2a0688a
-        self.response_buffer = BytesIO()
2a0688a
-        self.headers_buffer = BytesIO()
2a0688a
-        self.response_decoder = None
2a0688a
 
2a0688a
         self.url = url
2a0688a
         headers = headers or {}
2a0688a
         method = method.lower()
2a0688a
 
2a0688a
-        self.c = pycurl.Curl()
2a0688a
-        self.curl_multi = pycurl.CurlMulti()
2a0688a
-
2a0688a
-        if method == 'post':
2a0688a
-            self.c.setopt(pycurl.POST, 1)
2a0688a
-            headers["Expect"] = ""  # openshift can't handle Expect
2a0688a
-        elif method == 'get':
2a0688a
-            self.c.setopt(pycurl.HTTPGET, 1)
2a0688a
-        elif method == 'put':
2a0688a
-            # self.c.setopt(pycurl.PUT, 1)
2a0688a
-            self.c.setopt(pycurl.CUSTOMREQUEST, b"PUT")
2a0688a
-            headers["Expect"] = ""
2a0688a
-        elif method == 'delete':
2a0688a
-            self.c.setopt(pycurl.CUSTOMREQUEST, b"DELETE")
2a0688a
-        else:
2a0688a
+        if method not in ['post', 'get', 'put', 'delete']:
2a0688a
             raise RuntimeError("Unsupported method '%s' for curl call!" % method)
2a0688a
 
2a0688a
-        self.c.setopt(pycurl.COOKIEFILE, b'')
2a0688a
-        self.c.setopt(pycurl.URL, str(url))
2a0688a
-        self.c.setopt(pycurl.WRITEFUNCTION, self.response_buffer.write)
2a0688a
-        self.c.setopt(pycurl.HEADERFUNCTION, self.headers_buffer.write)
2a0688a
-        self.c.setopt(pycurl.DEBUGFUNCTION, self._curl_debug)
2a0688a
-        self.c.setopt(pycurl.SSL_VERIFYPEER, 1 if verify_ssl else 0)
2a0688a
-        self.c.setopt(pycurl.SSL_VERIFYHOST, 2 if verify_ssl else 0)
2a0688a
-        if ca:
2a0688a
-            logger.info("Setting CAINFO to %r", ca)
2a0688a
-            self.c.setopt(pycurl.CAINFO, ca)
2a0688a
-
2a0688a
-        self.c.setopt(pycurl.VERBOSE, 1 if verbose else 0)
2a0688a
+        args = {}
2a0688a
+
2a0688a
+        if method in ['post', 'put']:
2a0688a
+            headers['Expect'] = ''
2a0688a
+
2a0688a
+        if not verify_ssl:
2a0688a
+            args['verify'] = False
2a0688a
+        else:
2a0688a
+            if ca:
2a0688a
+                args['verify'] = ca
2a0688a
+            else:
2a0688a
+                args['verify'] = True
2a0688a
+
2a0688a
         if username and password:
2a0688a
-            username = username.encode('utf-8')
2a0688a
-            password = password.encode('utf-8')
2a0688a
-            self.c.setopt(pycurl.USERPWD, username + b":" + password)
2a0688a
+            args['auth'] = (username, password)
2a0688a
 
2a0688a
         if client_cert and client_key:
2a0688a
-            self.c.setopt(pycurl.SSLCERTTYPE, "PEM")
2a0688a
-            self.c.setopt(pycurl.SSLKEYTYPE, "PEM")
2a0688a
-            self.c.setopt(pycurl.SSLCERT, client_cert)
2a0688a
-            self.c.setopt(pycurl.SSLKEY, client_key)
2a0688a
+            args['cert'] = (client_cert, client_key)
2a0688a
 
2a0688a
         if data:
2a0688a
-            # curl sets the method to post if one sets any POSTFIELDS (even '')
2a0688a
-            self.c.setopt(pycurl.POSTFIELDS, data)
2a0688a
+            args['data'] = data
2a0688a
 
2a0688a
         if use_json:
2a0688a
-            headers['Content-Type'] = b'application/json'
2a0688a
+            headers['Content-Type'] = 'application/json'
2a0688a
 
2a0688a
-        if allow_redirects:
2a0688a
-            self.c.setopt(pycurl.FOLLOWLOCATION, 1)
2a0688a
+        args['allow_redirects'] = allow_redirects
2a0688a
 
2a0688a
         if kerberos_auth:
2a0688a
-            self.c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_GSSNEGOTIATE)
2a0688a
-            self.c.setopt(pycurl.USERPWD, b':')
2a0688a
+            if not HTTPKerberosAuth:
2a0688a
+                raise RuntimeError('Kerberos auth unavailable')
2a0688a
+            args['auth'] = HTTPKerberosAuth()
2a0688a
 
2a0688a
         if stream:
2a0688a
-            headers['Cache-Control'] = b'no-cache'
2a0688a
-
2a0688a
-        if headers:
2a0688a
-            header_list = []
2a0688a
-            for header_key, header_value in headers.items():
2a0688a
-                header_list.append(str("%s: %s" % (header_key, header_value)))
2a0688a
-            self.c.setopt(pycurl.HTTPHEADER, header_list)
2a0688a
-
2a0688a
-        self.curl_multi.add_handle(self.c)
2a0688a
-
2a0688a
-        # Send request and read all headers. We have all headers once we receive some data or once
2a0688a
-        # the response ends.
2a0688a
-        # NOTE: HTTP response in chunked encoding can contain additional headers ("trailers") in the
2a0688a
-        # last chunk. This is not handled here.
2a0688a
-        while not (self.finished or self._any_data_received()):
2a0688a
-            self._select()
2a0688a
-            self._perform()
2a0688a
-
2a0688a
-        self.headers = parse_headers(self.headers_buffer.getvalue())
2a0688a
-        self.status_code = self.c.getinfo(pycurl.HTTP_CODE)
2a0688a
-        self.response_decoder = codecs.getincrementaldecoder(self.encoding)()
2a0688a
-
2a0688a
-    def _perform(self):
2a0688a
-        while True:
2a0688a
-            ret, num_handles = self.curl_multi.perform()
2a0688a
-            if ret != pycurl.E_CALL_MULTI_PERFORM:
2a0688a
-                # see curl_multi_perform manpage
2a0688a
-                break
2a0688a
-
2a0688a
-        num_q, _, err_list = self.curl_multi.info_read()
2a0688a
-        if num_q != 0:
2a0688a
-            logger.warning("CurlMulti.info_read() has %s remaining messages", num_q)
2a0688a
-
2a0688a
-        if err_list:
2a0688a
-            err_obj = err_list[0]
2a0688a
-
2a0688a
-            # For anything but the connection being closed, raise
2a0688a
-            if err_obj[1] != pycurl.E_PARTIAL_FILE:
2a0688a
-                raise OsbsNetworkException(self.url, err_obj[2], err_obj[1])
2a0688a
-
2a0688a
-        self.finished = (num_handles == 0)
2a0688a
-
2a0688a
-    def _select(self):
2a0688a
-        sel = self.curl_multi.select(SELECT_TIMEOUT)
2a0688a
-        if sel == -1:
2a0688a
-            raise OsbsException("CurlMulti.select() timed out")
2a0688a
-        elif sel == 0:
2a0688a
-            # sel==0 means curl_multi_fdset returned -1
2a0688a
-            # manual page suggests sleeping >100ms when this happens:(
2a0688a
-            time.sleep(0.1)
2a0688a
-
2a0688a
-    def _any_data_received(self):
2a0688a
-        return self.response_buffer.tell() != 0
2a0688a
+            args['stream'] = True
2a0688a
+
2a0688a
+        args['headers'] = headers
2a0688a
+        self.req = requests.request(method, url, **args)
2a0688a
+
2a0688a
+        self.headers = self.req.headers
2a0688a
+        self.status_code = self.req.status_code
2a0688a
 
2a0688a
     def _get_received_data(self):
2a0688a
-        result = self.response_buffer.getvalue()
2a0688a
-        self.response_buffer.truncate(0)
2a0688a
-        self.response_buffer.seek(0)
2a0688a
-        return self.response_decoder.decode(result, final=self.finished)
2a0688a
+        return self.req.content
2a0688a
 
2a0688a
     def iter_chunks(self):
2a0688a
-        while True:
2a0688a
-            self._perform()
2a0688a
-            if self._any_data_received():
2a0688a
-                yield self._get_received_data()
2a0688a
-            if self.finished:
2a0688a
-                break
2a0688a
-            self._select()
2a0688a
-
2a0688a
-        logger.debug("end of the stream")
2a0688a
-        self.close()
2a0688a
+        return self.req.iter_content(None)
2a0688a
 
2a0688a
     def iter_lines(self):
2a0688a
-        chunks = self.iter_chunks()
2a0688a
-        return self._split_lines_from_chunks(chunks)
2a0688a
-
2a0688a
-    @staticmethod
2a0688a
-    def _split_lines_from_chunks(chunks):
2a0688a
-        # same behaviour as requests' Response.iter_lines(...)
2a0688a
-
2a0688a
-        pending = None
2a0688a
-        for chunk in chunks:
2a0688a
-
2a0688a
-            if pending is not None:
2a0688a
-                chunk = pending + chunk
2a0688a
-            lines = chunk.splitlines()
2a0688a
-
2a0688a
-            if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:
2a0688a
-                pending = lines.pop()
2a0688a
-            else:
2a0688a
-                pending = None
2a0688a
-
2a0688a
-            for line in lines:
2a0688a
-                yield line
2a0688a
-
2a0688a
-        if pending is not None:
2a0688a
-            yield pending
2a0688a
-
2a0688a
-    @staticmethod
2a0688a
-    def _curl_debug(debug_type, debug_msg):
2a0688a
-        try:
2a0688a
-            logger_name = {
2a0688a
-                pycurl.INFOTYPE_TEXT: 'curl',
2a0688a
-                pycurl.INFOTYPE_HEADER_IN: 'in',
2a0688a
-                pycurl.INFOTYPE_HEADER_OUT: 'out'
2a0688a
-            }[debug_type]
2a0688a
-        except KeyError:
2a0688a
-            return
2a0688a
-
2a0688a
-        curl_logger = logging.getLogger(__name__ + '.' + logger_name)
2a0688a
-        for line in debug_msg.splitlines():
2a0688a
-            if not line:
2a0688a
-                continue
2a0688a
-            curl_logger.debug(line)
2a0688a
-
2a0688a
-    @property
2a0688a
-    def encoding(self):
2a0688a
-        encoding = None
2a0688a
-        if 'content-type' in self.headers:
2a0688a
-            content_type = self.headers['content-type'].lower()
2a0688a
-            match = re.search(r'charset=(\S+)', content_type)
2a0688a
-            if match:
2a0688a
-                encoding = match.group(1)
2a0688a
-        if encoding is None:
2a0688a
-            encoding = 'utf-8'  # assume utf-8
2a0688a
-
2a0688a
-        return encoding
2a0688a
+        return self.req.iter_lines()
2a0688a
 
2a0688a
     def close(self):
2a0688a
         if not self.closed:
2a0688a
             logger.debug("cleaning up")
2a0688a
-            self.curl_multi.remove_handle(self.c)
2a0688a
-            self.c.close()
2a0688a
-            self.curl_multi.close()
2a0688a
+            del self.req
2a0688a
         self.closed = True
2a0688a
 
2a0688a
     def __del__(self):
aa7aaa1
@@ -376,7 +168,9 @@ def __init__(self, status_code, headers, content):
2a0688a
         self.content = content
2a0688a
 
2a0688a
     def json(self, check=True):
2a0688a
-        if check and self.status_code not in (0, httplib.OK, httplib.CREATED):
2a0688a
+        if check and self.status_code not in (0, requests.codes.OK, requests.codes.CREATED):
2a0688a
             raise OsbsResponseException(self.content, self.status_code)
2a0688a
 
aa7aaa1
+        if isinstance(self.content, bytes):
aa7aaa1
+            self.content = self.content.decode('utf-8')
aa7aaa1
         return json.loads(self.content)
2a0688a
aa7aaa1
From b626346936a0cd12cdaed028152311f70afcdc41 Mon Sep 17 00:00:00 2001
2a0688a
From: Patrick Uiterwijk <puiterwijk@redhat.com>
2a0688a
Date: Wed, 21 Sep 2016 14:13:25 +0000
2a0688a
Subject: [PATCH 2/3] Fix tests for porting of http client to requests
2a0688a
2a0688a
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>
2a0688a
---
2a0688a
 tests/test_http.py | 58 +-----------------------------------------------------
2a0688a
 1 file changed, 1 insertion(+), 57 deletions(-)
2a0688a
2a0688a
diff --git a/tests/test_http.py b/tests/test_http.py
2a0688a
index 674f9eb..30d2f14 100644
2a0688a
--- a/tests/test_http.py
2a0688a
+++ b/tests/test_http.py
2a0688a
@@ -12,7 +12,7 @@
2a0688a
 import pytest
2a0688a
 
2a0688a
 import osbs.http as osbs_http
2a0688a
-from osbs.http import parse_headers, HttpSession, HttpStream
2a0688a
+from osbs.http import HttpSession, HttpStream
2a0688a
 from osbs.exceptions import OsbsNetworkException
2a0688a
 
2a0688a
 from tests.fake_api import Connection, ResponseMapping
2a0688a
@@ -34,22 +34,6 @@ def has_connection():
2a0688a
         return False
2a0688a
 
2a0688a
 
2a0688a
-class TestParseHeaders(object):
2a0688a
-    def test_parse_headers(self):
2a0688a
-        conn = Connection("0.5.4")
2a0688a
-        rm = ResponseMapping("0.5.4", lookup=conn.get_definition_for)
2a0688a
-
2a0688a
-        key, value = conn.get_definition_for("/oauth/authorize")
2a0688a
-        file_name = value["get"]["file"]
2a0688a
-        raw_headers = rm.get_response_content(file_name)
2a0688a
-
2a0688a
-        headers = parse_headers(raw_headers)
2a0688a
-
2a0688a
-        assert headers is not None
2a0688a
-        assert len(headers.items()) > 0
2a0688a
-        assert headers["location"]
2a0688a
-
2a0688a
-
2a0688a
 @pytest.mark.skipif(not has_connection(),
2a0688a
                     reason="requires internet connection")
2a0688a
 class TestHttpSession(object):
2a0688a
@@ -141,43 +125,3 @@ def test_raise(self, s):
2a0688a
             with s.get("http://httpbin.org/stream/3", stream=True) as s:
2a0688a
                 raise RuntimeError("hi")
2a0688a
         assert s.closed
2a0688a
-
2a0688a
-
2a0688a
-class TestHttpStream(object):
2a0688a
-    @pytest.mark.parametrize('chunks,expected_content', [
2a0688a
-        ([b'foo', b'', b'bar', b'baz'], u'foobarbaz'),
2a0688a
-        ([b'a', b'b', b'\xc4', b'\x8d', b'x'], u'ab\u010dx'),
2a0688a
-        ([b'\xe2', b'\x8a', b'\x86'], u'\u2286'),
2a0688a
-        ([b'\xe2\x8a', b'\x86'], u'\u2286'),
2a0688a
-        ([b'\xe2', b'\x8a\x86'], u'\u2286'),
2a0688a
-        ([b'aaaa', b'\xe2\x8a', b'\x86'], u'aaaa\u2286'),
2a0688a
-        ([b'aaaa\xe2\x8a', b'\x86'], u'aaaa\u2286'),
2a0688a
-        ([b'\xe2\x8a', b'\x86ffff'], u'\u2286ffff'),
2a0688a
-    ])
2a0688a
-    def test_http_multibyte_decoding(self, chunks, expected_content):
2a0688a
-        class Whatever(object):
2a0688a
-            def __getattr__(self, name):
2a0688a
-                return self
2a0688a
-
2a0688a
-            def __call__(self, *args, **kwargs):
2a0688a
-                return self
2a0688a
-        flexmock(pycurl).should_receive('Curl').and_return(Whatever())
2a0688a
-        flexmock(pycurl).should_receive('CurlMulti').and_return(Whatever())
2a0688a
-        (flexmock(osbs_http).should_receive('parse_headers')
2a0688a
-                            .and_return({'content-type': 'application/json; charset=utf-8'}))
2a0688a
-        flexmock(HttpStream, _select=lambda: None)
2a0688a
-
2a0688a
-        def mock_perform(self):
2a0688a
-            if chunks:
2a0688a
-                self.response_buffer.write(chunks.pop(0))
2a0688a
-            else:
2a0688a
-                self.finished = True
2a0688a
-
2a0688a
-        try:
2a0688a
-            orig_perform = HttpStream._perform
2a0688a
-            HttpStream._perform = mock_perform
2a0688a
-
2a0688a
-            r = HttpSession(verbose=True).get('http://')
2a0688a
-            assert r.content == expected_content
2a0688a
-        finally:
2a0688a
-            HttpStream._perform = orig_perform
2a0688a
aa7aaa1
From cdd2061d6397e159bb32413b39e538be7685b505 Mon Sep 17 00:00:00 2001
2a0688a
From: Patrick Uiterwijk <puiterwijk@redhat.com>
2a0688a
Date: Wed, 21 Sep 2016 14:27:16 +0000
2a0688a
Subject: [PATCH 3/3] Update spec file to include python-requests
2a0688a
2a0688a
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>
2a0688a
---
2a0688a
 osbs-client.spec | 12 ++++++++----
2a0688a
 1 file changed, 8 insertions(+), 4 deletions(-)
2a0688a
2a0688a
diff --git a/osbs-client.spec b/osbs-client.spec
2a0688a
index 298aadf..c0b3cd5 100644
2a0688a
--- a/osbs-client.spec
2a0688a
+++ b/osbs-client.spec
2a0688a
@@ -60,7 +60,8 @@ BuildRequires:  python-pytest-capturelog
2a0688a
 BuildRequires:  python-flexmock
2a0688a
 BuildRequires:  python-six
2a0688a
 BuildRequires:  python-dockerfile-parse
2a0688a
-BuildRequires:  python-pycurl
2a0688a
+BuildRequires:  python-requests
2a0688a
+BuildRequires:  python-requests-kerberos
2a0688a
 %endif # with_check
2a0688a
 
2a0688a
 %if 0%{?with_python3}
2a0688a
@@ -73,7 +74,8 @@ BuildRequires:  python3-pytest-capturelog
2a0688a
 BuildRequires:  python3-flexmock
2a0688a
 BuildRequires:  python3-six
2a0688a
 BuildRequires:  python3-dockerfile-parse
2a0688a
-BuildRequires:  python3-pycurl
2a0688a
+BuildRequires:  python3-requests
2a0688a
+BuildRequires:  python3-requests-kerberos
2a0688a
 %endif # with_check
2a0688a
 %endif # with_python3
2a0688a
 
2a0688a
@@ -91,7 +93,8 @@ Summary:        Python 2 module for OpenShift Build Service
2a0688a
 Group:          Development/Tools
2a0688a
 License:        BSD
2a0688a
 Requires:       python-dockerfile-parse
2a0688a
-Requires:       python-pycurl
2a0688a
+Requires:       python-requests
2a0688a
+Requires:       python-requests-kerberos
2a0688a
 Requires:       python-setuptools
2a0688a
 Requires:       krb5-workstation
2a0688a
 %if 0%{?rhel} && 0%{?rhel} <= 6
2a0688a
@@ -113,7 +116,8 @@ Summary:        Python 3 module for OpenShift Build Service
2a0688a
 Group:          Development/Tools
2a0688a
 License:        BSD
2a0688a
 Requires:       python3-dockerfile-parse
2a0688a
-Requires:       python3-pycurl
2a0688a
+Requires:       python3-requests
2a0688a
+Requires:       python3-requests-kerberos
2a0688a
 Requires:       python3-dateutil
2a0688a
 Requires:       python3-setuptools
2a0688a
 Requires:       krb5-workstation