diff --git a/examples/email_templates/templates.py b/examples/email_templates/templates.py new file mode 100644 index 0000000..e028540 --- /dev/null +++ b/examples/email_templates/templates.py @@ -0,0 +1,62 @@ +from typing import Optional + +import mailtrap as mt +from mailtrap.models.common import DeletedObject +from mailtrap.models.templates import EmailTemplate + +API_TOKEN = "YOU_API_TOKEN" +ACCOUNT_ID = "YOU_ACCOUNT_ID" + +client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID) +templates_api = client.email_templates_api.templates + + +def list_templates() -> list[EmailTemplate]: + return templates_api.get_list() + + +def create_template( + name: str, + subject: str, + category: str, + body_text: Optional[str] = None, + body_html: Optional[str] = None, +) -> EmailTemplate: + params = mt.CreateEmailTemplateParams( + name=name, + subject=subject, + category=category, + body_text=body_text, + body_html=body_html, + ) + return templates_api.create(params) + + +def get_template(template_id: str) -> EmailTemplate: + return templates_api.get_by_id(template_id) + + +def update_template( + template_id: str, + name: Optional[str] = None, + subject: Optional[str] = None, + category: Optional[str] = None, + body_text: Optional[str] = None, + body_html: Optional[str] = None, +) -> EmailTemplate: + params = mt.UpdateEmailTemplateParams( + name=name, + subject=subject, + category=category, + body_text=body_text, + body_html=body_html, + ) + return templates_api.update(template_id, params) + + +def delete_template(template_id: str) -> DeletedObject: + return templates_api.delete(template_id) + + +if __name__ == "__main__": + print(list_templates()) diff --git a/mailtrap/__init__.py b/mailtrap/__init__.py index 9c039e0..009f79a 100644 --- a/mailtrap/__init__.py +++ b/mailtrap/__init__.py @@ -10,3 +10,5 @@ from .models.mail import Disposition from .models.mail import Mail from .models.mail import MailFromTemplate +from .models.templates import CreateEmailTemplateParams +from .models.templates import UpdateEmailTemplateParams diff --git a/mailtrap/api/resources/templates.py b/mailtrap/api/resources/templates.py new file mode 100644 index 0000000..beedbfc --- /dev/null +++ b/mailtrap/api/resources/templates.py @@ -0,0 +1,43 @@ +from mailtrap.http import HttpClient +from mailtrap.models.common import DeletedObject +from mailtrap.models.templates import CreateEmailTemplateParams +from mailtrap.models.templates import EmailTemplate +from mailtrap.models.templates import UpdateEmailTemplateParams + + +class TemplatesApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + def get_list(self) -> list[EmailTemplate]: + response = self._client.get(f"/api/accounts/{self._account_id}/email_templates") + return [EmailTemplate(**template) for template in response] + + def get_by_id(self, template_id: int) -> EmailTemplate: + response = self._client.get( + f"/api/accounts/{self._account_id}/email_templates/{template_id}" + ) + return EmailTemplate(**response) + + def create(self, template_params: CreateEmailTemplateParams) -> EmailTemplate: + response = self._client.post( + f"/api/accounts/{self._account_id}/email_templates", + json={"email_template": template_params.api_data}, + ) + return EmailTemplate(**response) + + def update( + self, template_id: int, template_params: UpdateEmailTemplateParams + ) -> EmailTemplate: + response = self._client.patch( + f"/api/accounts/{self._account_id}/email_templates/{template_id}", + json={"email_template": template_params.api_data}, + ) + return EmailTemplate(**response) + + def delete(self, template_id: int) -> DeletedObject: + self._client.delete( + f"/api/accounts/{self._account_id}/email_templates/{template_id}" + ) + return DeletedObject(template_id) diff --git a/mailtrap/api/templates.py b/mailtrap/api/templates.py new file mode 100644 index 0000000..cc2640d --- /dev/null +++ b/mailtrap/api/templates.py @@ -0,0 +1,12 @@ +from mailtrap.api.resources.templates import TemplatesApi +from mailtrap.http import HttpClient + + +class EmailTemplatesApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + @property + def templates(self) -> TemplatesApi: + return TemplatesApi(account_id=self._account_id, client=self._client) diff --git a/mailtrap/client.py b/mailtrap/client.py index 68409cf..f1d9f54 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -6,6 +6,7 @@ from pydantic import TypeAdapter from mailtrap.api.sending import SendingApi +from mailtrap.api.templates import EmailTemplatesApi from mailtrap.api.testing import TestingApi from mailtrap.config import BULK_HOST from mailtrap.config import GENERAL_HOST @@ -54,6 +55,14 @@ def testing_api(self) -> TestingApi: client=HttpClient(host=GENERAL_HOST, headers=self.headers), ) + @property + def email_templates_api(self) -> EmailTemplatesApi: + self._validate_account_id() + return EmailTemplatesApi( + account_id=cast(str, self.account_id), + client=HttpClient(host=GENERAL_HOST, headers=self.headers), + ) + @property def sending_api(self) -> SendingApi: http_client = HttpClient(host=self._sending_api_host, headers=self.headers) diff --git a/mailtrap/http.py b/mailtrap/http.py index 4e0c9bf..06524bd 100644 --- a/mailtrap/http.py +++ b/mailtrap/http.py @@ -50,6 +50,10 @@ def _url(self, path: str) -> str: def _process_response(self, response: Response) -> Any: if not response.ok: self._handle_failed_response(response) + + if not response.content.strip(): + return None + return response.json() def _handle_failed_response(self, response: Response) -> NoReturn: diff --git a/mailtrap/models/common.py b/mailtrap/models/common.py index da0e1fd..42fdf82 100644 --- a/mailtrap/models/common.py +++ b/mailtrap/models/common.py @@ -5,11 +5,11 @@ from pydantic import TypeAdapter from pydantic.dataclasses import dataclass -T = TypeVar("T", bound="RequestModel") +T = TypeVar("T", bound="RequestParams") @dataclass -class RequestModel: +class RequestParams: @property def api_data(self: T) -> dict[str, Any]: return cast( diff --git a/mailtrap/models/mail/address.py b/mailtrap/models/mail/address.py index 5e7281c..afaee6a 100644 --- a/mailtrap/models/mail/address.py +++ b/mailtrap/models/mail/address.py @@ -2,10 +2,10 @@ from pydantic.dataclasses import dataclass -from mailtrap.models.common import RequestModel +from mailtrap.models.common import RequestParams @dataclass -class Address(RequestModel): +class Address(RequestParams): email: str name: Optional[str] = None diff --git a/mailtrap/models/mail/attachment.py b/mailtrap/models/mail/attachment.py index a65b480..56373b5 100644 --- a/mailtrap/models/mail/attachment.py +++ b/mailtrap/models/mail/attachment.py @@ -6,7 +6,7 @@ from pydantic import field_serializer from pydantic.dataclasses import dataclass -from mailtrap.models.common import RequestModel +from mailtrap.models.common import RequestParams class Disposition(str, Enum): @@ -15,7 +15,7 @@ class Disposition(str, Enum): @dataclass -class Attachment(RequestModel): +class Attachment(RequestParams): content: bytes filename: str disposition: Optional[Disposition] = None diff --git a/mailtrap/models/mail/base.py b/mailtrap/models/mail/base.py index bd41ba9..5964583 100644 --- a/mailtrap/models/mail/base.py +++ b/mailtrap/models/mail/base.py @@ -4,13 +4,13 @@ from pydantic import Field from pydantic.dataclasses import dataclass -from mailtrap.models.common import RequestModel +from mailtrap.models.common import RequestParams from mailtrap.models.mail.address import Address from mailtrap.models.mail.attachment import Attachment @dataclass -class BaseMail(RequestModel): +class BaseMail(RequestParams): sender: Address = Field(..., serialization_alias="from") to: list[Address] = Field(...) cc: Optional[list[Address]] = None diff --git a/mailtrap/models/templates.py b/mailtrap/models/templates.py new file mode 100644 index 0000000..27ba319 --- /dev/null +++ b/mailtrap/models/templates.py @@ -0,0 +1,49 @@ +from typing import Optional + +from pydantic.dataclasses import dataclass + +from mailtrap.models.common import RequestParams + + +@dataclass +class CreateEmailTemplateParams(RequestParams): + name: str + subject: str + category: str + body_text: Optional[str] = None + body_html: Optional[str] = None + + +@dataclass +class UpdateEmailTemplateParams(RequestParams): + name: Optional[str] = None + subject: Optional[str] = None + category: Optional[str] = None + body_text: Optional[str] = None + body_html: Optional[str] = None + + def __post_init__(self) -> None: + if all( + value is None + for value in [ + self.name, + self.subject, + self.category, + self.body_text, + self.body_html, + ] + ): + raise ValueError("At least one field must be provided for update action") + + +@dataclass +class EmailTemplate: + id: int + name: str + uuid: str + category: str + subject: str + body_text: Optional[str] + body_html: Optional[str] + created_at: str + updated_at: str diff --git a/tests/unit/api/test_email_templates.py b/tests/unit/api/test_email_templates.py new file mode 100644 index 0000000..a1bb56b --- /dev/null +++ b/tests/unit/api/test_email_templates.py @@ -0,0 +1,330 @@ +from typing import Any + +import pytest +import responses + +from mailtrap.api.resources.templates import TemplatesApi +from mailtrap.config import GENERAL_HOST +from mailtrap.exceptions import APIError +from mailtrap.http import HttpClient +from mailtrap.models.common import DeletedObject +from mailtrap.models.templates import CreateEmailTemplateParams +from mailtrap.models.templates import EmailTemplate +from mailtrap.models.templates import UpdateEmailTemplateParams +from tests import conftest + +ACCOUNT_ID = "321" +TEMPLATE_ID = 26730 +BASE_TEMPLATES_URL = f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/email_templates" + + +@pytest.fixture +def client() -> TemplatesApi: + return TemplatesApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST)) + + +@pytest.fixture +def sample_template_dict() -> dict[str, Any]: + return { + "id": TEMPLATE_ID, + "name": "Promotion Template", + "uuid": "b81aabcd-1a1e-41cf-91b6-eca0254b3d96", + "category": "Promotion", + "subject": "Promotion Template subject", + "body_text": "Text body", + "body_html": "