From d11b71c4893037cc8ef9689abf1f79491281aef3 Mon Sep 17 00:00:00 2001 From: nomer77 Date: Fri, 19 Jun 2026 01:29:01 +0300 Subject: [PATCH] feature: add async client --- pyproject.toml | 4 + tempmail/__init__.py | 2 + tempmail/async_client.py | 246 ++++++++++++++++ tests/test_async_client.py | 570 +++++++++++++++++++++++++++++++++++++ 4 files changed, 822 insertions(+) create mode 100644 tempmail/async_client.py create mode 100644 tests/test_async_client.py diff --git a/pyproject.toml b/pyproject.toml index 9f93120..fb786cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ [project.optional-dependencies] dev = [ "pytest>=6.0.0", + "pytest-asyncio>=0.21.0", "pytest-cov>=2.10.0", "black>=21.0.0", "isort>=5.0.0", @@ -37,6 +38,9 @@ dev = [ "setuptools", ] +[tool.pytest.ini_options] +asyncio_mode = "auto" + [project.urls] Source = "https://github.com/temp-mail-io/temp-mail-python" diff --git a/tempmail/__init__.py b/tempmail/__init__.py index 65e360f..b989690 100644 --- a/tempmail/__init__.py +++ b/tempmail/__init__.py @@ -5,6 +5,7 @@ __version__ = "1.0.1" from .client import TempMailClient +from .async_client import AsyncTempMailClient from .models import ( RateLimit, Domain, @@ -21,6 +22,7 @@ __all__ = [ "TempMailClient", + "AsyncTempMailClient", "RateLimit", "Domain", "EmailAddress", diff --git a/tempmail/async_client.py b/tempmail/async_client.py new file mode 100644 index 0000000..c1a39a2 --- /dev/null +++ b/tempmail/async_client.py @@ -0,0 +1,246 @@ +"""Temp Mail API asynchronous client implementation.""" + +import typing +from typing import Optional, List, Dict, Any, overload, Literal +from urllib.parse import urljoin +import httpx + +from . import __version__ +from .models import ( + RateLimit, + Domain, + DomainType, + EmailAddress, + EmailMessage, + APIErrorResponse, +) +from .exceptions import ( + TempMailError, + AuthenticationError, + RateLimitError, + ValidationError, +) + + +class AsyncTempMailClient: + """Asynchronous client for interacting with the Temp Mail API.""" + + def __init__( + self, + api_key: str, + base_url: str = "https://api.temp-mail.io", + timeout: int = 30, + ): + self.api_key = api_key + self.base_url = base_url + self.timeout = timeout + + self.client = httpx.AsyncClient( + 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 + async 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 + async 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]: ... + + async def _make_request( + self, + method: str, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + json_data: Optional[Dict[str, Any]] = None, + return_content: bool = False, + update_rate_limit: bool = True, + ) -> typing.Union[Dict[str, Any], bytes]: + """ + Make an HTTP request to the API. + :param method: HTTP method (GET, POST, DELETE, etc.) + :param endpoint: API endpoint (e.g., "/v1/emails") + :param params: Query parameters + :param json_data: JSON body for POST/PUT requests + :param return_content: If True, return raw response content instead of JSON + """ + url = urljoin(self.base_url, endpoint) + + try: + response = await self.client.request( + method=method, + url=url, + params=params, + json=json_data, + ) + + if 200 <= response.status_code < 300: + if update_rate_limit: + self._update_rate_limit_from_headers(response.headers) + if return_content: + return response.content + return response.json() + else: + api_response: APIErrorResponse = APIErrorResponse.from_json( + response.json() + ) + if api_response.is_api_key_error(): + raise AuthenticationError(api_response.detail) + elif api_response.is_rate_limit_error(): + raise RateLimitError(api_response.detail) + elif api_response.is_validation_error(): + raise ValidationError(api_response.detail) + else: + raise TempMailError(api_response.detail) + except httpx.RequestError as e: + raise TempMailError(f"Request failed: {str(e)}") + + def _update_rate_limit_from_headers(self, headers: Any) -> None: + """Update rate limit info from response headers.""" + self._last_rate_limit = RateLimit( + limit=int(headers["X-Ratelimit-Limit"]), + remaining=int(headers["X-Ratelimit-Remaining"]), + reset=int(headers["X-Ratelimit-Reset"]), + used=int(headers["X-Ratelimit-Used"]), + ) + + async def create_email( + self, + email: Optional[str] = None, + domain: Optional[str] = None, + domain_type: Optional[DomainType] = None, + ) -> EmailAddress: + """ + Create a new temporary email address. + :param email: Optional specific email address to create + :param domain: Optional domain to use + :param domain_type: an Optional domain type + """ + json_data: Dict[str, Any] = {} + if email: + json_data["email"] = email + if domain: + json_data["domain"] = domain + if domain_type: + json_data["domain_type"] = domain_type.value + + data = await self._make_request( + "POST", + "/v1/emails", + json_data=json_data if json_data else None, + return_content=False, + ) + + return EmailAddress.from_json(data) + + async def list_domains(self) -> List[Domain]: + """ + Get a list of available email domains. + + Returns: + List[Domain]: Available domains + """ + data = await self._make_request("GET", "/v1/domains", return_content=False) + + return [Domain.from_json(domain) for domain in data["domains"]] + + async def list_email_messages( + self, + email: str, + ) -> List[EmailMessage]: + """Get all messages for a specific email address.""" + data = await self._make_request( + "GET", f"/v1/emails/{email}/messages", return_content=False + ) + + messages = [] + for msg_data in data["messages"]: + messages.append(EmailMessage.from_json(msg_data)) + + return messages + + async def get_message(self, message_id: str) -> EmailMessage: + """Get a specific message by ID.""" + data = await self._make_request( + "GET", f"/v1/messages/{message_id}", return_content=False + ) + return EmailMessage.from_json(data) + + async def delete_message(self, message_id: str) -> None: + """Delete a specific message by ID.""" + await self._make_request( + "DELETE", f"/v1/messages/{message_id}", return_content=False + ) + + async def delete_email(self, email: str) -> None: + """Delete an email address and all its messages.""" + await self._make_request("DELETE", f"/v1/emails/{email}", return_content=False) + + async def get_message_source_code(self, message_id: str) -> str: + """Get the raw source code of a message.""" + data = await self._make_request( + "GET", f"/v1/messages/{message_id}/source_code", return_content=False + ) + return data["data"] + + async def download_attachment(self, attachment_id: str) -> bytes: + """Download an attachment by ID.""" + content: bytes = await self._make_request( + "GET", f"/v1/attachments/{attachment_id}", return_content=True + ) + return content + + async def get_rate_limit(self) -> RateLimit: + """ + Get current rate limit information. + :return: RateLimit object + """ + data = await 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 + return rate_limit + + @property + def last_rate_limit(self) -> Optional[RateLimit]: + """ + Get the last known rate limit information. + It will be None if no requests have been made yet. + """ + return self._last_rate_limit + + async def close(self) -> None: + """Close the underlying HTTPX async client. + + The client will *not* be usable after this. + """ + await self.client.aclose() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + await self.close() diff --git a/tests/test_async_client.py b/tests/test_async_client.py new file mode 100644 index 0000000..550af69 --- /dev/null +++ b/tests/test_async_client.py @@ -0,0 +1,570 @@ +import datetime +import typing +import pytest + +from httpx import TimeoutException +from unittest.mock import Mock, AsyncMock, patch +from tempmail import ( + AsyncTempMailClient, + EmailAddress, + Domain, + EmailMessage, + AuthenticationError, + RateLimitError, + ValidationError, + TempMailError, +) +from httpx import ConnectError +from tempmail.models import DomainType, RateLimit, Attachment + + +class TestAsyncTempMailClient: + _rate_limit_headers: typing.Dict[str, str] = { + "X-Ratelimit-Limit": "100", + "X-Ratelimit-Remaining": "99", + "X-Ratelimit-Reset": "2073044847", + "X-Ratelimit-Used": "1", + } + + def test_client_initialization(self) -> None: + client = AsyncTempMailClient("test-api-key") + assert client.api_key == "test-api-key" + assert client.client.headers["X-API-Key"] == "test-api-key" + + def test_client_initialization_with_custom_params(self) -> None: + client = AsyncTempMailClient( + "test-api-key", base_url="https://custom.api.com", timeout=60 + ) + assert client.base_url == "https://custom.api.com" + assert client.timeout == 60 + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_create_email_success(self, mock_request) -> None: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"email": "test@example.com", "ttl": 86400} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client: AsyncTempMailClient = AsyncTempMailClient("test-api-key") + email: EmailAddress = await client.create_email() + assert email == EmailAddress(email="test@example.com", ttl=86400) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_create_email_premium_domain_type(self, mock_request) -> None: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"email": "test@example.com", "ttl": 86400} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client: AsyncTempMailClient = AsyncTempMailClient("test-api-key") + email: EmailAddress = await client.create_email(domain_type=DomainType.PREMIUM) + assert email == EmailAddress(email="test@example.com", ttl=86400) + + mock_request.assert_called_once_with( + method="POST", + url="https://api.temp-mail.io/v1/emails", + params=None, + json={"domain_type": "premium"}, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_create_email_with_options(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"email": "custom@mydomain.com", "ttl": 86400} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client: AsyncTempMailClient = AsyncTempMailClient("test-api-key") + email: EmailAddress = await client.create_email(domain="mydomain.com") + assert email == EmailAddress(email="custom@mydomain.com", ttl=86400) + + # Verify request was made with correct parameters + mock_request.assert_called_once_with( + method="POST", + url="https://api.temp-mail.io/v1/emails", + params=None, + json={"domain": "mydomain.com"}, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_list_domains_success(self, mock_request) -> None: + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "domains": [ + { + "name": "example.com", + "type": "public", + }, + { + "name": "test.org", + "type": "custom", + }, + { + "name": "example.io", + "type": "premium", + }, + ] + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client: AsyncTempMailClient = AsyncTempMailClient("test-api-key") + domains: typing.List[Domain] = await client.list_domains() + assert domains == [ + Domain(name="example.com", type=DomainType.PUBLIC), + Domain(name="test.org", type=DomainType.CUSTOM), + Domain(name="example.io", type=DomainType.PREMIUM), + ] + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_list_email_messages_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "messages": [ + { + "id": "msg1", + "from": "sender@example.com", + "to": "test@temp.io", + "cc": ["cc@example.com"], + "subject": "Test Subject", + "body_text": "Test body", + "body_html": "

Test body

", + "created_at": "2023-01-01T00:00:00Z", + "attachments": [ + { + "id": "att1", + "name": "file.txt", + "size": 1234, + }, + { + "id": "att2", + "name": "image.png", + "size": 4567, + }, + ], + } + ] + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + messages: typing.List[EmailMessage] = await client.list_email_messages( + "test@temp.io" + ) + + assert len(messages) == 1 + assert messages[0] == EmailMessage( + id="msg1", + from_addr="sender@example.com", + to_addr="test@temp.io", + cc=["cc@example.com"], + subject="Test Subject", + body_text="Test body", + body_html="

Test body

", + created_at=datetime.datetime( + 2023, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), + attachments=[ + Attachment(id="att1", name="file.txt", size=1234), + Attachment(id="att2", name="image.png", size=4567), + ], + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_list_email_messages_no_attachments(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "messages": [ + { + "id": "msg1", + "from": "", + "to": "test@temp.io", + "cc": ["cc@example.com"], + "subject": "Test Subject", + "body_text": "Test body", + "body_html": "

Test body

", + "created_at": "2023-01-01T00:00:00Z", + "attachments": None, + } + ] + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + messages: typing.List[EmailMessage] = await client.list_email_messages( + "test@temp.io" + ) + + assert len(messages) == 1 + assert messages[0] == EmailMessage( + id="msg1", + from_addr="", + to_addr="test@temp.io", + cc=["cc@example.com"], + subject="Test Subject", + body_text="Test body", + body_html="

Test body

", + created_at=datetime.datetime( + 2023, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), + attachments=[], + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_list_email_messages_empty(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"messages": []} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + messages = await client.list_email_messages("test@temp.io") + + assert len(messages) == 0 + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/emails/test@temp.io/messages", + params=None, + json=None, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_get_message_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": "msg1", + "from": "sender@example.com", + "to": "test@temp.io", + "cc": [], + "subject": "Test Subject", + "body_text": "Test body", + "body_html": "

Test body

", + "created_at": "2023-01-01T00:00:00Z", + "attachments": [], + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + message = await client.get_message("msg1") + + assert message == EmailMessage( + id="msg1", + from_addr="sender@example.com", + to_addr="test@temp.io", + cc=[], + subject="Test Subject", + body_text="Test body", + body_html="

Test body

", + created_at=datetime.datetime( + 2023, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), + attachments=[], + ) + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/messages/msg1", + params=None, + json=None, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_delete_message_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + await client.delete_message("msg123") + + mock_request.assert_called_once_with( + method="DELETE", + url="https://api.temp-mail.io/v1/messages/msg123", + params=None, + json=None, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_delete_email_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + await client.delete_email("test@temp.io") + + mock_request.assert_called_once_with( + method="DELETE", + url="https://api.temp-mail.io/v1/emails/test@temp.io", + params=None, + json=None, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_get_message_source_code_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": "Received: from example.com...\r\nSubject: Test Subject\r\n\r\nTest body" + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + source_code = await client.get_message_source_code("msg1") + + assert "Received: from example.com" in source_code + assert "Subject: Test Subject" in source_code + + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/messages/msg1/source_code", + params=None, + json=None, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_download_attachment_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = b"attachment content here" + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + content = await client.download_attachment("attachment1") + + assert content == b"attachment content here" + + mock_request.assert_called_once_with( + method="GET", + url="https://api.temp-mail.io/v1/attachments/attachment1", + params=None, + json=None, + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_authentication_error(self, mock_request): + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = { + "error": { + "code": "api_key_invalid", + "detail": "API token is invalid", + "type": "request_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } + mock_response.headers = {} + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + with pytest.raises(AuthenticationError, match="API token is invalid"): + await client.create_email() + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_rate_limit_error(self, mock_request): + mock_response = Mock() + mock_response.status_code = 429 + mock_response.json.return_value = { + "error": { + "code": "rate_limited", + "detail": "You have reached your rate limit. Please try again later.", + "type": "request_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } + mock_response.headers = {} + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + with pytest.raises( + RateLimitError, + match="You have reached your rate limit. Please try again later.", + ): + await client.create_email() + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_validation_error(self, mock_request): + mock_response = Mock() + mock_response.status_code = 400 + mock_response.json.return_value = { + "error": { + "code": "validation_error", + "detail": "Invalid domain name", + "type": "request_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } + mock_response.headers = {} + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + with pytest.raises(ValidationError, match="Invalid domain name"): + await client.create_email(domain="invalid_domain") + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_api_error(self, mock_request): + mock_response = Mock() + mock_response.status_code = 500 + mock_response.json.return_value = { + "error": { + "code": "internal_error", + "detail": "Internal server error", + "type": "api_error", + }, + "meta": {"request_id": "01K510JMH7V5PTN1TNCW5HF9AE"}, + } + mock_response.headers = {} + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + with pytest.raises(TempMailError, match="Internal server error"): + await client.create_email() + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_get_rate_limit_success(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "limit": 100, + "remaining": 95, + "used": 5, + "reset": 1640995200, + } + mock_response.headers = { + "X-Ratelimit-Limit": "100", + "X-Ratelimit-Remaining": "95", + "X-Ratelimit-Reset": "1640995200", + "X-Ratelimit-Used": "5", + } + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + rate_limit_data = await client.get_rate_limit() + + assert rate_limit_data == RateLimit( + limit=100, remaining=95, used=5, reset=1640995200 + ) + + # Verify the last rate limit was updated from headers + assert client.last_rate_limit is not None + assert client.last_rate_limit == RateLimit( + limit=100, remaining=95, used=5, reset=1640995200 + ) + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_request_exception(self, mock_request): + mock_request.side_effect = ConnectError("Connection failed") + + client = AsyncTempMailClient("test-api-key") + with pytest.raises(TempMailError, match="Request failed"): + await client.list_domains() + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_create_email_with_specific_email(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "email": "specific@example.com", + "ttl": 86400, + } + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + email = await client.create_email(email="specific@example.com") + assert email == EmailAddress(email="specific@example.com", ttl=86400) + + mock_request.assert_called_once_with( + method="POST", + url="https://api.temp-mail.io/v1/emails", + params=None, + json={"email": "specific@example.com"}, + ) + + async def test_context_manager(self): + with patch( + "tempmail.async_client.httpx.AsyncClient.aclose", new_callable=AsyncMock + ) as mock_close: + async with AsyncTempMailClient("test-api-key") as client: + assert isinstance(client, AsyncTempMailClient) + mock_close.assert_called_once() + + async def test_close_method(self): + with patch( + "tempmail.async_client.httpx.AsyncClient.aclose", new_callable=AsyncMock + ) as mock_close: + client = AsyncTempMailClient("test-api-key") + await client.close() + mock_close.assert_called_once() + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_create_email_with_empty_json_data(self, mock_request): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"email": "random@example.com", "ttl": 86400} + mock_response.headers = self._rate_limit_headers + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + email = await client.create_email() + assert email == EmailAddress(email="random@example.com", ttl=86400) + + mock_request.assert_called_once_with( + method="POST", + url="https://api.temp-mail.io/v1/emails", + params=None, + json=None, + ) + + def test_last_rate_limit_initial_state(self): + client = AsyncTempMailClient("test-api-key") + assert client.last_rate_limit is None + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_error_response_with_different_status_codes(self, mock_request): + mock_response = Mock() + mock_response.status_code = 404 + mock_response.json.return_value = { + "error": { + "code": "not_found", + "detail": "Message not found", + "type": "request_error", + }, + "meta": {"request_id": "123"}, + } + mock_response.headers = {} + mock_request.return_value = mock_response + + client = AsyncTempMailClient("test-api-key") + with pytest.raises(TempMailError, match="Message not found"): + await client.get_message("non-existent-id") + + def test_httpx_client_timeout_configuration(self): + client = AsyncTempMailClient("test-api-key", timeout=60) + # httpx.AsyncClient.timeout returns a Timeout object + assert client.client.timeout.read == 60 + assert client.client.timeout.connect == 60 + + @patch("tempmail.async_client.httpx.AsyncClient.request", new_callable=AsyncMock) + async def test_httpx_specific_error_handling(self, mock_request): + mock_request.side_effect = TimeoutException("Request timeout") + + client = AsyncTempMailClient("test-api-key") + with pytest.raises(TempMailError, match="Request failed"): + await client.list_domains()