Skip to content

Commit bbc33b1

Browse files
committed
Migrate from requests to httpx for improved performance and add support for request overloads
1 parent 3c17ff3 commit bbc33b1

File tree

4 files changed

+170
-233
lines changed

4 files changed

+170
-233
lines changed

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ readme = "README.md"
1010
keywords = ["temp-mail", "temp-mail-api", "tempmail", "temp-mail-io"]
1111
requires-python = ">=3.8"
1212
dependencies = [
13-
"requests>=2.25.0",
13+
"httpx>=0.25.0, <1",
1414
]
1515
classifiers = [
1616
"Programming Language :: Python :: 3",
@@ -32,7 +32,6 @@ dev = [
3232
"black>=21.0.0",
3333
"isort>=5.0.0",
3434
"mypy>=0.800",
35-
"types-requests",
3635
"setuptools",
3736
]
3837

tempmail/client.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Temp Mail API client implementation."""
22

33
import typing
4-
from typing import Optional, List, Dict, Any
4+
from typing import Optional, List, Dict, Any, overload, Literal
55
from urllib.parse import urljoin
6-
import requests
6+
import httpx
77

88
from . import __version__
99
from .models import (
@@ -35,17 +35,39 @@ def __init__(
3535
self.base_url = base_url
3636
self.timeout = timeout
3737

38-
self.session = requests.Session()
39-
self.session.headers.update(
40-
{
38+
self.client = httpx.Client(
39+
headers={
4140
"X-API-Key": api_key,
4241
"Content-Type": "application/json",
4342
"User-Agent": f"temp-mail-python/{__version__}",
44-
}
43+
},
44+
timeout=timeout,
4545
)
4646

4747
self._last_rate_limit: Optional[RateLimit] = None
4848

49+
@overload
50+
def _make_request(
51+
self,
52+
method: str,
53+
endpoint: str,
54+
params: Optional[Dict[str, Any]] = None,
55+
json_data: Optional[Dict[str, Any]] = None,
56+
return_content: Literal[True] = ...,
57+
update_rate_limit: bool = True,
58+
) -> bytes: ...
59+
60+
@overload
61+
def _make_request(
62+
self,
63+
method: str,
64+
endpoint: str,
65+
params: Optional[Dict[str, Any]] = None,
66+
json_data: Optional[Dict[str, Any]] = None,
67+
return_content: Literal[False] = ...,
68+
update_rate_limit: bool = True,
69+
) -> Dict[str, Any]: ...
70+
4971
def _make_request(
5072
self,
5173
method: str,
@@ -66,12 +88,11 @@ def _make_request(
6688
url = urljoin(self.base_url, endpoint)
6789

6890
try:
69-
response = self.session.request(
91+
response = self.client.request(
7092
method=method,
7193
url=url,
7294
params=params,
7395
json=json_data,
74-
timeout=self.timeout,
7596
)
7697

7798
if 200 <= response.status_code < 300:
@@ -92,7 +113,7 @@ def _make_request(
92113
raise ValidationError(api_response.detail)
93114
else:
94115
raise TempMailError(api_response.detail)
95-
except requests.exceptions.RequestException as e:
116+
except httpx.RequestError as e:
96117
raise TempMailError(f"Request failed: {str(e)}")
97118

98119
def _update_rate_limit_from_headers(self, headers: Any) -> None:
@@ -125,7 +146,10 @@ def create_email(
125146
json_data["domain_type"] = domain_type.value
126147

127148
data = self._make_request(
128-
"POST", "/v1/emails", json_data=json_data if json_data else None
149+
"POST",
150+
"/v1/emails",
151+
json_data=json_data if json_data else None,
152+
return_content=False,
129153
)
130154

131155
return EmailAddress.from_json(data)
@@ -137,7 +161,7 @@ def list_domains(self) -> List[Domain]:
137161
Returns:
138162
List[Domain]: Available domains
139163
"""
140-
data = self._make_request("GET", "/v1/domains")
164+
data = self._make_request("GET", "/v1/domains", return_content=False)
141165

142166
return [Domain.from_json(domain) for domain in data["domains"]]
143167

@@ -146,7 +170,9 @@ def list_email_messages(
146170
email: str,
147171
) -> List[EmailMessage]:
148172
"""Get all messages for a specific email address."""
149-
data = self._make_request("GET", f"/v1/emails/{email}/messages")
173+
data = self._make_request(
174+
"GET", f"/v1/emails/{email}/messages", return_content=False
175+
)
150176

151177
messages = []
152178
for msg_data in data["messages"]:
@@ -156,20 +182,24 @@ def list_email_messages(
156182

157183
def get_message(self, message_id: str) -> EmailMessage:
158184
"""Get a specific message by ID."""
159-
data = self._make_request("GET", f"/v1/messages/{message_id}")
185+
data = self._make_request(
186+
"GET", f"/v1/messages/{message_id}", return_content=False
187+
)
160188
return EmailMessage.from_json(data)
161189

162190
def delete_message(self, message_id: str) -> None:
163191
"""Delete a specific message by ID."""
164-
self._make_request("DELETE", f"/v1/messages/{message_id}")
192+
self._make_request("DELETE", f"/v1/messages/{message_id}", return_content=False)
165193

166194
def delete_email(self, email: str) -> None:
167195
"""Delete an email address and all its messages."""
168-
self._make_request("DELETE", f"/v1/emails/{email}")
196+
self._make_request("DELETE", f"/v1/emails/{email}", return_content=False)
169197

170198
def get_message_source_code(self, message_id: str) -> str:
171199
"""Get the raw source code of a message."""
172-
data = self._make_request("GET", f"/v1/messages/{message_id}/source_code")
200+
data = self._make_request(
201+
"GET", f"/v1/messages/{message_id}/source_code", return_content=False
202+
)
173203
return data["data"]
174204

175205
def download_attachment(self, attachment_id: str) -> bytes:
@@ -184,7 +214,9 @@ def get_rate_limit(self) -> RateLimit:
184214
Get current rate limit information.
185215
:return: RateLimit object
186216
"""
187-
data = self._make_request("GET", "/v1/rate_limit", update_rate_limit=False)
217+
data = self._make_request(
218+
"GET", "/v1/rate_limit", return_content=False, update_rate_limit=False
219+
)
188220
rate_limit: RateLimit = RateLimit.from_json(data)
189221
# Also update the last known rate limit since this method doesn't use headers
190222
self._last_rate_limit = rate_limit
@@ -197,3 +229,16 @@ def last_rate_limit(self) -> Optional[RateLimit]:
197229
It will be None if no requests have been made yet.
198230
"""
199231
return self._last_rate_limit
232+
233+
def close(self) -> None:
234+
"""Close the underlying HTTPX client.
235+
236+
The client will *not* be usable after this.
237+
"""
238+
self.client.close()
239+
240+
def __enter__(self):
241+
return self
242+
243+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
244+
self.close()

tests/test_client.py

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
ValidationError,
1414
TempMailError,
1515
)
16-
from requests.exceptions import ConnectionError
16+
from httpx import ConnectError
1717
from tempmail.models import DomainType, RateLimit, Attachment
1818

1919

@@ -28,7 +28,7 @@ class TestTempMailClient:
2828
def test_client_initialization(self) -> None:
2929
client = TempMailClient("test-api-key")
3030
assert client.api_key == "test-api-key"
31-
assert client.session.headers["X-API-Key"] == "test-api-key"
31+
assert client.client.headers["X-API-Key"] == "test-api-key"
3232

3333
def test_client_initialization_with_custom_params(self) -> None:
3434
client = TempMailClient(
@@ -37,7 +37,7 @@ def test_client_initialization_with_custom_params(self) -> None:
3737
assert client.base_url == "https://custom.api.com"
3838
assert client.timeout == 60
3939

40-
@patch("tempmail.client.requests.Session.request")
40+
@patch("tempmail.client.httpx.Client.request")
4141
def test_create_email_success(self, mock_request) -> None:
4242
mock_response = Mock()
4343
mock_response.status_code = 200
@@ -49,7 +49,7 @@ def test_create_email_success(self, mock_request) -> None:
4949
email: EmailAddress = client.create_email()
5050
assert email == EmailAddress(email="[email protected]", ttl=86400)
5151

52-
@patch("tempmail.client.requests.Session.request")
52+
@patch("tempmail.client.httpx.Client.request")
5353
def test_create_email_premium_domain_type(self, mock_request) -> None:
5454
mock_response = Mock()
5555
mock_response.status_code = 200
@@ -66,10 +66,9 @@ def test_create_email_premium_domain_type(self, mock_request) -> None:
6666
url="https://api.temp-mail.io/v1/emails",
6767
params=None,
6868
json={"domain_type": "premium"},
69-
timeout=30,
7069
)
7170

72-
@patch("tempmail.client.requests.Session.request")
71+
@patch("tempmail.client.httpx.Client.request")
7372
def test_create_email_with_options(self, mock_request):
7473
mock_response = Mock()
7574
mock_response.status_code = 200
@@ -87,10 +86,9 @@ def test_create_email_with_options(self, mock_request):
8786
url="https://api.temp-mail.io/v1/emails",
8887
params=None,
8988
json={"domain": "mydomain.com"},
90-
timeout=30,
9189
)
9290

