Skip to content

Commit 20533d8

Browse files
Ihor BilousIhor Bilous
authored andcommitted
Fix issue #41: Add ContactExportsApi, relates models, tests and examples
1 parent afc1905 commit 20533d8

File tree

7 files changed

+386
-0
lines changed

7 files changed

+386
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import mailtrap as mt
2+
from mailtrap.models.contacts import ContactImport
3+
4+
API_TOKEN = "YOUR_API_TOKEN"
5+
ACCOUNT_ID = "YOUR_ACCOUNT_ID"
6+
7+
client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID)
8+
contact_exports_api = client.contacts_api.contact_exports
9+
10+
11+
def create_export_contacts(
12+
contact_exports_params: mt.CreateContactExportParams,
13+
) -> ContactImport:
14+
return contact_exports_api.create(contact_exports_params=contact_exports_params)
15+
16+
17+
def get_contact_export(import_id: int) -> ContactImport:
18+
return contact_exports_api.get_by_id(import_id)
19+
20+
21+
if __name__ == "__main__":
22+
contact_export = create_export_contacts(
23+
contact_exports_params=mt.CreateContactExportParams(
24+
filters=[mt.ContactExportFilter(name="list_id", operator="equal", value=[10])]
25+
)
26+
)
27+
print(contact_export)
28+
29+
contact_export = get_contact_export(contact_export.id)
30+
print(contact_export)

mailtrap/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from .exceptions import AuthorizationError
66
from .exceptions import ClientConfigurationError
77
from .exceptions import MailtrapError
8+
from .models.contacts import ContactExportFilter
89
from .models.contacts import ContactListParams
10+
from .models.contacts import CreateContactExportParams
911
from .models.contacts import CreateContactFieldParams
1012
from .models.contacts import CreateContactParams
1113
from .models.contacts import ImportContactParams

mailtrap/api/contacts.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from mailtrap.api.resources.contact_exports import ContactExportsApi
12
from mailtrap.api.resources.contact_fields import ContactFieldsApi
23
from mailtrap.api.resources.contact_imports import ContactImportsApi
34
from mailtrap.api.resources.contact_lists import ContactListsApi
@@ -10,6 +11,10 @@ def __init__(self, client: HttpClient, account_id: str) -> None:
1011
self._account_id = account_id
1112
self._client = client
1213

14+
@property
15+
def contact_exports(self) -> ContactExportsApi:
16+
return ContactExportsApi(account_id=self._account_id, client=self._client)
17+
1318
@property
1419
def contact_fields(self) -> ContactFieldsApi:
1520
return ContactFieldsApi(account_id=self._account_id, client=self._client)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Optional
2+
3+
from mailtrap.http import HttpClient
4+
from mailtrap.models.contacts import ContactExportDetail
5+
from mailtrap.models.contacts import CreateContactExportParams
6+
7+
8+
class ContactExportsApi:
9+
def __init__(self, client: HttpClient, account_id: str) -> None:
10+
self._account_id = account_id
11+
self._client = client
12+
13+
def create(
14+
self, contact_exports_params: CreateContactExportParams
15+
) -> ContactExportDetail:
16+
"""Create a new Contact Export"""
17+
response = self._client.post(
18+
self._api_path(),
19+
json=contact_exports_params.api_data,
20+
)
21+
return ContactExportDetail(**response)
22+
23+
def get_by_id(self, export_id: int) -> ContactExportDetail:
24+
"""Get Contact Export"""
25+
response = self._client.get(self._api_path(export_id))
26+
return ContactExportDetail(**response)
27+
28+
def _api_path(self, export_id: Optional[int] = None) -> str:
29+
path = f"/api/accounts/{self._account_id}/contacts/exports"
30+
if export_id is not None:
31+
return f"{path}/{export_id}"
32+
return path

