Skip to content

Commit f837f7f

Browse files
Ihor BilousIhor Bilous
authored andcommitted
Fix issue #23: Add SuppressionsApi, related models, tests and examples
1 parent ddfac64 commit f837f7f

File tree

6 files changed

+279
-0
lines changed

6 files changed

+279
-0
lines changed

examples/suppressions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Optional
2+
3+
import mailtrap as mt
4+
from mailtrap.models.suppressions import Suppression
5+
6+
API_TOKEN = "YOU_API_TOKEN"
7+
ACCOUNT_ID = "YOU_ACCOUNT_ID"
8+
9+
client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID)
10+
suppressions_api = client.suppressions_api.suppressions
11+
12+
13+
def list_suppressions(email: Optional[str] = None) -> list[Suppression]:
14+
return suppressions_api.get_list(email)
15+
16+
17+
def delete_suppression(suppression_id: str) -> Suppression:
18+
return suppressions_api.delete(suppression_id)
19+
20+
21+
if __name__ == "__main__":
22+
print(list_suppressions())
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Optional
2+
3+
from mailtrap.http import HttpClient
4+
from mailtrap.models.suppressions import Suppression
5+
6+
7+
class SuppressionsApi:
8+
def __init__(self, client: HttpClient, account_id: str) -> None:
9+
self._account_id = account_id
10+
self._client = client
11+
12+
def get_list(self, email: Optional[str] = None) -> list[Suppression]:
13+
params = {"email": email} if email is not None else None
14+
response = self._client.get(self._api_path(), params=params)
15+
return [Suppression(**suppression) for suppression in response]
16+
17+
def delete(self, suppression_id: str) -> Suppression:
18+
response = self._client.delete(self._api_path(suppression_id))
19+
return Suppression(**response)
20+
21+
def _api_path(self, suppression_id: Optional[str] = None) -> str:
22+
path = f"/api/accounts/{self._account_id}/suppressions"
23+
if suppression_id is not None:
24+
return f"{path}/{suppression_id}"
25+
return path

mailtrap/api/suppressions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from mailtrap.api.resources.suppressions import SuppressionsApi
2+
from mailtrap.http import HttpClient
3+
4+
5+
class SuppressionsBaseApi:
6+
def __init__(self, client: HttpClient, account_id: str) -> None:
7+
self._account_id = account_id
8+
self._client = client
9+
10+
@property
11+
def suppressions(self) -> SuppressionsApi:
12+
return SuppressionsApi(account_id=self._account_id, client=self._client)

mailtrap/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from mailtrap.api.contacts import ContactsBaseApi
99
from mailtrap.api.sending import SendingApi
10+
from mailtrap.api.suppressions import SuppressionsBaseApi
1011
from mailtrap.api.templates import EmailTemplatesApi
1112
from mailtrap.api.testing import TestingApi
1213
from mailtrap.config import BULK_HOST
@@ -72,6 +73,14 @@ def contacts_api(self) -> ContactsBaseApi:
7273
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
7374
)
7475

76+
@property
77+
def suppressions_api(self) -> SuppressionsBaseApi:
78+
self._validate_account_id()
79+
return SuppressionsBaseApi(
80+
account_id=cast(str, self.account_id),
81+
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
82+
)
83+
7584
@property
7685
def sending_api(self) -> SendingApi:
7786
http_client = HttpClient(host=self._sending_api_host, headers=self.headers)

mailtrap/models/suppressions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from datetime import datetime
2+
from enum import Enum
3+
from typing import Optional
4+
from typing import Union
5+
6+
from pydantic.dataclasses import dataclass
7+
8+
9+
class SuppressionType(str, Enum):
10+
HARD_BOUNCE = "hard bounce"
11+
SPAM_COMPLAINT = "spam complaint"
12+
UNSUBSCRIPTION = "unsubscription"
13+
MANUAL_IMPORT = "manual import"
14+
15+
16+
class SendingStream(str, Enum):
17+
TRANSACTIONAL = "transactional"
18+
BULK = "bulk"
19+
20+
21+
@dataclass
22+
class Suppression:
23+
id: str
24+
type: SuppressionType
25+
created_at: datetime
26+
email: str
27+
sending_stream: SendingStream
28+
domain_name: Optional[str] = None
29+
message_bounce_category: Optional[str] = None
30+
message_category: Optional[str] = None
31+
message_client_ip: Optional[str] = None
32+
message_created_at: Optional[Union[str, datetime]] = None
33+
message_esp_response: Optional[str] = None
34+
message_esp_server_type: Optional[str] = None
35+
message_outgoing_ip: Optional[str] = None
36+
message_recipient_mx_name: Optional[str] = None
37+
message_sender_email: Optional[str] = None
38+
message_subject: Optional[str] = None