93-
@patch("tempmail.client.requests.Session.request")
91+
@patch("tempmail.client.httpx.Client.request")
9492
def test_list_domains_success(self, mock_request) -> None:
9593
mock_response = Mock()
9694
mock_response.status_code = 200
@@ -121,7 +119,7 @@ def test_list_domains_success(self, mock_request) -> None:
121119
Domain(name="example.io", type=DomainType.PREMIUM),
122120
]
123121

124-
@patch("tempmail.client.requests.Session.request")
122+
@patch("tempmail.client.httpx.Client.request")
125123
def test_list_email_messages_success(self, mock_request):
126124
mock_response = Mock()
127125
mock_response.status_code = 200
@@ -175,7 +173,7 @@ def test_list_email_messages_success(self, mock_request):
175173
],
176174
)
177175

178-
@patch("tempmail.client.requests.Session.request")
176+
@patch("tempmail.client.httpx.Client.request")
179177
def test_list_email_messages_no_attachments(self, mock_request):
180178
mock_response = Mock()
181179
mock_response.status_code = 200
@@ -215,7 +213,7 @@ def test_list_email_messages_no_attachments(self, mock_request):
215213
attachments=[],
216214
)
217215

218-
@patch("tempmail.client.requests.Session.request")
216+
@patch("tempmail.client.httpx.Client.request")
219217
def test_list_email_messages_empty(self, mock_request):
220218
mock_response = Mock()
221219
mock_response.status_code = 200
@@ -232,10 +230,9 @@ def test_list_email_messages_empty(self, mock_request):
232230
url="https://api.temp-mail.io/v1/emails/[email protected]/messages",
233231
params=None,
234232
json=None,
235-
timeout=30,
236233
)
237234