mailtrap/models/contacts.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime
12
from enum import Enum
23
from typing import Optional
34
from typing import Union
@@ -121,3 +122,24 @@ class ImportContactParams(RequestParams):
121122
)
122123
list_ids_included: Optional[list[int]] = None
123124
list_ids_excluded: Optional[list[int]] = None
125+
126+
127+
@dataclass
128+
class ContactExportFilter(RequestParams):
129+
name: str
130+
operator: str
131+
value: list[int]
132+
133+
134+
@dataclass
135+
class CreateContactExportParams(RequestParams):
136+
filters: list[ContactExportFilter]
137+
138+
139+
@dataclass
140+
class ContactExportDetail:
141+
id: int
142+
status: str
143+
created_at: datetime
144+
updated_at: datetime
145+
url: Optional[str] = None
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
from typing import Any
2+
3+
import pytest
4+
import responses
5+
6+
from mailtrap.api.resources.contact_exports import ContactExportsApi
7+
from mailtrap.config import GENERAL_HOST
8+
from mailtrap.exceptions import APIError
9+
from mailtrap.http import HttpClient
10+
from mailtrap.models.contacts import ContactExportDetail
11+
from mailtrap.models.contacts import ContactExportFilter
12+
from mailtrap.models.contacts import CreateContactExportParams
13+
from tests import conftest
14+
15+
ACCOUNT_ID = "321"
16+
EXPORT_ID = 1
17+
BASE_CONTACT_EXPORTS_URL = (
18+
f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/contacts/exports"
19+
)
20+
21+
22+
@pytest.fixture
23+
def client() -> ContactExportsApi:
24+
return ContactExportsApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST))
25+
26+
27+
@pytest.fixture
28+
def sample_contact_export_started_dict() -> dict[str, Any]:
29+
return {
30+
"id": EXPORT_ID,
31+
"status": "started",
32+
"created_at": "2021-01-01T00:00:00Z",
33+
"updated_at": "2021-01-01T00:00:00Z",
34+
"url": None,
35+
}
36+
37+
38+
@pytest.fixture
39+
def sample_contact_export_finished_dict() -> dict[str, Any]:
40+
return {
41+
"id": EXPORT_ID,
42+
"status": "finished",
43+
"created_at": "2021-01-01T00:00:00Z",
44+
"updated_at": "2021-01-01T00:00:00Z",
45+
"url": "https://example.com/export.csv.gz",
46+
}
47+
48+
49+
@pytest.fixture
50+
def sample_contact_export_filter() -> ContactExportFilter:
51+
return ContactExportFilter(name="list_id", operator="in", value=[1, 2, 3])
52+
53+
54+
@pytest.fixture
55+
def sample_create_contact_export_params() -> CreateContactExportParams:
56+
return CreateContactExportParams(
57+
filters=[ContactExportFilter(name="list_id", operator="in", value=[1, 2, 3])]
58+
)
59+
60+
61+
class TestContactExportsApi:
62+
63+
@pytest.mark.parametrize(
64+
"status_code,response_json,expected_error_message",
65+
[
66+
(
67+
conftest.UNAUTHORIZED_STATUS_CODE,
68+
conftest.UNAUTHORIZED_RESPONSE,
69+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
70+
),
71+
(
72+
conftest.FORBIDDEN_STATUS_CODE,
73+
conftest.FORBIDDEN_RESPONSE,
74+
conftest.FORBIDDEN_ERROR_MESSAGE,
75+
),
76+
],
77+
)
78+
@responses.activate
79+
def test_create_should_raise_api_errors(
80+
self,
81+
client: ContactExportsApi,
82+
status_code: int,
83+
response_json: dict,
84+
expected_error_message: str,
85+
sample_create_contact_export_params: CreateContactExportParams,
86+
) -> None:
87+
responses.post(
88+
BASE_CONTACT_EXPORTS_URL,
89+
status=status_code,
90+
json=response_json,
91+
)
92+
93+
with pytest.raises(APIError) as exc_info:
94+
client.create(sample_create_contact_export_params)
95+
96+
assert expected_error_message in str(exc_info.value)
97+
98+
@responses.activate
99+
def test_create_should_return_contact_export_detail(
100+
self,
101+
client: ContactExportsApi,
102+
sample_contact_export_started_dict: dict,
103+
sample_create_contact_export_params: CreateContactExportParams,
104+
) -> None:
105+
responses.post(
106+
BASE_CONTACT_EXPORTS_URL,
107+
json=sample_contact_export_started_dict,
108+
status=201,
109+
)
110+
111+
result = client.create(sample_create_contact_export_params)
112+
113+
assert isinstance(result, ContactExportDetail)
114+
assert result.id == EXPORT_ID
115+
assert result.status == "started"
116+
assert result.url is None
117+
assert len(responses.calls) == 1
118+
request = responses.calls[0].request
119+
assert request.url == BASE_CONTACT_EXPORTS_URL
120+
121+
@responses.activate
122+
def test_create_should_handle_validation_errors(
123+
self,
124+
client: ContactExportsApi,
125+
sample_create_contact_export_params: CreateContactExportParams,
126+
) -> None:
127+
response_body = {
128+
"errors": {
129+
"filters": "invalid",
130+
"base": [
131+
"There is a previous export initiated. "
132+
"You will be notified by email once it is completed."
133+
],
134+
}
135+
}
136+
responses.post(
137+
BASE_CONTACT_EXPORTS_URL,
138+
json=response_body,
139+
status=422,
140+
)
141+
142+
with pytest.raises(APIError) as exc_info:
143+
client.create(sample_create_contact_export_params)
144+
145+
assert exc_info.value.status == 422
146+
assert "filters: invalid" in str(exc_info.value.errors)
147+
assert (
148+
"base: There is a previous export initiated. "
149+
"You will be notified by email once it is completed."
150+
) in str(exc_info.value.errors)
151+
152+
@pytest.mark.parametrize(
153+
"status_code,response_json,expected_error_message",
154+
[
155+
(
156+
conftest.UNAUTHORIZED_STATUS_CODE,
157+
conftest.UNAUTHORIZED_RESPONSE,
158+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
159+
),
160+
(
161+
conftest.FORBIDDEN_STATUS_CODE,
162+
conftest.FORBIDDEN_RESPONSE,
163+
conftest.FORBIDDEN_ERROR_MESSAGE,
164+
),
165+
(
166+
conftest.NOT_FOUND_STATUS_CODE,
167+
conftest.NOT_FOUND_RESPONSE,
168+
conftest.NOT_FOUND_ERROR_MESSAGE,
169+
),
170+
],
171+
)
172+
@responses.activate
173+
def test_get_by_id_should_raise_api_errors(
174+
self,
175+
client: ContactExportsApi,
176+
status_code: int,
177+
response_json: dict,
178+
expected_error_message: str,
179+
) -> None:
180+
url = f"{BASE_CONTACT_EXPORTS_URL}/{EXPORT_ID}"
181+
responses.get(
182+
url,
183+
status=status_code,
184+
json=response_json,
185+
)
186+
187+
with pytest.raises(APIError) as exc_info:
188+
client.get_by_id(EXPORT_ID)
189+
190+
assert expected_error_message in str(exc_info.value)
191+
192+
@responses.activate
193+
def test_get_by_id_should_return_started_export(
194+
self, client: ContactExportsApi, sample_contact_export_started_dict: dict
195+
) -> None:
196+
url = f"{BASE_CONTACT_EXPORTS_URL}/{EXPORT_ID}"
197+
responses.get(
198+
url,
199+
json=sample_contact_export_started_dict,
200+
status=200,
201+
)
202+
203+
result = client.get_by_id(EXPORT_ID)
204+
205+
assert isinstance(result, ContactExportDetail)
206+
assert result.id == EXPORT_ID
207+
assert result.status == "started"
208+
assert result.url is None
209+
210+
@responses.activate
211+
def test_get_by_id_should_return_finished_export(
212+
self, client: ContactExportsApi, sample_contact_export_finished_dict: dict
213+
) -> None:
214+
url = f"{BASE_CONTACT_EXPORTS_URL}/{EXPORT_ID}"
215+
responses.get(
216+
url,
217+
json=sample_contact_export_finished_dict,
218+
status=200,
219+
)
220+
221+
result = client.get_by_id(EXPORT_ID)
222+
223+
assert isinstance(result, ContactExportDetail)
224+
assert result.id == EXPORT_ID
225+
assert result.status == "finished"
226+
assert result.url == "https://example.com/export.csv.gz"
227+
228+
@responses.activate
229+
def test_get_by_id_with_different_export_id_should_use_correct_url(
230+
self, client: ContactExportsApi, sample_contact_export_started_dict: dict
231+
) -> None:
232+
different_export_id = 999
233+
url = f"{BASE_CONTACT_EXPORTS_URL}/{different_export_id}"
234+
responses.get(
235+
url,
236+
json=sample_contact_export_started_dict,
237+
status=200,
238+
)
239+
240+
client.get_by_id(different_export_id)
241+
242+
assert len(responses.calls) == 1
243+
request = responses.calls[0].request
244+
assert request.url == url

0 commit comments

Comments
 (0)