Skip to content

Commit afcc75e

Browse files
committed
Add sub account endpoints
1 parent 9f078b3 commit afcc75e

11 files changed

Lines changed: 272 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,13 @@ The same situation applies to both `client.batch_send()` and `client.sending_api
256256
### General API:
257257
- Account Accesses management – [`general/account_accesses.py`](examples/general/account_accesses.py)
258258
- Accounts info – [`general/accounts.py`](examples/general/accounts.py)
259-
- API Tokens listing[`general/api_tokens.py`](examples/general/api_tokens.py)
259+
- API Tokens management[`general/api_tokens.py`](examples/general/api_tokens.py)
260260
- Billing info – [`general/billing.py`](examples/general/billing.py)
261261
- Permissions listing – [`general/permissions.py`](examples/general/permissions.py)
262262

263+
### Organizations API:
264+
- Sub-Accounts management – [`organizations/sub_accounts.py`](examples/organizations/sub_accounts.py)
265+
263266
## Contributing
264267

265268
Bug reports and pull requests are welcome on [GitHub](https://github.com/mailtrap/mailtrap-python). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).

examples/organizations/__init__.py

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import mailtrap as mt
2+
from mailtrap.models.organizations import SubAccount
3+
4+
API_TOKEN = "YOUR_API_TOKEN"
5+
ORGANIZATION_ID = "YOUR_ORGANIZATION_ID"
6+
7+
client = mt.MailtrapClient(token=API_TOKEN, organization_id=ORGANIZATION_ID)
8+
sub_accounts_api = client.organizations_api.sub_accounts
9+
10+
11+
def list_sub_accounts() -> list[SubAccount]:
12+
return sub_accounts_api.get_list()
13+
14+
15+
def create_sub_account(name: str) -> SubAccount:
16+
return sub_accounts_api.create(mt.CreateSubAccountParams(name=name))
17+
18+
19+
if __name__ == "__main__":
20+
sub_accounts = list_sub_accounts()
21+
print(sub_accounts)
22+
23+
created = create_sub_account("New Team Account")
24+
print(created)

mailtrap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .models.mail import Mail
3434
from .models.mail import MailFromTemplate
3535
from .models.messages import UpdateEmailMessageParams
36+
from .models.organizations import CreateSubAccountParams
3637
from .models.permissions import PermissionResourceParams
3738
from .models.projects import ProjectParams
3839
from .models.sending_domains import CreateSendingDomainParams

mailtrap/api/organizations.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from mailtrap.api.resources.sub_accounts import SubAccountsApi
2+
from mailtrap.http import HttpClient
3+
4+
5+
class OrganizationsBaseApi:
6+
def __init__(self, client: HttpClient, organization_id: str) -> None:
7+
self._organization_id = organization_id
8+
self._client = client
9+
10+
@property
11+
def sub_accounts(self) -> SubAccountsApi:
12+
return SubAccountsApi(
13+
organization_id=self._organization_id, client=self._client
14+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from mailtrap.http import HttpClient
2+
from mailtrap.models.organizations import CreateSubAccountParams
3+
from mailtrap.models.organizations import SubAccount
4+
5+
6+
class SubAccountsApi:
7+
def __init__(self, client: HttpClient, organization_id: str) -> None:
8+
self._organization_id = organization_id
9+
self._client = client
10+
11+
def get_list(self) -> list[SubAccount]:
12+
"""
13+
Get a list of sub accounts for the organization. Requires sub
14+
account management permissions for this organization.
15+
"""
16+
response = self._client.get(self._api_path())
17+
return [SubAccount(**sub_account) for sub_account in response]
18+
19+
def create(self, sub_account_params: CreateSubAccountParams) -> SubAccount:
20+
"""
21+
Create a new sub account under the organization. Requires sub
22+
account management permissions for this organization.
23+
"""
24+
response = self._client.post(
25+
self._api_path(),
26+
json={"account": sub_account_params.api_data},
27+
)
28+
return SubAccount(**response)
29+
30+
def _api_path(self) -> str:
31+
return f"/api/organizations/{self._organization_id}/sub_accounts"

mailtrap/client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from mailtrap.api.contacts import ContactsBaseApi
1010
from mailtrap.api.email_logs import EmailLogsBaseApi
1111
from mailtrap.api.general import GeneralApi
12+
from mailtrap.api.organizations import OrganizationsBaseApi
1213
from mailtrap.api.resources.stats import StatsApi
1314
from mailtrap.api.sending import SendingApi
1415
from mailtrap.api.sending_domains import SendingDomainsBaseApi
@@ -51,6 +52,7 @@ def __init__(
5152
sandbox: bool = False,
5253
account_id: Optional[str] = None,
5354
inbox_id: Optional[str] = None,
55+
organization_id: Optional[str] = None,
5456
user_agent: Optional[str] = None,
5557
) -> None:
5658
self.token = token
@@ -60,6 +62,7 @@ def __init__(
6062
self.sandbox = sandbox
6163
self.account_id = account_id
6264
self.inbox_id = inbox_id
65+
self.organization_id = organization_id
6366
self._user_agent = (
6467
user_agent if user_agent is not None else self.DEFAULT_USER_AGENT
6568
)
@@ -121,6 +124,14 @@ def email_logs_api(self) -> EmailLogsBaseApi:
121124
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
122125
)
123126

127+
@property
128+
def organizations_api(self) -> OrganizationsBaseApi:
129+
self._validate_organization_id("Organizations API")
130+
return OrganizationsBaseApi(
131+
organization_id=cast(str, self.organization_id),
132+
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
133+
)
134+
124135
@property
125136
def sending_api(self) -> SendingApi:
126137
http_client = HttpClient(host=self._sending_api_host, headers=self.headers)
@@ -189,6 +200,12 @@ def _validate_account_id(self, api_name: str = "Testing API") -> None:
189200
if not self.account_id:
190201
raise ClientConfigurationError(f"`account_id` is required for {api_name}")
191202

203+
def _validate_organization_id(self, api_name: str) -> None:
204+
if not self.organization_id:
205+
raise ClientConfigurationError(
206+
f"`organization_id` is required for {api_name}"
207+
)
208+
192209
def _validate_itself(self) -> None:
193210
if self.sandbox and not self.inbox_id:
194211
raise ClientConfigurationError("`inbox_id` is required for sandbox mode")

mailtrap/models/organizations.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from pydantic.dataclasses import dataclass
2+
3+
from mailtrap.models.common import RequestParams
4+
5+
6+
@dataclass
7+
class SubAccount:
8+
id: int
9+
name: str
10+
11+
12+
@dataclass
13+
class CreateSubAccountParams(RequestParams):
14+
name: str

tests/unit/api/organizations/__init__.py

Whitespace-only changes.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from typing import Any
2+
3+
import pytest
4+
import responses
5+
6+
from mailtrap.api.resources.sub_accounts import SubAccountsApi
7+
from mailtrap.config import GENERAL_HOST
8+
from mailtrap.exceptions import APIError
9+
from mailtrap.http import HttpClient
10+
from mailtrap.models.organizations import CreateSubAccountParams
11+
from mailtrap.models.organizations import SubAccount
12+
from tests import conftest
13+
14+
ORGANIZATION_ID = "1001"
15+
SUB_ACCOUNT_ID = 12345
16+
BASE_SUB_ACCOUNTS_URL = (
17+
f"https://{GENERAL_HOST}/api/organizations/{ORGANIZATION_ID}/sub_accounts"
18+
)
19+
20+
21+
@pytest.fixture
22+
def client() -> SubAccountsApi:
23+
return SubAccountsApi(
24+
client=HttpClient(GENERAL_HOST), organization_id=ORGANIZATION_ID
25+
)
26+
27+
28+
@pytest.fixture
29+
def sample_sub_account_dict() -> dict[str, Any]:
30+
return {"id": SUB_ACCOUNT_ID, "name": "Development Team Account"}
31+
32+
33+
class TestSubAccountsApi:
34+
35+
@pytest.mark.parametrize(
36+
"status_code,response_json,expected_error_message",
37+
[
38+
(
39+
conftest.UNAUTHORIZED_STATUS_CODE,
40+
conftest.UNAUTHORIZED_RESPONSE,
41+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
42+
),
43+
(
44+
conftest.FORBIDDEN_STATUS_CODE,
45+
conftest.FORBIDDEN_RESPONSE,
46+
conftest.FORBIDDEN_ERROR_MESSAGE,
47+
),
48+
],
49+
)
50+
@responses.activate
51+
def test_get_list_should_raise_api_errors(
52+
self,
53+
client: SubAccountsApi,
54+
status_code: int,
55+
response_json: dict,
56+
expected_error_message: str,
57+
) -> None:
58+
responses.get(
59+
BASE_SUB_ACCOUNTS_URL,
60+
status=status_code,
61+
json=response_json,
62+
)
63+
64+
with pytest.raises(APIError) as exc_info:
65+
client.get_list()
66+
67+
assert expected_error_message in str(exc_info.value)
68+
69+
@responses.activate
70+
def test_get_list_should_return_sub_accounts_list(
71+
self, client: SubAccountsApi, sample_sub_account_dict: dict
72+
) -> None:
73+
responses.get(
74+
BASE_SUB_ACCOUNTS_URL,
75+
json=[
76+
sample_sub_account_dict,
77+
{"id": 12346, "name": "QA Team Account"},
78+
],
79+
status=200,
80+
)
81+
82+
sub_accounts = client.get_list()
83+
84+
assert isinstance(sub_accounts, list)
85+
assert all(isinstance(s, SubAccount) for s in sub_accounts)
86+
assert len(sub_accounts) == 2
87+
assert sub_accounts[0].id == SUB_ACCOUNT_ID
88+
assert sub_accounts[0].name == "Development Team Account"
89+
90+
@responses.activate
91+
def test_get_list_should_return_empty_list(self, client: SubAccountsApi) -> None:
92+
responses.get(BASE_SUB_ACCOUNTS_URL, json=[], status=200)
93+
94+
sub_accounts = client.get_list()
95+
96+
assert isinstance(sub_accounts, list)
97+
assert len(sub_accounts) == 0
98+
99+
@pytest.mark.parametrize(
100+
"status_code,response_json,expected_error_message",
101+
[
102+
(
103+
conftest.UNAUTHORIZED_STATUS_CODE,
104+
conftest.UNAUTHORIZED_RESPONSE,
105+
conftest.UNAUTHORIZED_ERROR_MESSAGE,
106+
),
107+
(
108+
conftest.FORBIDDEN_STATUS_CODE,
109+
conftest.FORBIDDEN_RESPONSE,
110+
conftest.FORBIDDEN_ERROR_MESSAGE,
111+
),
112+
(
113+
conftest.VALIDATION_ERRORS_STATUS_CODE,
114+
{"errors": "Name is invalid"},
115+
"Name is invalid",
116+
),
117+
],
118+
)
119+
@responses.activate
120+
def test_create_should_raise_api_errors(
121+
self,
122+
client: SubAccountsApi,
123+
status_code: int,
124+
response_json: dict,
125+
expected_error_message: str,
126+
) -> None:
127+
responses.post(
128+
BASE_SUB_ACCOUNTS_URL, status=status_code, json=response_json
129+
)
130+
131+
with pytest.raises(APIError) as exc_info:
132+
client.create(CreateSubAccountParams(name="New Team Account"))
133+
134+
assert expected_error_message in str(exc_info.value)
135+
136+
@responses.activate
137+
def test_create_should_return_sub_account_and_wrap_body_under_account_key(
138+
self, client: SubAccountsApi, sample_sub_account_dict: dict
139+
) -> None:
140+
responses.post(
141+
BASE_SUB_ACCOUNTS_URL,
142+
json={"id": 12347, "name": "New Team Account"},
143+
status=200,
144+
)
145+
146+
sub_account = client.create(
147+
CreateSubAccountParams(name="New Team Account")
148+
)
149+
150+
assert isinstance(sub_account, SubAccount)
151+
assert sub_account.id == 12347
152+
assert sub_account.name == "New Team Account"
153+
154+
assert len(responses.calls) == 1
155+
assert (
156+
responses.calls[0].request.body
157+
== b'{"account": {"name": "New Team Account"}}'
158+
)

0 commit comments

Comments
 (0)