238-
@patch("tempmail.client.requests.Session.request")
235+
@patch("tempmail.client.httpx.Client.request")
239236
def test_get_message_success(self, mock_request):
240237
mock_response = Mock()
241238
mock_response.status_code = 200
@@ -274,10 +271,9 @@ def test_get_message_success(self, mock_request):
274271
url="https://api.temp-mail.io/v1/messages/msg1",
275272
params=None,
276273
json=None,
277-
timeout=30,
278274
)
279275

280-
@patch("tempmail.client.requests.Session.request")
276+
@patch("tempmail.client.httpx.Client.request")
281277
def test_delete_message_success(self, mock_request):
282278
mock_response = Mock()
283279
mock_response.status_code = 200
@@ -293,10 +289,9 @@ def test_delete_message_success(self, mock_request):
293289
url="https://api.temp-mail.io/v1/messages/msg123",
294290
params=None,
295291
json=None,
296-
timeout=30,
297292
)
298293

299-
@patch("tempmail.client.requests.Session.request")
294+
@patch("tempmail.client.httpx.Client.request")
300295
def test_delete_email_success(self, mock_request):
301296
mock_response = Mock()
302297
mock_response.status_code = 200
@@ -312,10 +307,9 @@ def test_delete_email_success(self, mock_request):
312307
url="https://api.temp-mail.io/v1/emails/[email protected]",
313308
params=None,
314309
json=None,
315-
timeout=30,
316310
)
317311

