Skip to content

Commit 0172cf4

Browse files
authored
Use requests Session for persistent HTTPS connections (#135)
hcloud.Client now reuses the underlying TCP connection instead of establishing a new connection on every request. This was initially found in #123. This commit superseeds the mentioned MR and fixes the tests. Signed-off-by: Lukas Kämmerling <lukas.kaemmerling@hetzner-cloud.de>
1 parent 256d4fd commit 0172cf4

2 files changed

Lines changed: 30 additions & 24 deletions

File tree

hcloud/hcloud.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def __init__(self, token, api_endpoint="https://api.hetzner.cloud/v1", applicati
5959
self._api_endpoint = api_endpoint
6060
self._application_name = application_name
6161
self._application_version = application_version
62+
self._requests_session = requests.Session()
6263
self.poll_interval = poll_interval
6364

6465
self.datacenters = DatacentersClient(self)
@@ -195,7 +196,7 @@ def request(self, method, url, tries=1, **kwargs):
195196
:return: Response
196197
:rtype: requests.Response
197198
"""
198-
response = requests.request(
199+
response = self._requests_session.request(
199200
method,
200201
self._api_endpoint + url,
201202
headers=self._get_headers(),

tests/unit/test_hcloud.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33
import json
4+
from unittest.mock import MagicMock
5+
46
import requests
57
import pytest
68
from hcloud import Client, APIException
@@ -11,7 +13,10 @@ class TestHetznerClient(object):
1113
@pytest.fixture()
1214
def client(self):
1315
Client._version = '0.0.0'
14-
return Client(token="project_token")
16+
client = Client(token="project_token")
17+
18+
client._requests_session = MagicMock()
19+
return client
1520

1621
@pytest.fixture()
1722
def response(self):
@@ -76,61 +81,61 @@ def test_request_library_mocked(self, client):
7681
response = client.request("POST", "url", params={"1": 2})
7782
assert response.__class__.__name__ == 'MagicMock'
7883

79-
def test_request_ok(self, mocked_requests, client, response):
80-
mocked_requests.request.return_value = response
84+
def test_request_ok(self, client, response):
85+
client._requests_session.request.return_value = response
8186
response = client.request("POST", "/servers", params={"argument": "value"}, timeout=2)
82-
mocked_requests.request.assert_called_once()
83-
assert mocked_requests.request.call_args[0] == ('POST', 'https://api.hetzner.cloud/v1/servers')
84-
assert mocked_requests.request.call_args[1]['params'] == {'argument': 'value'}
85-
assert mocked_requests.request.call_args[1]['timeout'] == 2
87+
client._requests_session.request.assert_called_once()
88+
assert client._requests_session.request.call_args[0] == ('POST', 'https://api.hetzner.cloud/v1/servers')
89+
assert client._requests_session.request.call_args[1]['params'] == {'argument': 'value'}
90+
assert client._requests_session.request.call_args[1]['timeout'] == 2
8691
assert response == {"result": "data"}
8792

88-
def test_request_fails(self, mocked_requests, client, fail_response):
89-
mocked_requests.request.return_value = fail_response
93+
def test_request_fails(self, client, fail_response):
94+
client._requests_session.request.return_value = fail_response
9095
with pytest.raises(APIException) as exception_info:
9196
client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
9297
error = exception_info.value
9398
assert error.code == "invalid_input"
9499
assert error.message == "invalid input in field 'broken_field': is too long"
95100
assert error.details['fields'][0]['name'] == "broken_field"
96101

97-
def test_request_500(self, mocked_requests, client, fail_response):
102+
def test_request_500(self, client, fail_response):
98103
fail_response.status_code = 500
99104
fail_response.reason = "Internal Server Error"
100105
fail_response._content = "Internal Server Error"
101-
mocked_requests.request.return_value = fail_response
106+
client._requests_session.request.return_value = fail_response
102107
with pytest.raises(APIException) as exception_info:
103108
client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
104109
error = exception_info.value
105110
assert error.code == 500
106111
assert error.message == "Internal Server Error"
107112
assert error.details['content'] == "Internal Server Error"
108113

109-
def test_request_broken_json_200(self, mocked_requests, client, response):
114+
def test_request_broken_json_200(self, client, response):
110115
content = "{'key': 'value'".encode('utf-8')
111116
response.reason = "OK"
112117
response._content = content
113-
mocked_requests.request.return_value = response
118+
client._requests_session.request.return_value = response
114119
with pytest.raises(APIException) as exception_info:
115120
client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
116121
error = exception_info.value
117122
assert error.code == 200
118123
assert error.message == "OK"
119124
assert error.details['content'] == content
120125

121-
def test_request_empty_content_200(self, mocked_requests, client, response):
126+
def test_request_empty_content_200(self, client, response):
122127
content = ""
123128
response.reason = "OK"
124129
response._content = content
125-
mocked_requests.request.return_value = response
130+
client._requests_session.request.return_value = response
126131
response = client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
127132
assert response == ""
128133

129-
def test_request_500_empty_content(self, mocked_requests, client, fail_response):
134+
def test_request_500_empty_content(self, client, fail_response):
130135
fail_response.status_code = 500
131136
fail_response.reason = "Internal Server Error"
132137
fail_response._content = ""
133-
mocked_requests.request.return_value = fail_response
138+
client._requests_session.request.return_value = fail_response
134139
with pytest.raises(APIException) as exception_info:
135140
client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
136141
error = exception_info.value
@@ -139,22 +144,22 @@ def test_request_500_empty_content(self, mocked_requests, client, fail_response)
139144
assert error.details["content"] == ""
140145
assert str(error) == "Internal Server Error"
141146

142-
def test_request_limit(self, mocked_requests, client, rate_limit_response):
147+
def test_request_limit(self, client, rate_limit_response):
143148
client._retry_wait_time = 0
144-
mocked_requests.request.return_value = rate_limit_response
149+
client._requests_session.request.return_value = rate_limit_response
145150
with pytest.raises(APIException) as exception_info:
146151
client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
147152
error = exception_info.value
148-
assert mocked_requests.request.call_count == 5
153+
assert client._requests_session.request.call_count == 5
149154
assert error.code == "rate_limit_exceeded"
150155
assert error.message == "limit of 10 requests per hour reached"
151156

152-
def test_request_limit_then_success(self, mocked_requests, client, rate_limit_response):
157+
def test_request_limit_then_success(self, client, rate_limit_response):
153158
client._retry_wait_time = 0
154159
response = requests.Response()
155160
response.status_code = 200
156161
response._content = json.dumps({"result": "data"}).encode('utf-8')
157-
mocked_requests.request.side_effect = [rate_limit_response, response]
162+
client._requests_session.request.side_effect = [rate_limit_response, response]
158163

159164
client.request("POST", "http://url.com", params={"argument": "value"}, timeout=2)
160-
assert mocked_requests.request.call_count == 2
165+
assert client._requests_session.request.call_count == 2

0 commit comments

Comments
 (0)