Skip to content

Commit fe863ce

Browse files
Ihor BilousIhor Bilous
authored andcommitted
Fix issue #22: Add ContactImportsApi, related models, tests, examples
1 parent 66545dc commit fe863ce

File tree

7 files changed

+337
-0
lines changed

7 files changed

+337
-0
lines changed

examples/contacts/contact_imports.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import mailtrap as mt
2+
from mailtrap.models.contacts import ContactImport
3+
4+
API_TOKEN = "YOU_API_TOKEN"
5+
ACCOUNT_ID = "YOU_ACCOUNT_ID"
6+
7+
client = mt.MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID)
8+
contact_imports_api = client.contacts_api.contact_imports
9+
10+
11+
def import_contacts(contacts: list[mt.ImportContactParams]) -> ContactImport:
12+
return contact_imports_api.import_contacts(contacts=contacts)
13+
14+
15+
def get_contact_import(import_id: int) -> ContactImport:
16+
return contact_imports_api.get_by_id(import_id)
17+
18+
19+
if __name__ == "__main__":
20+
contact_import = import_contacts(
21+
contacts=[
22+
mt.ImportContactParams(
23+
24+
fields={"first_name": "Test", "last_name": "Test"},
25+
)
26+
]
27+
)
28+
print(contact_import)
29+
30+
contact_import = get_contact_import(contact_import.id)
31+
print(contact_import)

mailtrap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .models.contacts import ContactListParams
88
from .models.contacts import CreateContactFieldParams
99
from .models.contacts import CreateContactParams
10+
from .models.contacts import ImportContactParams
1011
from .models.contacts import UpdateContactFieldParams
1112
from .models.contacts import UpdateContactParams
1213
from .models.mail import Address

mailtrap/api/contacts.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from mailtrap.api.resources.contact_fields import ContactFieldsApi
2+
from mailtrap.api.resources.contact_imports import ContactImportsApi
23
from mailtrap.api.resources.contact_lists import ContactListsApi
34
from mailtrap.api.resources.contacts import ContactsApi
45
from mailtrap.http import HttpClient
@@ -17,6 +18,10 @@ def contact_fields(self) -> ContactFieldsApi:
1718
def contact_lists(self) -> ContactListsApi:
1819
return ContactListsApi(account_id=self._account_id, client=self._client)
1920

21+
@property
22+
def contact_imports(self) -> ContactImportsApi:
23+
return ContactImportsApi(account_id=self._account_id, client=self._client)
24+
2025
@property
2126
def contacts(self) -> ContactsApi:
2227
return ContactsApi(account_id=self._account_id, client=self._client)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import Optional
2+
3+
from mailtrap.http import HttpClient
4+
from mailtrap.models.contacts import ContactImport
5+
from mailtrap.models.contacts import ImportContactParams
6+
7+
8+
class ContactImportsApi:
9+
def __init__(self, client: HttpClient, account_id: str) -> None:
10+
self._account_id = account_id
11+
self._client = client
12+
13+
def import_contacts(self, contacts: list[ImportContactParams]) -> ContactImport:
14+
response = self._client.post(
15+
self._api_path(),
16+
json={"contacts": [contact.api_data for contact in contacts]},
17+
)
18+
return ContactImport(**response)
19+
20+
def get_by_id(self, import_id: int) -> ContactImport:
21+
response = self._client.get(self._api_path(import_id))
22+
return ContactImport(**response)
23+
24+
def _api_path(self, import_id: Optional[int] = None) -> str:
25+
path = f"/api/accounts/{self._account_id}/contacts/imports"
26+
if import_id is not None:
27+
return f"{path}/{import_id}"
28+
return path

