Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: "3.13"

- name: Install build tools
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.13"

- name: Install uv
uses: astral-sh/setup-uv@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- name: Check out repository
Expand Down Expand Up @@ -42,7 +42,7 @@ jobs:
uv run pytest tests/ -v --cov=tempmail --cov-report=xml --cov-report=term-missing

- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ readme = "README.md"
keywords = ["temp-mail", "temp-mail-api", "tempmail", "temp-mail-io"]
requires-python = ">=3.8"
dependencies = [
"requests>=2.25.0",
"httpx>=0.25.0, <1",
]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Development Status :: 5 - Production/Stable",
Expand All @@ -32,7 +34,6 @@ dev = [
"black>=21.0.0",
"isort>=5.0.0",
"mypy>=0.800",
"types-requests",
"setuptools",
]

Expand Down
79 changes: 62 additions & 17 deletions tempmail/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Temp Mail API client implementation."""

import typing
from typing import Optional, List, Dict, Any
from typing import Optional, List, Dict, Any, overload, Literal
from urllib.parse import urljoin
import requests
import httpx

from . import __version__
from .models import (
Expand Down Expand Up @@ -35,17 +35,39 @@ def __init__(
self.base_url = base_url
self.timeout = timeout

self.session = requests.Session()
self.session.headers.update(
{
self.client = httpx.Client(
headers={
"X-API-Key": api_key,
"Content-Type": "application/json",
"User-Agent": f"temp-mail-python/{__version__}",
}
},
timeout=timeout,
)

self._last_rate_limit: Optional[RateLimit] = None

@overload
def _make_request(
self,
method: str,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None,
return_content: Literal[True] = ...,
update_rate_limit: bool = True,
) -> bytes: ...

@overload
def _make_request(
self,
method: str,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None,
return_content: Literal[False] = ...,
update_rate_limit: bool = True,
) -> Dict[str, Any]: ...

def _make_request(
self,
method: str,
Expand All @@ -66,12 +88,11 @@ def _make_request(
url = urljoin(self.base_url, endpoint)

try:
response = self.session.request(
response = self.client.request(
method=method,
url=url,
params=params,
json=json_data,
timeout=self.timeout,
)

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

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

data = self._make_request(
"POST", "/v1/emails", json_data=json_data if json_data else None
"POST",
"/v1/emails",
json_data=json_data if json_data else None,
return_content=False,
)

return EmailAddress.from_json(data)
Expand All @@ -137,7 +161,7 @@ def list_domains(self) -> List[Domain]:
Returns:
List[Domain]: Available domains
"""
data = self._make_request("GET", "/v1/domains")
data = self._make_request("GET", "/v1/domains", return_content=False)

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

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

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

def get_message(self, message_id: str) -> EmailMessage:
"""Get a specific message by ID."""
data = self._make_request("GET", f"/v1/messages/{message_id}")
data = self._make_request(
"GET", f"/v1/messages/{message_id}", return_content=False
)
return EmailMessage.from_json(data)

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

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

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

def download_attachment(self, attachment_id: str) -> bytes:
Expand All @@ -184,7 +214,9 @@ def get_rate_limit(self) -> RateLimit:
Get current rate limit information.
:return: RateLimit object
"""
data = self._make_request("GET", "/v1/rate_limit", update_rate_limit=False)
data = self._make_request(
"GET", "/v1/rate_limit", return_content=False, update_rate_limit=False
)
rate_limit: RateLimit = RateLimit.from_json(data)
# Also update the last known rate limit since this method doesn't use headers
self._last_rate_limit = rate_limit
Expand All @@ -197,3 +229,16 @@ def last_rate_limit(self) -> Optional[RateLimit]:
It will be None if no requests have been made yet.
"""
return self._last_rate_limit

def close(self) -> None:
"""Close the underlying HTTPX client.

The client will *not* be usable after this.
"""
self.client.close()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.close()
Loading
Loading