Skip to content

Commit 83d3082

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

File tree

6 files changed

+277
-0
lines changed

6 files changed

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

0 commit comments

Comments
 (0)