From c1be4d79be2444d8287d57c416d328435bb493ae Mon Sep 17 00:00:00 2001 From: Andrii Porokhnavets Date: Wed, 30 Apr 2025 11:02:40 +0300 Subject: [PATCH 1/2] Add sandbox support in MailtrapClient Introduced `sandbox` mode, allowing configuration for sandbox environments via `inbox_id`. Implemented validation to ensure correct usage of `sandbox` and `inbox_id`, along with related tests. Updated URL generation logic and error handling for enhanced flexibility. --- mailtrap/__init__.py | 1 + mailtrap/client.py | 43 +++++++++++++++++++++++++++++++++++---- mailtrap/exceptions.py | 5 +++++ tests/unit/test_client.py | 32 +++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/mailtrap/__init__.py b/mailtrap/__init__.py index 276cc45..f03b693 100644 --- a/mailtrap/__init__.py +++ b/mailtrap/__init__.py @@ -1,6 +1,7 @@ from .client import MailtrapClient from .exceptions import APIError from .exceptions import AuthorizationError +from .exceptions import ClientConfigurationError from .exceptions import MailtrapError from .mail import Address from .mail import Attachment diff --git a/mailtrap/client.py b/mailtrap/client.py index 9d33682..42f072f 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -1,30 +1,40 @@ from typing import NoReturn +from typing import Optional from typing import Union import requests from mailtrap.exceptions import APIError from mailtrap.exceptions import AuthorizationError +from mailtrap.exceptions import ClientConfigurationError from mailtrap.mail.base import BaseMail class MailtrapClient: DEFAULT_HOST = "send.api.mailtrap.io" DEFAULT_PORT = 443 + SANDBOX_HOST = "sandbox.api.mailtrap.io" def __init__( self, token: str, - api_host: str = DEFAULT_HOST, + api_host: Optional[str] = None, api_port: int = DEFAULT_PORT, + sandbox: bool = False, + inbox_id: Optional[str] = None, ) -> None: self.token = token self.api_host = api_host self.api_port = api_port + self.sandbox = sandbox + self.inbox_id = inbox_id + + self._validate_itself() def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]: - url = f"{self.base_url}/api/send" - response = requests.post(url, headers=self.headers, json=mail.api_data) + response = requests.post( + self.api_send_url, headers=self.headers, json=mail.api_data + ) if response.ok: data: dict[str, Union[bool, list[str]]] = response.json() @@ -34,7 +44,15 @@ def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]: @property def base_url(self) -> str: - return f"https://{self.api_host.rstrip('/')}:{self.api_port}" + return f"https://{self._host.rstrip('/')}:{self.api_port}" + + @property + def api_send_url(self) -> str: + url = f"{self.base_url}/api/send" + if self.sandbox and self.inbox_id: + return f"{url}/{self.inbox_id}" + + return url @property def headers(self) -> dict[str, str]: @@ -46,6 +64,14 @@ def headers(self) -> dict[str, str]: ), } + @property + def _host(self) -> str: + if self.api_host: + return self.api_host + if self.sandbox: + return self.SANDBOX_HOST + return self.DEFAULT_HOST + @staticmethod def _handle_failed_response(response: requests.Response) -> NoReturn: status_code = response.status_code @@ -55,3 +81,12 @@ def _handle_failed_response(response: requests.Response) -> NoReturn: raise AuthorizationError(data["errors"]) raise APIError(status_code, data["errors"]) + + def _validate_itself(self) -> None: + if self.sandbox and not self.inbox_id: + raise ClientConfigurationError("`inbox_id` is required for sandbox mode") + + if not self.sandbox and self.inbox_id: + raise ClientConfigurationError( + "`inbox_id` is not allowed in non-sandbox mode" + ) diff --git a/mailtrap/exceptions.py b/mailtrap/exceptions.py index 978f1b8..f8c7c67 100644 --- a/mailtrap/exceptions.py +++ b/mailtrap/exceptions.py @@ -2,6 +2,11 @@ class MailtrapError(Exception): pass +class ClientConfigurationError(MailtrapError): + def __init__(self, message: str) -> None: + super().__init__(message) + + class APIError(MailtrapError): def __init__(self, status: int, errors: list[str]) -> None: self.status = status diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index b34ea2c..8b82e2e 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -30,11 +30,43 @@ def get_client(**kwargs: Any) -> mt.MailtrapClient: props = {"token": "fake_token", **kwargs} return mt.MailtrapClient(**props) + @pytest.mark.parametrize( + "arguments", + [ + {"sandbox": True}, + {"inbox_id": "12345"}, + ], + ) + def test_client_validation(self, arguments: dict[str, Any]) -> None: + with pytest.raises(mt.ClientConfigurationError): + self.get_client(**arguments) + def test_base_url_should_truncate_slash_from_host(self) -> None: client = self.get_client(api_host="example.send.com/", api_port=543) assert client.base_url == "https://example.send.com:543" + @pytest.mark.parametrize( + "arguments, expected_url", + [ + ({}, "https://send.api.mailtrap.io:443/api/send"), + ( + {"api_host": "example.send.com", "api_port": 543}, + "https://example.send.com:543/api/send", + ), + ( + {"sandbox": True, "inbox_id": "12345"}, + "https://sandbox.api.mailtrap.io:443/api/send/12345", + ), + ], + ) + def test_api_send_url_should_return_default_sending_url( + self, arguments: dict[str, Any], expected_url: str + ) -> None: + client = self.get_client(**arguments) + + assert client.api_send_url == expected_url + def test_headers_should_return_appropriate_dict(self) -> None: client = self.get_client() From a138158b71b717637348f8125cae4264db5fbd0d Mon Sep 17 00:00:00 2001 From: Andrii Porokhnavets Date: Wed, 30 Apr 2025 11:10:19 +0300 Subject: [PATCH 2/2] Add support for bulk sending mode in Mailtrap client Introduce a new `bulk` mode in the Mailtrap client, setting a dedicated `BULK_HOST` for bulk operations. Updated validation logic to prevent conflicts between bulk and sandbox modes. Added corresponding test cases to ensure correct functionality and URL generation. --- mailtrap/client.py | 8 ++++++++ tests/unit/test_client.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/mailtrap/client.py b/mailtrap/client.py index 42f072f..60a1d6a 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -13,6 +13,7 @@ class MailtrapClient: DEFAULT_HOST = "send.api.mailtrap.io" DEFAULT_PORT = 443 + BULK_HOST = "bulk.api.mailtrap.io" SANDBOX_HOST = "sandbox.api.mailtrap.io" def __init__( @@ -20,12 +21,14 @@ def __init__( token: str, api_host: Optional[str] = None, api_port: int = DEFAULT_PORT, + bulk: bool = False, sandbox: bool = False, inbox_id: Optional[str] = None, ) -> None: self.token = token self.api_host = api_host self.api_port = api_port + self.bulk = bulk self.sandbox = sandbox self.inbox_id = inbox_id @@ -70,6 +73,8 @@ def _host(self) -> str: return self.api_host if self.sandbox: return self.SANDBOX_HOST + if self.bulk: + return self.BULK_HOST return self.DEFAULT_HOST @staticmethod @@ -90,3 +95,6 @@ def _validate_itself(self) -> None: raise ClientConfigurationError( "`inbox_id` is not allowed in non-sandbox mode" ) + + if self.bulk and self.sandbox: + raise ClientConfigurationError("bulk mode is not allowed in sandbox mode") diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 8b82e2e..29d3679 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -35,6 +35,7 @@ def get_client(**kwargs: Any) -> mt.MailtrapClient: [ {"sandbox": True}, {"inbox_id": "12345"}, + {"bulk": True, "sandbox": True, "inbox_id": "12345"}, ], ) def test_client_validation(self, arguments: dict[str, Any]) -> None: @@ -54,10 +55,22 @@ def test_base_url_should_truncate_slash_from_host(self) -> None: {"api_host": "example.send.com", "api_port": 543}, "https://example.send.com:543/api/send", ), + ( + {"api_host": "example.send.com", "sandbox": True, "inbox_id": "12345"}, + "https://example.send.com:443/api/send/12345", + ), + ( + {"api_host": "example.send.com", "bulk": True}, + "https://example.send.com:443/api/send", + ), ( {"sandbox": True, "inbox_id": "12345"}, "https://sandbox.api.mailtrap.io:443/api/send/12345", ), + ( + {"bulk": True}, + "https://bulk.api.mailtrap.io:443/api/send", + ), ], ) def test_api_send_url_should_return_default_sending_url(