Skip to content

Commit 26d8c57

Browse files
Ihor BilousIhor Bilous
authored andcommitted
Fixx issue #29. Start using pydantic models instead of dataclasses
1 parent 3723d35 commit 26d8c57

File tree

18 files changed

+123
-162
lines changed

18 files changed

+123
-162
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ dist/
88
# IntelliJ's project specific settings
99
.idea/
1010

11+
# VSCode's project specific settings
12+
.vscode/
13+
1114
# mypy
1215
.mypy_cache/
1316

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Versions of this package up to 1.0.1 were a different, unrelated project, that i
1717

1818
### Prerequisites
1919

20-
- Python version 3.6+
20+
- Python version 3.9+
2121

2222
### Install package
2323

@@ -150,7 +150,7 @@ To setup virtual environments, run tests and linters use:
150150
tox
151151
```
152152

153-
It will create virtual environments with all installed dependencies for each available python interpreter (starting from `python3.6`) on your machine.
153+
It will create virtual environments with all installed dependencies for each available python interpreter (starting from `python3.9`) on your machine.
154154
By default, they will be available in `{project}/.tox/` directory. So, for instance, to activate `python3.11` environment, run the following:
155155

156156
```bash

examples/testing/projects.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import os
2-
import sys
3-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
4-
51
from typing import Optional
2+
63
from mailtrap import MailtrapClient
7-
from mailtrap.models.projects import Project
4+
from mailtrap.schemas.projects import Project
85

96
API_TOKEN = "YOU_API_TOKEN"
107
ACCOUNT_ID = "YOU_ACCOUNT_ID"
@@ -17,14 +14,15 @@ def find_project_by_name(project_name: str, projects: list[Project]) -> Optional
1714
return None
1815

1916

20-
client = MailtrapClient(token=API_TOKEN, account_id=ACCOUNT_ID)
21-
api = client.testing.projects
17+
MailtrapClient.configure_access_token(API_TOKEN)
18+
testing_api = MailtrapClient.get_testing_api(ACCOUNT_ID)
19+
projects_api = testing_api.projects
2220

2321
project_name = "Example-project"
2422

25-
created_project = api.create(project_name=project_name)
26-
projects = api.get_list()
23+
created_project = projects_api.create(project_name=project_name)
24+
projects = projects_api.get_list()
2725
project_id = find_project_by_name(project_name, projects)
28-
project = api.get_by_id(project_id)
29-
updated_projected = api.update(project_id, "Updated-project-name")
30-
api.delete(project_id)
26+
project = projects_api.get_by_id(project_id)
27+
updated_projected = projects_api.update(project_id, "Updated-project-name")
28+
deleted_object = projects_api.delete(project_id)

mailtrap/api/resources/projects.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
from mailtrap.http import HttpClient
2-
from mailtrap.models.base import DeletedObject
3-
from mailtrap.models.projects import Project
2+
from mailtrap.schemas.base import DeletedObject
3+
from mailtrap.schemas.projects import Project
44

55

66
class ProjectsApi:
7-
def __init__(self, account_id: str, client: HttpClient) -> None:
7+
def __init__(self, client: HttpClient, account_id: str) -> None:
88
self.account_id = account_id
99
self.client = client
1010

1111
def get_list(self) -> list[Project]:
1212
response = self.client.list(f"/api/accounts/{self.account_id}/projects")
13-
return [Project.from_dict(project) for project in response]
13+
return [Project(**project) for project in response]
1414

15-
def get_by_id(self, project_id: str) -> Project:
16-
response = self.client.get(f"/api/accounts/{self.account_id}/projects/{project_id}")
17-
return Project.from_dict(response)
15+
def get_by_id(self, project_id: int) -> Project:
16+
response = self.client.get(
17+
f"/api/accounts/{self.account_id}/projects/{project_id}"
18+
)
19+
return Project(**response)
1820

1921
def create(self, project_name: str) -> Project:
2022
response = self.client.post(
2123
f"/api/accounts/{self.account_id}/projects",
2224
json={"project": {"name": project_name}},
2325
)
24-
return Project.from_dict(response)
26+
return Project(**response)
2527

26-
def update(self, project_id: str, project_name: str) -> Project:
28+
def update(self, project_id: int, project_name: str) -> Project:
2729
response = self.client.patch(
2830
f"/api/accounts/{self.account_id}/projects/{project_id}",
2931
json={"project": {"name": project_name}},
3032
)
31-
return Project.from_dict(response)
33+
return Project(**response)
3234