tests/unit/api/test_suppressions.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
from datetime import datetime
2+
from typing import Any
3+
4+
import pytest
5+
import responses
6+
7+
from mailtrap.api.resources.suppressions import SuppressionsApi
8+
from mailtrap.config import GENERAL_HOST
9+
from mailtrap.exceptions import APIError
10+
from mailtrap.http import HttpClient
11+
from mailtrap.models.suppressions import SendingStream
12+
from mailtrap.models.suppressions import Suppression
13+
from mailtrap.models.suppressions import SuppressionType
14+
from tests import conftest
15+
16+
ACCOUNT_ID = "321"
17+
SUPPRESSION_ID = "supp_123456"
18+
BASE_SUPPRESSIONS_URL = f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/suppressions"
19+
20+
21+
@pytest.fixture
22+
def suppressions_api() -> SuppressionsApi:
23+
return SuppressionsApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST))
24+
25+
26+
@pytest.fixture
27+
def sample_suppression_dict() -> dict[str, Any]:
28+
return {
29+
"id": SUPPRESSION_ID,
30+
"type": "unsubscription",
31+
"created_at": "2024-12-26T09:40:44.161Z",
32+
"email": "[email protected]",
33+
"sending_stream": "transactional",
34+
"domain_name": "sender.com",
35+
"message_bounce_category": None,
36+
"message_category": "Welcome email",
37+
"message_client_ip": "123.123.123.123",
38+
"message_created_at": "2024-12-26T07:10:00.889Z",
39+
"message_outgoing_ip": "1.1.1.1",
40+
"message_recipient_mx_name": "Other Providers",
41+
"message_sender_email": "[email protected]",
42+
"message_subject": "Welcome!",
43+
}
44+
45+
46+
class TestSuppressionsApi:
47+
48+
@pytest.mark.parametrize(
49+
"status_code,response_json,expected_error_message",
50+
[
51+
(
52+
conftest.UNAUTHORIZED_STATUS_CODE,
53+
conftest.UNAUTHORIZED_RESPONSE,
54+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
55+
),
56+
(
57+
conftest.FORBIDDEN_STATUS_CODE,
58+
conftest.FORBIDDEN_RESPONSE,
59+
conftest.FORBIDDEN_ERROR_MESSAGE,
60+
),
61+
],
62+
)
63+
@responses.activate
64+
def test_get_suppressions_should_raise_api_errors(
65+
self,
66+
suppressions_api: SuppressionsApi,
67+
status_code: int,
68+
response_json: dict,
69+
expected_error_message: str,
70+
) -> None:
71+
responses.get(
72+
BASE_SUPPRESSIONS_URL,
73+
status=status_code,
74+
json=response_json,
75+
)
76+
77+
with pytest.raises(APIError) as exc_info:
78+
suppressions_api.get_list()
79+
80+
assert expected_error_message in str(exc_info.value)
81+
82+
@responses.activate
83+
def test_get_suppressions_should_return_suppression_list(
84+
self, suppressions_api: SuppressionsApi, sample_suppression_dict: dict
85+
) -> None:
86+
responses.get(
87+
BASE_SUPPRESSIONS_URL,
88+
json=[sample_suppression_dict],
89+
status=200,
90+
)
91+
92+
suppressions = suppressions_api.get_list()
93+
94+
assert isinstance(suppressions, list)
95+
assert all(isinstance(s, Suppression) for s in suppressions)
96+
assert suppressions[0].id == SUPPRESSION_ID
97+
98+
@responses.activate
99+
def test_get_suppressions_with_email_filter_should_return_filtered_list(
100+
self, suppressions_api: SuppressionsApi, sample_suppression_dict: dict
101+
) -> None:
102+
email_filter = "[email protected]"
103+
responses.get(
104+
BASE_SUPPRESSIONS_URL,
105+
json=[sample_suppression_dict],
106+
status=200,
107+
match=[responses.matchers.query_param_matcher({"email": email_filter})],
108+
)
109+
110+
suppressions = suppressions_api.get_list(email=email_filter)
111+
112+
assert isinstance(suppressions, list)
113+
assert all(isinstance(s, Suppression) for s in suppressions)
114+
assert suppressions[0].id == SUPPRESSION_ID
115+
assert suppressions[0].email == email_filter
116+
assert isinstance(suppressions[0].created_at, datetime)
117+
118+
@pytest.mark.parametrize(
119+
"status_code,response_json,expected_error_message",
120+
[
121+
(
122+
conftest.UNAUTHORIZED_STATUS_CODE,
123+
conftest.UNAUTHORIZED_RESPONSE,
124+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
125+
),
126+
(
127+
conftest.FORBIDDEN_STATUS_CODE,
128+
conftest.FORBIDDEN_RESPONSE,
129+
conftest.FORBIDDEN_ERROR_MESSAGE,
130+
),
131+
(
132+
conftest.NOT_FOUND_STATUS_CODE,
133+
conftest.NOT_FOUND_RESPONSE,
134+
conftest.NOT_FOUND_ERROR_MESSAGE,
135+
),
136+
],
137+
)
138+
@responses.activate
139+
def test_delete_suppression_should_raise_api_errors(
140+
self,
141+
suppressions_api: SuppressionsApi,
142+
status_code: int,
143+
response_json: dict,
144+
expected_error_message: str,
145+
) -> None:
146+
responses.delete(
147+
f"{BASE_SUPPRESSIONS_URL}/{SUPPRESSION_ID}",
148+
status=status_code,
149+
json=response_json,
150+
)
151+
152+
with pytest.raises(APIError) as exc_info:
153+
suppressions_api.delete(SUPPRESSION_ID)
154+
155+
assert expected_error_message in str(exc_info.value)
156+
157+
@responses.activate
158+
def test_delete_suppression_should_return_deleted_suppression(
159+
self, suppressions_api: SuppressionsApi, sample_suppression_dict: dict
160+
) -> None:
161+
responses.delete(
162+
f"{BASE_SUPPRESSIONS_URL}/{SUPPRESSION_ID}",
163+
json=sample_suppression_dict,
164+
status=200,
165+
)
166+
167+
deleted_suppression = suppressions_api.delete(SUPPRESSION_ID)
168+
169+
assert isinstance(deleted_suppression, Suppression)
170+
assert deleted_suppression.id == SUPPRESSION_ID
171+
assert deleted_suppression.type == SuppressionType.UNSUBSCRIPTION
172+
assert deleted_suppression.email == "[email protected]"
173+
assert deleted_suppression.sending_stream == SendingStream.TRANSACTIONAL

0 commit comments

Comments
 (0)