mailtrap/models/contacts.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,34 @@ class Contact:
9595
@dataclass
9696
class ContactResponse:
9797
data: Contact
98+
99+
100+
class ContactImportStatus(str, Enum):
101+
CREATED = "created"
102+
STARTED = "started"
103+
FINISHED = "finished"
104+
FAILED = "failed"
105+
106+
107+
@dataclass
108+
class ContactImport:
109+
id: int
110+
status: ContactImportStatus
111+
created_contacts_count: Optional[int] = None
112+
updated_contacts_count: Optional[int] = None
113+
contacts_over_limit_count: Optional[int] = None
114+
115+
116+
@dataclass
117+
class ImportContactParams(RequestParams):
118+
email: str
119+
fields: Optional[dict[str, Union[str, int, float, bool]]] = (
120+
None # field_merge_tag: value
121+
)
122+
list_ids_included: Optional[list[int]] = None
123+
list_ids_excluded: Optional[list[int]] = None
124+
125+
126+
@dataclass
127+
class ContactImportRequest(RequestParams):
128+
contacts: list[CreateContactParams]
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from typing import Any
2+
3+
import pytest
4+
import responses
5+
6+
from mailtrap.api.resources.contact_imports import ContactImportsApi
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 ContactImport
11+
from mailtrap.models.contacts import ContactImportStatus
12+
from mailtrap.models.contacts import ImportContactParams
13+
from tests import conftest
14+
15+
ACCOUNT_ID = "321"
16+
IMPORT_ID = 1234
17+
BASE_CONTACT_IMPORTS_URL = (
18+
f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/contacts/imports"
19+
)
20+
21+
22+
@pytest.fixture
23+
def contact_imports_api() -> ContactImportsApi:
24+
return ContactImportsApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST))
25+
26+
27+
@pytest.fixture
28+
def sample_contact_import_dict() -> dict[str, Any]:
29+
return {
30+
"id": IMPORT_ID,
31+
"status": "started",
32+
}
33+
34+
35+
@pytest.fixture
36+
def sample_finished_contact_import_dict() -> dict[str, Any]:
37+
return {
38+
"id": IMPORT_ID,
39+
"status": "finished",
40+
"created_contacts_count": 1,
41+
"updated_contacts_count": 3,
42+
"contacts_over_limit_count": 3,
43+
}
44+
45+
46+
@pytest.fixture
47+
def import_contacts_params() -> list[ImportContactParams]:
48+
return [
49+
ImportContactParams(
50+
51+
fields={"first_name": "John", "last_name": "Smith"},
52+
list_ids_included=[1],
53+
list_ids_excluded=[2],
54+
),
55+
ImportContactParams(
56+
57+
fields={"first_name": "John", "last_name": "Doe"},
58+
list_ids_included=[3],
59+
list_ids_excluded=[4],
60+
),
61+
]
62+
63+
64+
class TestContactImportsApi:
65+
66+
@pytest.mark.parametrize(
67+
"status_code,response_json,expected_error_message",
68+
[
69+
(
70+
conftest.UNAUTHORIZED_STATUS_CODE,
71+
conftest.UNAUTHORIZED_RESPONSE,
72+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
73+
),
74+
(
75+
conftest.FORBIDDEN_STATUS_CODE,
76+
conftest.FORBIDDEN_RESPONSE,
77+
conftest.FORBIDDEN_ERROR_MESSAGE,
78+
),
79+
(
80+
conftest.VALIDATION_ERRORS_STATUS_CODE,
81+
{
82+
"errors": [
83+
{
84+
"email": "[email protected]",
85+
"errors": {
86+
"base": [
87+
"contacts limit reached",
88+
"cannot import more than 50000 contacts at once",
89+
],
90+
},
91+
}
92+
]
93+
},
94+
"contacts limit reached",
95+
),
96+
],
97+
)
98+
@responses.activate
99+
def test_import_contacts_should_raise_api_errors(
100+
self,
101+
contact_imports_api: ContactImportsApi,
102+
import_contacts_params: list[ImportContactParams],
103+
status_code: int,
104+
response_json: dict,
105+
expected_error_message: str,
106+
) -> None:
107+
responses.post(
108+
BASE_CONTACT_IMPORTS_URL,
109+
status=status_code,
110+
json=response_json,
111+
)
112+
113+
with pytest.raises(APIError) as exc_info:
114+
_ = contact_imports_api.import_contacts(import_contacts_params)
115+
116+
assert expected_error_message in str(exc_info.value)
117+
118+
@responses.activate
119+
def test_import_contacts_should_return_started_import(
120+
self,
121+
contact_imports_api: ContactImportsApi,
122+
import_contacts_params: list[ImportContactParams],
123+
) -> None:
124+
expected_response = {
125+
"id": IMPORT_ID,
126+
"status": "started",
127+
}
128+
responses.post(
129+
BASE_CONTACT_IMPORTS_URL,
130+
json=expected_response,
131+
status=201,
132+
)
133+
134+
contact_import = contact_imports_api.import_contacts(import_contacts_params)
135+
136+
assert isinstance(contact_import, ContactImport)
137+
assert contact_import.id == IMPORT_ID
138+
assert contact_import.status == ContactImportStatus.STARTED
139+
140+
@pytest.mark.parametrize(
141+
"status_code,response_json,expected_error_message",
142+
[
143+
(
144+
conftest.UNAUTHORIZED_STATUS_CODE,
145+
conftest.UNAUTHORIZED_RESPONSE,
146+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
147+
),
148+
(
149+
conftest.FORBIDDEN_STATUS_CODE,
150+
conftest.FORBIDDEN_RESPONSE,
151+
conftest.FORBIDDEN_ERROR_MESSAGE,
152+
),
153+
(
154+
conftest.NOT_FOUND_STATUS_CODE,
155+
conftest.NOT_FOUND_RESPONSE,
156+
conftest.NOT_FOUND_ERROR_MESSAGE,
157+
),
158+
],
159+
)
160+
@responses.activate
161+
def test_get_contact_import_should_raise_api_errors(
162+
self,
163+
contact_imports_api: ContactImportsApi,
164+
status_code: int,
165+
response_json: dict,
166+
expected_error_message: str,
167+
) -> None:
168+
responses.get(
169+
f"{BASE_CONTACT_IMPORTS_URL}/{IMPORT_ID}",
170+
status=status_code,
171+
json=response_json,
172+
)
173+
174+
with pytest.raises(APIError) as exc_info:
175+
contact_imports_api.get_by_id(IMPORT_ID)
176+
177+
assert expected_error_message in str(exc_info.value)
178+
179+
@responses.activate
180+
def test_get_contact_import_should_return_started_import(
181+
self, contact_imports_api: ContactImportsApi, sample_contact_import_dict: dict
182+
) -> None:
183+
responses.get(
184+
f"{BASE_CONTACT_IMPORTS_URL}/{IMPORT_ID}",
185+
json=sample_contact_import_dict,
186+
status=200,
187+
)
188+
189+
contact_import = contact_imports_api.get_by_id(IMPORT_ID)
190+
191+
assert isinstance(contact_import, ContactImport)
192+
assert contact_import.id == IMPORT_ID
193+
assert contact_import.status == ContactImportStatus.STARTED
194+
195+
@responses.activate
196+
def test_get_contact_import_should_return_finished_import(
197+
self,
198+
contact_imports_api: ContactImportsApi,
199+
sample_finished_contact_import_dict: dict,
200+
) -> None:
201+
responses.get(
202+
f"{BASE_CONTACT_IMPORTS_URL}/{IMPORT_ID}",
203+
json=sample_finished_contact_import_dict,
204+
status=200,
205+
)
206+
207+
contact_import = contact_imports_api.get_by_id(IMPORT_ID)
208+
209+
assert isinstance(contact_import, ContactImport)
210+
assert contact_import.id == IMPORT_ID
211+
assert contact_import.status == ContactImportStatus.FINISHED
212+
assert contact_import.created_contacts_count == 1
213+
assert contact_import.updated_contacts_count == 3
214+
assert contact_import.contacts_over_limit_count == 3