33-
def delete(self, project_id: str) -> DeletedObject:
35+
def delete(self, project_id: int) -> DeletedObject:
3436
response = self.client.delete(
3537
f"/api/accounts/{self.account_id}/projects/{project_id}",
3638
)
37-
return DeletedObject(response["id"])
39+
return DeletedObject(**response)

mailtrap/api/testing.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
from typing import Optional
12

2-
from mailtrap.api.resources.projects import ProjectsApi
3+
from mailtrap.api.resources.projects import ProjectsApi
34
from mailtrap.http import HttpClient
45

56

67
class TestingApi:
7-
def __init__(self, account_id: str, client: HttpClient) -> None:
8+
def __init__(
9+
self, client: HttpClient, account_id: str, inbox_id: Optional[str] = None
10+
) -> None:
811
self.account_id = account_id
12+
self.inbox_id = inbox_id
913
self.client = client
1014

1115
@property

mailtrap/client.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,38 @@ class MailtrapClient:
1919
BULK_HOST = "bulk.api.mailtrap.io"
2020
SANDBOX_HOST = "sandbox.api.mailtrap.io"
2121

22+
_default_token: Optional[str] = None
23+
2224
def __init__(
2325
self,
24-
token: str,
2526
api_host: Optional[str] = None,
2627
api_port: int = DEFAULT_PORT,
2728
bulk: bool = False,
2829
sandbox: bool = False,
2930
inbox_id: Optional[str] = None,
30-
account_id: Optional[str] = None,
3131
) -> None:
32-
self.token = token
3332
self.api_host = api_host
3433
self.api_port = api_port
3534
self.bulk = bulk
3635
self.sandbox = sandbox
3736
self.inbox_id = inbox_id
38-
self.account_id = account_id
3937

4038
self._validate_itself()
4139

42-
@property
43-
def testing(self) -> TestingApi:
44-
if not self.account_id:
45-
raise ClientConfigurationError("`account_id` is required for Testing API")
40+
@classmethod
41+
def configure_access_token(cls, token: str) -> None:
42+
cls._default_token = token
4643

47-
http_client = HttpClient(host=MAILTRAP_HOST, headers=self.headers)
48-
return TestingApi(account_id=self.account_id, client=http_client)
44+
@classmethod
45+
def get_testing_api(
46+
cls, account_id: str, inbox_id: Optional[str] = None
47+
) -> TestingApi:
48+
http_client = HttpClient(host=MAILTRAP_HOST, headers=cls.get_default_headers())
49+
return TestingApi(account_id=account_id, inbox_id=inbox_id, client=http_client)
4950

5051
def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]:
5152
response = requests.post(
52-
self.api_send_url, headers=self.headers, json=mail.api_data
53+
self.api_send_url, headers=self.get_default_headers(), json=mail.api_data
5354
)
5455

5556
if response.ok:
@@ -70,10 +71,16 @@ def api_send_url(self) -> str:
7071

7172
return url
7273

