Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ dist/
# IntelliJ's project specific settings
.idea/

# VSCode's project specific settings
.vscode/

# mypy
.mypy_cache/

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Versions of this package up to 1.0.1 were a different, unrelated project, that i

### Prerequisites

- Python version 3.6+
- Python version 3.9+

### Install package

Expand Down Expand Up @@ -150,7 +150,7 @@ To setup virtual environments, run tests and linters use:
tox
```

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

```bash
Expand Down
54 changes: 54 additions & 0 deletions examples/testing/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import logging
from typing import Optional

from mailtrap import MailtrapApiClient
from mailtrap.models.projects import Project

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

API_TOKEN = "YOU_API_TOKEN"
ACCOUNT_ID = "YOU_ACCOUNT_ID"


def find_project_by_name(project_name: str, projects: list[Project]) -> Optional[str]:
filtered_projects = [project for project in projects if project.name == project_name]
if filtered_projects:
return filtered_projects[0].id
return None


logging.info("Starting Mailtrap Testing API example...")

client = MailtrapApiClient(token=API_TOKEN)
testing_api = client.get_testing_api(ACCOUNT_ID)
projects_api = testing_api.projects

project_name = "Example-project"
created_project = projects_api.create(project_name=project_name)
logging.info(f"Project created! ID: {created_project.id}, Name: {created_project.name}")

projects = projects_api.get_list()
logging.info(f"Found {len(projects)} projects:")
for project in projects:
logging.info(f" - {project.name} (ID: {project.id})")

project_id = find_project_by_name(project_name, projects)
if project_id:
logging.info(f"Found project with ID: {project_id}")
else:
logging.info("Project not found in the list")

if project_id:
project = projects_api.get_by_id(project_id)
logging.info(f"Project details: {project.name} (ID: {project.id})")

new_name = "Updated-project-name"
updated_project = projects_api.update(project_id, new_name)
logging.info(f"Project updated!ID: {project_id}, New name: {updated_project.name}")

deleted_object = projects_api.delete(project_id)
logging.info(f"Project deleted! Deleted ID: {deleted_object.id}")

logging.info("Example completed successfully!")
1 change: 1 addition & 0 deletions mailtrap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .client import MailtrapApiClient
from .client import MailtrapClient
from .exceptions import APIError
from .exceptions import AuthorizationError
Expand Down
Empty file added mailtrap/api/__init__.py
Empty file.
Empty file.
39 changes: 39 additions & 0 deletions mailtrap/api/resources/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from mailtrap.http import HttpClient
from mailtrap.models.base import DeletedObject
from mailtrap.models.projects import Project


class ProjectsApi:
def __init__(self, client: HttpClient, account_id: str) -> None:
self.account_id = account_id
self.client = client

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

def get_by_id(self, project_id: int) -> Project:
response = self.client.get(
f"/api/accounts/{self.account_id}/projects/{project_id}"
)
return Project(**response)

def create(self, project_name: str) -> Project:
response = self.client.post(
f"/api/accounts/{self.account_id}/projects",
json={"project": {"name": project_name}},
)
return Project(**response)

def update(self, project_id: int, project_name: str) -> Project:
response = self.client.patch(
f"/api/accounts/{self.account_id}/projects/{project_id}",
json={"project": {"name": project_name}},
)
return Project(**response)

def delete(self, project_id: int) -> DeletedObject:
response = self.client.delete(
f"/api/accounts/{self.account_id}/projects/{project_id}",
)
return DeletedObject(**response)
17 changes: 17 additions & 0 deletions mailtrap/api/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Optional

from mailtrap.api.resources.projects import ProjectsApi
from mailtrap.http import HttpClient


class TestingApi:
def __init__(
self, client: HttpClient, account_id: str, inbox_id: Optional[str] = None
) -> None:
self.account_id = account_id
self.inbox_id = inbox_id
self.client = client

@property
def projects(self) -> ProjectsApi:
return ProjectsApi(account_id=self.account_id, client=self.client)
21 changes: 21 additions & 0 deletions mailtrap/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

import requests

from mailtrap.api.testing import TestingApi
from mailtrap.config import GENERAL_ENDPOINT
from mailtrap.exceptions import APIError
from mailtrap.exceptions import AuthorizationError
from mailtrap.exceptions import ClientConfigurationError
from mailtrap.http import HttpClient
from mailtrap.mail.base import BaseMail


Expand Down Expand Up @@ -98,3 +101,21 @@ def _validate_itself(self) -> None:

if self.bulk and self.sandbox:
raise ClientConfigurationError("bulk mode is not allowed in sandbox mode")


class MailtrapApiClient:
def __init__(self, token: str) -> None:
self.token = token

def testing_api(self, account_id: str, inbox_id: str) -> TestingApi:
http_client = HttpClient(host=GENERAL_ENDPOINT, headers=self.get_headers())
return TestingApi(account_id=account_id, inbox_id=inbox_id, client=http_client)