tests/unit/models/test_contacts.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from mailtrap.models.contacts import ContactListParams
44
from mailtrap.models.contacts import CreateContactFieldParams
55
from mailtrap.models.contacts import CreateContactParams
6+
from mailtrap.models.contacts import ImportContactParams
67
from mailtrap.models.contacts import UpdateContactFieldParams
78
from mailtrap.models.contacts import UpdateContactParams
89

@@ -120,3 +121,29 @@ def test_update_contact_params_api_data_should_return_correct_dicts(
120121
"list_ids_excluded": [1],
121122
"unsubscribed": False,
122123
}
124+
125+
126+
class TestImportContactParams:
127+
def test_import_contact_params_api_data_should_exclude_none_values(
128+
self,
129+
) -> None:
130+
params = ImportContactParams(email="[email protected]")
131+
api_data = params.api_data
132+
assert api_data == {"email": "[email protected]"}
133+
134+
def test_import_contact_params_api_data_should_return_correct_dicts(
135+
self,
136+
) -> None:
137+
params = ImportContactParams(
138+
139+
fields={"first_name": "Test"},
140+
list_ids_included=[1],
141+
list_ids_excluded=[2],
142+
)
143+
api_data = params.api_data
144+
assert api_data == {
145+
"email": "[email protected]",
146+
"fields": {"first_name": "Test"},
147+
"list_ids_included": [1],
148+
"list_ids_excluded": [2],
149+
}

0 commit comments

Comments
 (0)