73-
@property
74-
def headers(self) -> dict[str, str]:
74+
@classmethod
75+
def get_default_headers(cls) -> dict[str, str]:
76+
if cls._default_token is None:
77+
raise ValueError(
78+
"Access token is not configured. "
79+
"Call MailtrapClient.configure_token(...) first."
80+
)
81+
7582
return {
76-
"Authorization": f"Bearer {self.token}",
83+
"Authorization": f"Bearer {cls._default_token}",
7784
"Content-Type": "application/json",
7885
"User-Agent": (
7986
"mailtrap-python (https://github.com/railsware/mailtrap-python)"

mailtrap/http.py

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from typing import Any, Optional, Type, TypeVar
1+
from typing import Any
22
from typing import NoReturn
3+
from typing import Optional
4+
from typing import TypeVar
35

4-
from requests import Response, Session
6+
from requests import Response
7+
from requests import Session
58

69
from mailtrap.config import DEFAULT_REQUEST_TIMEOUT
710
from mailtrap.exceptions import APIError
@@ -12,10 +15,10 @@
1215

1316
class HttpClient:
1417
def __init__(
15-
self,
16-
host: str,
17-
headers: Optional[dict[str, str]] = None,
18-
timeout: int = DEFAULT_REQUEST_TIMEOUT
18+
self,
19+
host: str,
20+
headers: Optional[dict[str, str]] = None,
21+
timeout: int = DEFAULT_REQUEST_TIMEOUT,
1922
):
2023
self._host = host
2124
self._session = Session()
@@ -39,16 +42,15 @@ def _handle_failed_response(self, response: Response) -> NoReturn:
3942

4043
raise APIError(status_code, errors=errors)
4144

42-
def _process_response(
43-
self,
44-
response: Response,
45-
expected_type: Type[T]
46-
) -> T:
45+
def _process_response(self, response: Response, expected_type: type[T]) -> T:
4746
if not response.ok:
4847
self._handle_failed_response(response)
4948
data = response.json()
5049
if not isinstance(data, expected_type):
51-
raise APIError(response.status_code, errors=[f"Expected response type {expected_type.__name__}"])
50+
raise APIError(
51+
response.status_code,
52+
errors=[f"Expected response type {expected_type.__name__}"],
53+
)
5254
return data
5355

5456
def _process_response_dict(self, response: Response) -> dict[str, Any]:
@@ -57,43 +59,29 @@ def _process_response_dict(self, response: Response) -> dict[str, Any]:
5759
def _process_response_list(self, response: Response) -> list[dict[str, Any]]:
5860
return self._process_response(response, list)
5961

60-
def get(
61-
self,
62-
path: str,
63-
params: Optional[dict[str, Any]] = None
64-
) -> dict[str, Any]:
65-
response = self._session.get(self._url(path), params=params, timeout=self._timeout)
62+
def get(self, path: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]:
63+
response = self._session.get(
64+
self._url(path), params=params, timeout=self._timeout
65+
)
6666
return self._process_response_dict(response)
67-
67+
6868
def list(
69-
self,
70-
path: str,
71-
params: Optional[dict[str, Any]] = None
69+
self, path: str, params: Optional[dict[str, Any]] = None
7270
) -> list[dict[str, Any]]:
73-
response = self._session.get(self._url(path), params=params, timeout=self._timeout)
71+
response = self._session.get(
72+
self._url(path), params=params, timeout=self._timeout
73+
)
7474
return self._process_response_list(response)
7575

76-
def post(
77-
self,
78-
path: str,
79-
json: Optional[dict[str, Any]] = None
80-
) -> dict[str, Any]:
76+
def post(self, path: str, json: Optional[dict[str, Any]] = None) -> dict[str, Any]:
8177
response = self._session.post(self._url(path), json=json, timeout=self._timeout)
8278
return self._process_response_dict(response)
8379

84-
def put(
85-
self,
86-
path: str,
87-
json: Optional[dict[str, Any]] = None
88-
) -> dict[str, Any]:
80+
def put(self, path: str, json: Optional[dict[str, Any]] = None) -> dict[str, Any]:
8981
response = self._session.put(self._url(path), json=json, timeout=self._timeout)
9082
return self._process_response_dict(response)
9183

92-
def patch(
93-
self,
94-
path: str,
95-
json: Optional[dict[str, Any]] = None
96-
) -> dict[str, Any]:
84+
def patch(self, path: str, json: Optional[dict[str, Any]] = None) -> dict[str, Any]:
9785
response = self._session.patch(self._url(path), json=json, timeout=self._timeout)
9886
return self._process_response_dict(response)
9987

@@ -102,11 +90,11 @@ def delete(self, path: str) -> dict[str, Any]:
10290
return self._process_response_dict(response)
10391

10492

105-
def _extract_errors(data: dict[str, Any]) -> list[str]:
93+
def _extract_errors(data: dict[str, Any]) -> list[str]:
10694
def flatten_errors(errors: Any) -> list[str]:
10795
if isinstance(errors, list):
10896
return [str(error) for error in errors]
109-
97+
11098
if isinstance(errors, dict):
11199
flat_errors = []
112100
for key, value in errors.items():
@@ -117,10 +105,10 @@ def flatten_errors(errors: Any) -> list[str]:
117105
return flat_errors
118106

119107
return [str(errors)]
120-
108+
121109
if "errors" in data:
122110
return flatten_errors(data["errors"])
123-
111+
124112
if "error" in data:
125113
return flatten_errors(data["error"])
126114

mailtrap/models/base.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)