Skip to content

Commit 16ac0ab

Browse files
Ihor BilousIhor Bilous
authored andcommitted
Fix issue #29. Add support of Emails Sandbox (Testing) API: Projects
1 parent 709f096 commit 16ac0ab

File tree

8 files changed

+378
-3
lines changed

8 files changed

+378
-3
lines changed

mailtrap/api/base.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from abc import ABC
2+
from enum import Enum
3+
from typing import Any, Dict, List, NoReturn, Union, cast
4+
from requests import Response, Session
5+
6+
from mailtrap.exceptions import APIError, AuthorizationError
7+
8+
RESPONSE_TYPE = Dict[str, Any]
9+
LIST_RESPONSE_TYPE = List[Dict[str, Any]]
10+
11+
12+
class HttpMethod(Enum):
13+
GET = "GET"
14+
POST = "POST"
15+
PUT = "PUT"
16+
PATCH = "PATCH"
17+
DELTE = "DELETE"
18+
19+
20+
def _extract_errors(data: Dict[str, Any]) -> List[str]:
21+
if "errors" in data:
22+
errors = data["errors"]
23+
24+
if isinstance(errors, list):
25+
return [str(err) for err in errors]
26+
27+
if isinstance(errors, dict):
28+
flat_errors = []
29+
for key, value in errors.items():
30+
if isinstance(value, list):
31+
flat_errors.extend([f"{key}: {v}" for v in value])
32+
else:
33+
flat_errors.append(f"{key}: {value}")
34+
return flat_errors
35+
36+
return [str(errors)]
37+
38+
elif "error" in data:
39+
return [str(data["error"])]
40+
41+
return ["Unknown error"]
42+
43+
44+
class BaseHttpApiClient(ABC):
45+
def __init__(self, session: Session):
46+
self.session = session
47+
48+
def _request(self, method: HttpMethod, url: str, **kwargs: Any) -> Union[RESPONSE_TYPE, LIST_RESPONSE_TYPE]:
49+
response = self.session.request(method.value, url, **kwargs)
50+
if response.ok:
51+
data = cast(Union[RESPONSE_TYPE, LIST_RESPONSE_TYPE], response.json())
52+
return data
53+
54+
self._handle_failed_response(response)
55+
56+
@staticmethod
57+
def _handle_failed_response(response: Response) -> NoReturn:
58+
status_code = response.status_code
59+
60+
try:
61+
data = response.json()
62+
except ValueError:
63+
raise APIError(status_code, errors=["Unknown Error"])
64+
65+
errors = _extract_errors(data)
66+
67+
if status_code == 401:
68+
raise AuthorizationError(errors=errors)
69+
70+
raise APIError(status_code, errors=errors)
71+

mailtrap/api/projects.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import List, cast
2+
3+
from mailtrap.api.base import LIST_RESPONSE_TYPE, RESPONSE_TYPE, BaseHttpApiClient, HttpMethod
4+
from mailtrap.constants import MAILTRAP_HOST
5+
from mailtrap.models.common import DeletedObject
6+
from mailtrap.models.projects import Project
7+
8+
9+
class ProjectsApiClient(BaseHttpApiClient):
10+
11+
def _build_url(self, account_id: str, *parts: str) -> str:
12+
base_url = f"https://{MAILTRAP_HOST}/api/accounts/{account_id}/projects"
13+
return "/".join([base_url, *parts])
14+
15+
def get_list(self, account_id: str) -> List[Project]:
16+
response: LIST_RESPONSE_TYPE = cast(LIST_RESPONSE_TYPE, self._request(
17+
HttpMethod.GET,
18+
self._build_url(account_id)
19+
))
20+
return [Project.from_dict(proj) for proj in response]
21+
22+
def get_by_id(self, account_id: str, project_id: str) -> Project:
23+
response: RESPONSE_TYPE = cast(RESPONSE_TYPE, self._request(
24+
HttpMethod.GET,
25+
self._build_url(account_id, project_id)
26+
))
27+
return Project.from_dict(response)
28+
29+
def create(self, account_id: str, name: str) -> Project:
30+
response: RESPONSE_TYPE = cast(RESPONSE_TYPE, self._request(
31+
HttpMethod.POST,
32+
self._build_url(account_id),
33+
json={"project": {"name": name}},
34+
))
35+
return Project.from_dict(response)
36+
37+
def update(self, account_id: str, project_id: str, name: str) -> Project:
38+
response: RESPONSE_TYPE = cast(RESPONSE_TYPE, self._request(
39+
HttpMethod.PATCH,
40+
self._build_url(account_id, project_id),
41+
json={"project": {"name": name}},
42+
))
43+
return Project.from_dict(response)
44+
45+
def delete(self, account_id: str, project_id: str) -> DeletedObject:
46+
response: RESPONSE_TYPE = cast(RESPONSE_TYPE, self._request(
47+
HttpMethod.DELTE,
48+
self._build_url(account_id, project_id),
49+
))
50+
return DeletedObject(response["id"])

mailtrap/client.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import requests
66

7+
from mailtrap.api.projects import ProjectsApiClient
78
from mailtrap.exceptions import APIError
89
from mailtrap.exceptions import AuthorizationError
910
from mailtrap.exceptions import ClientConfigurationError
@@ -34,10 +35,15 @@ def __init__(
3435

3536
self._validate_itself()
3637

38+
self._http_client = requests.Session()
39+
self._http_client.headers.update(self.headers)
40+
41+
@property
42+
def projects_api(self) -> ProjectsApiClient:
43+
return ProjectsApiClient(self._http_client)
44+
3745
def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]:
38-
response = requests.post(
39-
self.api_send_url, headers=self.headers, json=mail.api_data
40-
)
46+
response = self._http_client.post(self.api_send_url, json=mail.api_data)
4147

4248
if response.ok:
4349
data: dict[str, Union[bool, list[str]]] = response.json()

mailtrap/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MAILTRAP_HOST = "mailtrap.io"

mailtrap/models/common.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class DeletedObject:
2+
def __init__(self, id: str):
3+
self.id = id

mailtrap/models/projects.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import Any, List, Dict
2+
3+
4+
class ShareLinks:
5+
def __init__(self, admin: str, viewer: str):
6+
self.admin = admin
7+
self.viewer = viewer
8+
9+
10+
class Permissions:
11+
def __init__(
12+
self,
13+
can_read: bool,
14+
can_update: bool,
15+
can_destroy: bool,
16+
can_leave: bool
17+
):
18+
self.can_read = can_read
19+
self.can_update = can_update
20+
self.can_destroy = can_destroy
21+
self.can_leave = can_leave
22+
23+
24+
class Project:
25+
def __init__(
26+
self,
27+
id: str,
28+
name: str,
29+
share_links: ShareLinks,
30+
inboxes: List[Dict[str, Any]],
31+
permissions: Permissions
32+
):
33+
self.id = id
34+
self.name = name
35+
self.share_links = share_links
36+
self.inboxes = inboxes
37+
self.permissions = permissions
38+
39+
@classmethod
40+
def from_dict(cls, data: Dict[str, Any]) -> "Project":
41+
share_links = ShareLinks(**data["share_links"])
42+
permissions = Permissions(**data["permissions"])
43+
inboxes = data.get("inboxes", [])
44+
return cls(
45+
id=data["id"],
46+
name=data["name"],
47+
share_links=share_links,
48+
inboxes=inboxes,
49+
permissions=permissions,
50+
)

tests/unit/api/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)