Skip to content

Commit 7931e93

Browse files
authored
Migrate from requests to httpx (#2)
* Migrate from requests to httpx for improved performance and add support for request overloads * Update coverage * Update Python version to 3.13 in workflows and metadata
1 parent 3c17ff3 commit 7931e93

File tree

7 files changed

+267
-237
lines changed

7 files changed

+267
-237
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: Set up Python
1616
uses: actions/setup-python@v5
1717
with:
18-
python-version: "3.9"
18+
python-version: "3.13"
1919

2020
- name: Install build tools
2121
run: |

.github/workflows/test-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- name: Set up Python
2222
uses: actions/setup-python@v5
2323
with:
24-
python-version: "3.11"
24+
python-version: "3.13"
2525

2626
- name: Install uv
2727
uses: astral-sh/setup-uv@v3

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
os: [ubuntu-latest, windows-latest, macOS-latest]
16-
python-version: ["3.8", "3.9", "3.10", "3.11"]
16+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1717

1818
steps:
1919
- name: Check out repository
@@ -42,7 +42,7 @@ jobs:
4242
uv run pytest tests/ -v --cov=tempmail --cov-report=xml --cov-report=term-missing
4343
4444
- name: Upload coverage to Codecov
45-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
45+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
4646
uses: codecov/codecov-action@v5
4747
with:
4848
files: ./coverage.xml

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ 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",
1717
"Programming Language :: Python :: 3.8",
1818
"Programming Language :: Python :: 3.9",
1919
"Programming Language :: Python :: 3.10",
2020
"Programming Language :: Python :: 3.11",
21+
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
2123
"License :: OSI Approved :: MIT License",
2224
"Operating System :: OS Independent",
2325
"Development Status :: 5 - Production/Stable",
@@ -32,7 +34,6 @@ dev = [
3234
"black>=21.0.0",
3335
"isort>=5.0.0",
3436
"mypy>=0.800",
35-
"types-requests",
3637
"setuptools",
3738
]
3839

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()

0 commit comments

Comments
 (0)