diff --git a/CHANGELOG.md b/CHANGELOG.md index a57769c0..5d868eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +* Requests sessions is initialized only once instead of it being done in each request. + ## 4.41.0 * Add extra fields in `sender` and `receiver` details in `transfer` to `Transaction` * Remove unused error code `AdjustmentAmountMustBeGreaterThanZero` diff --git a/braintree/configuration.py b/braintree/configuration.py index 45bca10e..d2622945 100644 --- a/braintree/configuration.py +++ b/braintree/configuration.py @@ -1,3 +1,4 @@ +import requests import braintree from braintree.credentials_parser import CredentialsParser from braintree.environment import Environment @@ -101,8 +102,10 @@ def __init__(self, environment=None, merchant_id=None, public_key=None, private_ self.access_token = parser.access_token self.timeout = kwargs.get("timeout", 60) self.wrap_http_exceptions = kwargs.get("wrap_http_exceptions", False) - http_strategy = kwargs.get("http_strategy", None) + self.requests_session = requests.Session() + # Work around known proxy bug (see https://github.com/psf/requests/issues/5677) + self.requests_session.proxies.update(requests.utils.getproxies()) if http_strategy: self._http_strategy = http_strategy(self, self.environment) @@ -119,7 +122,7 @@ def graphql_base_url(self): return self.environment.protocol + self.environment.graphql_server_and_port + "/graphql" def http(self): - return braintree.util.http.Http(self) + return braintree.util.http.Http(self, requests_session=self.requests_session) def graphql_client(self): return GraphQLClient(self) diff --git a/braintree/util/http.py b/braintree/util/http.py index 131b36dd..79dcb283 100644 --- a/braintree/util/http.py +++ b/braintree/util/http.py @@ -55,9 +55,10 @@ def raise_exception_from_status(status, message=None): else: raise UnexpectedError("Unexpected HTTP_RESPONSE " + str(status)) - def __init__(self, config, environment=None): + def __init__(self, config, environment=None, requests_session=None): self.config = config self.environment = environment or self.config.environment + self.requests_session = requests_session or requests.Session() def post(self, path, params=None): return self._make_request("POST", path, Http.ContentType.Xml, params) @@ -100,35 +101,26 @@ def _make_request(self, http_verb, path, content_type, params=None, files=None, return XmlUtil.dict_from_xml(response_body) def http_do(self, http_verb, path, headers, request_body): - data = request_body - files = None + data, files = request_body, None full_path = self.__full_path(path) - if type(request_body) is tuple: - data = request_body[0] - files = request_body[1] + if isinstance(request_body, tuple): + data, files = request_body[0], request_body[1] if (self.config.environment == Environment.Development): verify = False else: verify = self.environment.ssl_certificate - with requests.Session() as session: - request = requests.Request( - method=http_verb, - url=full_path, - headers=headers, - data=data, - files=files) - prepared_request = request.prepare() - prepared_request.url = full_path - # there's a bug in requests module that requires we manually update proxy settings, - # see https://github.com/psf/requests/issues/5677 - session.proxies.update(requests.utils.getproxies()) - - response = session.send(prepared_request, - verify=verify, - timeout=self.config.timeout) + response = self.requests_session.request( + method=http_verb, + url=full_path, + headers=headers, + data=data, + files=files, + verify=verify, + timeout=self.config.timeout, + ) return [response.status_code, response.text] @@ -174,7 +166,8 @@ def __headers(self, content_type, header_overrides=None): if content_type == Http.ContentType.Xml: headers["Content-type"] = Http.ContentType.Xml - headers.update(header_overrides or {}) + if header_overrides is not None: + headers.update(header_overrides) return headers diff --git a/tests/unit/test_http.py b/tests/unit/test_http.py index 600086aa..a31d4082 100644 --- a/tests/unit/test_http.py +++ b/tests/unit/test_http.py @@ -140,33 +140,16 @@ def test_http_do_strategy(http_verb, path, headers, request_body): http.handle_exception(requests.exceptions.ConnectTimeout()) def test_request_urls_retain_dots(self): - with patch('requests.Session.send') as send: + config = Configuration( + Environment.Development, + "integration_merchant_id", + public_key="integration_public_key", + private_key="integration_private_key", + wrap_http_exceptions=True + ) + with patch.object(config.requests_session, 'request') as send: send.return_value.status_code = 200 - config = Configuration( - Environment.Development, - "integration_merchant_id", - public_key="integration_public_key", - private_key="integration_private_key", - wrap_http_exceptions=True - ) http = config.http() http.get("/../../customers/") - - prepared_request = send.call_args[0][0] - request_url = prepared_request.url + request_url = send.call_args[1]['url'] self.assertTrue(request_url.endswith("/../../customers/")) - - def test_sessions_close_after_request(self): - with patch('requests.Session.send') as send, patch('requests.Session.close') as close: - send.return_value.status_code = 200 - config = Configuration( - Environment.Development, - "integration_merchant_id", - public_key="integration_public_key", - private_key="integration_private_key", - wrap_http_exceptions=True - ) - http = config.http() - http.get("/../../customers/") - - self.assertTrue(close.called)