318-
@patch("tempmail.client.requests.Session.request")
312+
@patch("tempmail.client.httpx.Client.request")
319313
def test_get_message_source_code_success(self, mock_request):
320314
mock_response = Mock()
321315
mock_response.status_code = 200
@@ -336,10 +330,9 @@ def test_get_message_source_code_success(self, mock_request):
336330
url="https://api.temp-mail.io/v1/messages/msg1/source_code",
337331
params=None,
338332
json=None,
339-
timeout=30,
340333
)
341334

342-
@patch("tempmail.client.requests.Session.request")
335+
@patch("tempmail.client.httpx.Client.request")
343336
def test_download_attachment_success(self, mock_request):
344337
mock_response = Mock()
345338
mock_response.status_code = 200
@@ -357,10 +350,9 @@ def test_download_attachment_success(self, mock_request):
357350
url="https://api.temp-mail.io/v1/attachments/attachment1",
358351
params=None,
359352
json=None,
360-
timeout=30,
361353
)
362354

363-
@patch("tempmail.client.requests.Session.request")
355+
@patch("tempmail.client.httpx.Client.request")
364356
def test_authentication_error(self, mock_request):
365357
mock_response = Mock()
366358
mock_response.status_code = 400
@@ -379,7 +371,7 @@ def test_authentication_error(self, mock_request):
379371
with pytest.raises(AuthenticationError, match="API token is invalid"):
380372
client.create_email()
381373

382-
@patch("tempmail.client.requests.Session.request")
374+
@patch("tempmail.client.httpx.Client.request")
383375
def test_rate_limit_error(self, mock_request):
384376
mock_response = Mock()
385377
mock_response.status_code = 429
@@ -401,7 +393,7 @@ def test_rate_limit_error(self, mock_request):
401393
):
402394
client.create_email()
403395

404-
@patch("tempmail.client.requests.Session.request")
396+
@patch("tempmail.client.httpx.Client.request")
405397
def test_validation_error(self, mock_request):
406398
mock_response = Mock()
407399
mock_response.status_code = 400
@@ -420,7 +412,7 @@ def test_validation_error(self, mock_request):
420412
with pytest.raises(ValidationError, match="Invalid domain name"):
421413
client.create_email(domain="invalid_domain")
422414

423-
@patch("tempmail.client.requests.Session.request")
415+
@patch("tempmail.client.httpx.Client.request")
424416
def test_api_error(self, mock_request):
425417
mock_response = Mock()
426418
mock_response.status_code = 500
@@ -439,7 +431,7 @@ def test_api_error(self, mock_request):
439431
with pytest.raises(TempMailError, match="Internal server error"):
440432
client.create_email()
441433

442-
@patch("tempmail.client.requests.Session.request")
434+
@patch("tempmail.client.httpx.Client.request")
443435
def test_get_rate_limit_success(self, mock_request):
444436
mock_response = Mock()
445437
mock_response.status_code = 200
@@ -470,9 +462,9 @@ def test_get_rate_limit_success(self, mock_request):
470462
limit=100, remaining=95, used=5, reset=1640995200
471463
)
472464

473-
@patch("tempmail.client.requests.Session.request")
465+
@patch("tempmail.client.httpx.Client.request")
474466
def test_request_exception(self, mock_request):
475-
mock_request.side_effect = ConnectionError("Connection failed")
467+
mock_request.side_effect = ConnectError("Connection failed")
476468

477469
client = TempMailClient("test-api-key")
478470
with pytest.raises(TempMailError, match="Request failed"):

0 commit comments

Comments
 (0)