def get_headers(self) -> dict[str, str]:
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json",
"User-Agent": (
"mailtrap-python (https://github.com/railsware/mailtrap-python)"
),
}
3 changes: 3 additions & 0 deletions mailtrap/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GENERAL_ENDPOINT = "mailtrap.io"

DEFAULT_REQUEST_TIMEOUT = 30 # in seconds
98 changes: 98 additions & 0 deletions mailtrap/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from typing import Any
from typing import NoReturn
from typing import Optional

from requests import Response
from requests import Session

from mailtrap.config import DEFAULT_REQUEST_TIMEOUT
from mailtrap.exceptions import APIError
from mailtrap.exceptions import AuthorizationError


class HttpClient:
def __init__(
self,
host: str,
headers: Optional[dict[str, str]] = None,
timeout: int = DEFAULT_REQUEST_TIMEOUT,
):
self._host = host
self._session = Session()
self._session.headers.update(headers or {})
self._timeout = timeout

def _url(self, path: str) -> str:
return f"https://{self._host}/{path.lstrip('/')}"

def _handle_failed_response(self, response: Response) -> NoReturn:
status_code = response.status_code
try:
data = response.json()
except ValueError as exc:
raise APIError(status_code, errors=["Unknown Error"]) from exc

errors = _extract_errors(data)

if status_code == 401:
raise AuthorizationError(errors=errors)

raise APIError(status_code, errors=errors)

def _process_response(self, response: Response) -> Any:
if not response.ok:
self._handle_failed_response(response)
return response.json()

def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
response = self._session.get(
self._url(path), params=params, timeout=self._timeout
)
return self._process_response(response)

def list(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
response = self._session.get(
self._url(path), params=params, timeout=self._timeout
)
return self._process_response(response)

def post(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
response = self._session.post(self._url(path), json=json, timeout=self._timeout)
return self._process_response(response)

def put(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
response = self._session.put(self._url(path), json=json, timeout=self._timeout)
return self._process_response(response)

def patch(self, path: str, json: Optional[dict[str, Any]] = None) -> Any:
response = self._session.patch(self._url(path), json=json, timeout=self._timeout)
return self._process_response(response)

def delete(self, path: str) -> Any:
response = self._session.delete(self._url(path), timeout=self._timeout)
return self._process_response(response)


def _extract_errors(data: dict[str, Any]) -> list[str]:
def flatten_errors(errors: Any) -> list[str]:
if isinstance(errors, list):
return [str(error) for error in errors]

if isinstance(errors, dict):
flat_errors = []
for key, value in errors.items():
if isinstance(value, list):
flat_errors.extend([f"{key}: {v}" for v in value])
else:
flat_errors.append(f"{key}: {value}")
return flat_errors

return [str(errors)]

if "errors" in data:
return flatten_errors(data["errors"])

if "error" in data:
return flatten_errors(data["error"])

return ["Unknown error"]
Empty file added mailtrap/models/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions mailtrap/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class DeletedObject(BaseModel):
id: int
33 changes: 33 additions & 0 deletions mailtrap/models/inboxes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Optional

from pydantic import BaseModel

from mailtrap.models.permissions import Permissions


class Inbox(BaseModel):
id: int
name: str
username: str
max_size: int
status: str
email_username: str
email_username_enabled: bool
sent_messages_count: int
forwarded_messages_count: int
used: bool
forward_from_email_address: str
project_id: int
domain: str
pop3_domain: str
email_domain: str
emails_count: int
emails_unread_count: int
smtp_ports: list[int]
pop3_ports: list[int]
max_message_size: int
permissions: Permissions
password: Optional[str] = (
None # Password is only available if you have admin permissions for the inbox.
)
last_message_sent_at: Optional[str] = None
8 changes: 8 additions & 0 deletions mailtrap/models/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pydantic import BaseModel


class Permissions(BaseModel):
can_read: bool
can_update: bool
can_destroy: bool
can_leave: bool
18 changes: 18 additions & 0 deletions mailtrap/models/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import BaseModel
from pydantic import Field

from mailtrap.models.inboxes import Inbox
from mailtrap.models.permissions import Permissions


class ShareLinks(BaseModel):
admin: str
viewer: str


class Project(BaseModel):
id: int
name: str
share_links: ShareLinks
inboxes: list[Inbox]
permissions: Permissions
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"requests>=2.26.0",
"pydantic>=2.11.7",
]

[project.urls]
Expand Down
2 changes: 2 additions & 0 deletions requirements.test.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
-r requirements.txt

pytest>=7.0.1
responses>=0.17.0
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
requests>=2.26.0
pydantic>=2.11.7
Empty file added tests/unit/api/__init__.py
Empty file.
Loading