From 86bd4ad1ae9b816677e9ddbcac0fe67d354b6ca8 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 3 Nov 2026 09:11:53 +0100 Subject: [PATCH 1/4] Add Sandbox and Domain names, deprecate the old ones Decisions: - the product and API docs now say sandbox and domain, so the SDK exposes those names - old names stay as aliases that raise DeprecationWarning, so a minor release breaks no one - API paths and wire fields stay the same, so requests and responses are unchanged - renamed method keywords go through renamed_kwargs, so type checkers see only the new names - MailtrapClient and SendingApi keep inbox_id in the signature, so the most common call still type-checks - old models are plain aliases, so imports stay typed and don't warn Co-Authored-By: Claude Opus 5.5 --- README.md | 48 +++- examples/domains/domains.py | 65 +++++ examples/{testing => sandbox}/attachments.py | 16 +- examples/sandbox/messages.py | 121 ++++++++++ examples/{testing => sandbox}/projects.py | 2 +- examples/sandbox/sandboxes.py | 93 +++++++ examples/sending/advanced_sending.py | 2 +- examples/sending/batch_advanced_sending.py | 2 +- examples/sending/batch_minimal_sending.py | 2 +- .../sending/batch_sending_with_template.py | 2 +- examples/sending/minimal_sending.py | 2 +- examples/sending/sending_with_template.py | 2 +- examples/sending_domains/sending_domains.py | 65 ----- examples/testing/inboxes.py | 92 ------- examples/testing/messages.py | 121 ---------- mailtrap/__init__.py | 6 +- mailtrap/_deprecation.py | 77 ++++++ mailtrap/api/domains.py | 12 + mailtrap/api/resources/attachments.py | 17 +- mailtrap/api/resources/billing.py | 2 +- mailtrap/api/resources/company_info.py | 26 +- mailtrap/api/resources/domains.py | 86 +++++++ mailtrap/api/resources/email_campaigns.py | 2 +- mailtrap/api/resources/inboxes.py | 80 +----- mailtrap/api/resources/messages.py | 78 +++--- mailtrap/api/resources/permissions.py | 2 +- mailtrap/api/resources/projects.py | 6 +- mailtrap/api/resources/sandboxes.py | 88 +++++++ mailtrap/api/resources/sending_domains.py | 89 +------ mailtrap/api/resources/tracking_opt_outs.py | 2 +- mailtrap/api/sandbox.py | 27 +++ mailtrap/api/sending.py | 20 +- mailtrap/api/sending_domains.py | 23 +- mailtrap/api/testing.py | 39 ++- mailtrap/client.py | 76 +++++- mailtrap/models/domains.py | 69 ++++++ mailtrap/models/inboxes.py | 73 ++---- mailtrap/models/projects.py | 4 +- mailtrap/models/sandboxes.py | 57 +++++ mailtrap/models/sending_domains.py | 101 +++----- .../{sending_domains => domains}/__init__.py | 0 .../test_domains.py} | 126 +++++----- .../unit/api/{testing => sandbox}/__init__.py | 0 .../{testing => sandbox}/test_attachments.py | 0 .../api/{testing => sandbox}/test_messages.py | 0 .../api/{testing => sandbox}/test_projects.py | 0 .../test_sandboxes.py} | 226 ++++++++--------- tests/unit/api/test_sending.py | 28 ++- ...est_sending_domains.py => test_domains.py} | 20 +- .../{test_inboxes.py => test_sandboxes.py} | 18 +- tests/unit/test_client.py | 34 ++- tests/unit/test_deprecations.py | 228 ++++++++++++++++++ 52 files changed, 1487 insertions(+), 890 deletions(-) create mode 100644 examples/domains/domains.py rename examples/{testing => sandbox}/attachments.py (50%) create mode 100644 examples/sandbox/messages.py rename examples/{testing => sandbox}/projects.py (96%) create mode 100644 examples/sandbox/sandboxes.py delete mode 100644 examples/sending_domains/sending_domains.py delete mode 100644 examples/testing/inboxes.py delete mode 100644 examples/testing/messages.py create mode 100644 mailtrap/_deprecation.py create mode 100644 mailtrap/api/domains.py create mode 100644 mailtrap/api/resources/domains.py create mode 100644 mailtrap/api/resources/sandboxes.py create mode 100644 mailtrap/api/sandbox.py create mode 100644 mailtrap/models/domains.py create mode 100644 mailtrap/models/sandboxes.py rename tests/unit/api/{sending_domains => domains}/__init__.py (100%) rename tests/unit/api/{sending_domains/test_sending_domains.py => domains/test_domains.py} (74%) rename tests/unit/api/{testing => sandbox}/__init__.py (100%) rename tests/unit/api/{testing => sandbox}/test_attachments.py (100%) rename tests/unit/api/{testing => sandbox}/test_messages.py (100%) rename tests/unit/api/{testing => sandbox}/test_projects.py (100%) rename tests/unit/api/{testing/test_inboxes.py => sandbox/test_sandboxes.py} (70%) rename tests/unit/models/{test_sending_domains.py => test_domains.py} (73%) rename tests/unit/models/{test_inboxes.py => test_sandboxes.py} (59%) create mode 100644 tests/unit/test_deprecations.py diff --git a/README.md b/README.md index 7621316..f315ec2 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,14 @@ client.send(mail) ### Sandbox vs Production (easy switching) Mailtrap lets you test safely in the Email Sandbox and then switch to Production (Sending). -Remove the inbox_id field or set it to None. Then, remove the sandbox field or set it to False. +Remove the sandbox_id field or set it to None. Then, remove the sandbox field or set it to False. You can change the arguments in the code or via another way. Here is an example using environment variables. Set next environment variables: ```bash MAILTRAP_API_KEY=your_api_token # https://mailtrap.io/api-tokens MAILTRAP_USE_SANDBOX=true # true/false toggle -MAILTRAP_INBOX_ID=123456 # Only needed for sandbox +MAILTRAP_SANDBOX_ID=123456 # Only needed for sandbox ``` Bootstrap logic: @@ -67,12 +67,12 @@ import mailtrap as mt API_KEY = os.environ["MAILTRAP_API_KEY"] IS_SANDBOX = os.environ.get("MAILTRAP_USE_SANDBOX", "true").lower() == "true" -INBOX_ID = os.environ.get("MAILTRAP_INBOX_ID") +SANDBOX_ID = os.environ.get("MAILTRAP_SANDBOX_ID") client = mt.MailtrapClient( token=API_KEY, sandbox=IS_SANDBOX, - inbox_id=INBOX_ID, # None is ignored for production + sandbox_id=SANDBOX_ID, # None is ignored for production ) # Create mail object @@ -224,11 +224,11 @@ The same situation applies to both `client.batch_send()` and `client.sending_api - Advanced sending – [`sending/advanced_sending.py`](examples/sending/advanced_sending.py) - Advanced batch sending – [`sending/batch_advanced_sending.py`](examples/sending/batch_advanced_sending.py) -### Email Sandbox (Testing) API: -- Attachments management – [`testing/attachments.py`](examples/testing/attachments.py) -- Inboxes management – [`testing/inboxes.py`](examples/testing/inboxes.py) -- Messages management – [`testing/messages.py`](examples/testing/messages.py) -- Projects management – [`testing/projects.py`](examples/testing/projects.py) +### Email Sandbox API: +- Attachments management – [`sandbox/attachments.py`](examples/sandbox/attachments.py) +- Sandboxes management – [`sandbox/sandboxes.py`](examples/sandbox/sandboxes.py) +- Messages management – [`sandbox/messages.py`](examples/sandbox/messages.py) +- Projects management – [`sandbox/projects.py`](examples/sandbox/projects.py) ### Contacts API: - Contacts management – [`contacts/contacts.py`](examples/contacts/contacts.py) @@ -242,9 +242,9 @@ The same situation applies to both `client.batch_send()` and `client.sending_api - Templates (experimental, `/api/templates`) – [`paginated_templates/templates.py`](examples/paginated_templates/templates.py) - Templates management – [`email_templates/templates.py`](examples/email_templates/templates.py) -### Sending Domains API: -- Sending Domains – [`sending_domains/sending_domains.py`](examples/sending_domains/sending_domains.py) -- Sending Domain Company Info – [`company_info/company_info.py`](examples/company_info/company_info.py) +### Domains API: +- Domains – [`domains/domains.py`](examples/domains/domains.py) +- Domain Company Info – [`company_info/company_info.py`](examples/company_info/company_info.py) ### Inbound Email API: - Folders management – [`inbound/folders.py`](examples/inbound/folders.py) @@ -281,6 +281,30 @@ The same situation applies to both `client.batch_send()` and `client.sending_api ### Organizations API: - Sub-Accounts management – [`organizations/sub_accounts.py`](examples/organizations/sub_accounts.py) +## Renamed APIs + +Email Sandbox inboxes are now called sandboxes, and sending domains are now called domains. +The old names still work, but they raise a `DeprecationWarning` and will be removed in a future major release. +API requests are the same with either name. + +| Old name | New name | +|---|---| +| `MailtrapClient(inbox_id=...)` | `MailtrapClient(sandbox_id=...)` | +| `client.inbox_id` | `client.sandbox_id` | +| `client.testing_api` (`TestingApi`) | `client.sandbox_api` (`SandboxApi`) | +| `client.testing_api.inboxes` (`InboxesApi`) | `client.sandbox_api.sandboxes` (`SandboxesApi`) | +| `inbox_id=`, `inbox_params=` keyword arguments of the sandbox, message and attachment methods | `sandbox_id=`, `sandbox_params=` | +| `Inbox`, `CreateInboxParams`, `UpdateInboxParams` | `Sandbox`, `CreateSandboxParams`, `UpdateSandboxParams` | +| `SendingApi(inbox_id=...)` | `SendingApi(sandbox_id=...)` | +| `client.sending_domains_api` (`SendingDomainsBaseApi`) | `client.domains_api` (`DomainsBaseApi`) | +| `client.sending_domains_api.sending_domains` (`SendingDomainsApi`) | `client.domains_api.domains` (`DomainsApi`) | +| `sending_domain_id=` keyword argument of the domain and company info methods | `domain_id=` | +| `SendingDomain`, `SendingDomainPermissions`, `CreateSendingDomainParams`, `UpdateSendingDomainParams` | `Domain`, `DomainPermissions`, `CreateDomainParams`, `UpdateDomainParams` | + +Fields that mirror the API response or query parameters keep their names, for example `EmailMessage.inbox_id`, `Project.inboxes` and `sending_domain_ids`. Email Inbound inboxes are not renamed. + +Python hides `DeprecationWarning` outside `__main__` by default. To see the warnings, run with `python -W default::DeprecationWarning`. + ## Contributing 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). diff --git a/examples/domains/domains.py b/examples/domains/domains.py new file mode 100644 index 0000000..76b9bca --- /dev/null +++ b/examples/domains/domains.py @@ -0,0 +1,65 @@ +import os + +import mailtrap as mt +from mailtrap.models.common import DeletedObject +from mailtrap.models.domains import Domain +from mailtrap.models.domains import SendSetupInstructionsResponse + +API_KEY = os.environ["MAILTRAP_API_KEY"] +ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] + +client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) +domains_api = client.domains_api.domains + + +def list_domains() -> list[Domain]: + return domains_api.get_list() + + +def get_domain(domain_id: int) -> Domain: + return domains_api.get_by_id(domain_id) + + +def create_domain(domain_name: str) -> Domain: + params = mt.CreateDomainParams(domain_name=domain_name) + return domains_api.create(params) + + +def update_domain(domain_id: int) -> Domain: + params = mt.UpdateDomainParams( + open_tracking_enabled=True, + click_tracking_enabled=True, + tracking_opt_out_enabled=True, + auto_unsubscribe_link_enabled=False, + inbound_enabled=False, + ) + return domains_api.update(domain_id, params) + + +def delete_domain(domain_id: int) -> DeletedObject: + return domains_api.delete(domain_id) + + +def send_setup_instructions(domain_id: int, email: str) -> SendSetupInstructionsResponse: + params = mt.SendSetupInstructionsParams(email=email) + return domains_api.send_setup_instructions(domain_id, params) + + +if __name__ == "__main__": + new_domain = create_domain("example.com") + print(new_domain) + + domains = list_domains() + print(domains) + + domain = get_domain(new_domain.id) + print(domain) + + updated_domain = update_domain(new_domain.id) + print(updated_domain) + + response = send_setup_instructions(new_domain.id, "example@mail.com") + print(response) + + deleted_domain = delete_domain(new_domain.id) + print(deleted_domain) diff --git a/examples/testing/attachments.py b/examples/sandbox/attachments.py similarity index 50% rename from examples/testing/attachments.py rename to examples/sandbox/attachments.py index fe0177e..3c8de32 100644 --- a/examples/testing/attachments.py +++ b/examples/sandbox/attachments.py @@ -5,30 +5,30 @@ API_KEY = os.environ["MAILTRAP_API_KEY"] ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] -INBOX_ID = os.environ["MAILTRAP_INBOX_ID"] +SANDBOX_ID = os.environ["MAILTRAP_SANDBOX_ID"] MESSAGE_ID = os.environ["MAILTRAP_SANDBOX_MESSAGE_ID"] client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) -attachments_api = client.testing_api.attachments +attachments_api = client.sandbox_api.attachments -def list_attachments(inbox_id: int, message_id: int) -> list[Attachment]: - return attachments_api.get_list(inbox_id=inbox_id, message_id=message_id) +def list_attachments(sandbox_id: int, message_id: int) -> list[Attachment]: + return attachments_api.get_list(sandbox_id=sandbox_id, message_id=message_id) -def get_attachment(inbox_id: int, message_id: int, attachment_id: int) -> Attachment: +def get_attachment(sandbox_id: int, message_id: int, attachment_id: int) -> Attachment: return attachments_api.get( - inbox_id=inbox_id, + sandbox_id=sandbox_id, message_id=message_id, attachment_id=attachment_id, ) if __name__ == "__main__": - attachments = list_attachments(inbox_id=INBOX_ID, message_id=MESSAGE_ID) + attachments = list_attachments(sandbox_id=SANDBOX_ID, message_id=MESSAGE_ID) print(attachments) if attachments: attachment = get_attachment( - inbox_id=INBOX_ID, message_id=MESSAGE_ID, attachment_id=attachments[0].id + sandbox_id=SANDBOX_ID, message_id=MESSAGE_ID, attachment_id=attachments[0].id ) print(attachment) diff --git a/examples/sandbox/messages.py b/examples/sandbox/messages.py new file mode 100644 index 0000000..0092ef3 --- /dev/null +++ b/examples/sandbox/messages.py @@ -0,0 +1,121 @@ +import os +from typing import Optional + +import mailtrap as mt +from mailtrap.models.messages import AnalysisReport +from mailtrap.models.messages import EmailMessage +from mailtrap.models.messages import ForwardedMessage +from mailtrap.models.messages import SpamReport + +API_KEY = os.environ["MAILTRAP_API_KEY"] +ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] +SANDBOX_ID = os.environ["MAILTRAP_SANDBOX_ID"] + +client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) +messages_api = client.sandbox_api.messages + + +def get_message(sandbox_id: int, message_id: int) -> EmailMessage: + return messages_api.show_message(sandbox_id=sandbox_id, message_id=message_id) + + +def update_message(sandbox_id: int, message_id: int, is_read: bool) -> EmailMessage: + return messages_api.update( + sandbox_id=sandbox_id, + message_id=message_id, + message_params=mt.UpdateEmailMessageParams(is_read=is_read), + ) + + +def delete_message(sandbox_id: int, message_id: int) -> EmailMessage: + return messages_api.delete(sandbox_id=sandbox_id, message_id=message_id) + + +def list_messages( + sandbox_id: int, + search: Optional[str] = None, + last_id: Optional[int] = None, + page: Optional[int] = None, +) -> list[EmailMessage]: + return messages_api.get_list( + sandbox_id=sandbox_id, search=search, last_id=last_id, page=page + ) + + +def forward_message(sandbox_id: int, message_id: int, email: str) -> ForwardedMessage: + return messages_api.forward(sandbox_id=sandbox_id, message_id=message_id, email=email) + + +def get_spam_report(sandbox_id: int, message_id: str) -> SpamReport: + return messages_api.get_spam_report(sandbox_id=sandbox_id, message_id=message_id) + + +def get_html_analysis(sandbox_id: int, message_id: str) -> AnalysisReport: + return messages_api.get_html_analysis(sandbox_id=sandbox_id, message_id=message_id) + + +def get_text_message(sandbox_id: int, message_id: str) -> str: + return messages_api.get_text_message(sandbox_id=sandbox_id, message_id=message_id) + + +def get_raw_message(sandbox_id: int, message_id: str) -> str: + return messages_api.get_raw_message(sandbox_id=sandbox_id, message_id=message_id) + + +def get_html_source(sandbox_id: int, message_id: str) -> str: + return messages_api.get_html_source(sandbox_id=sandbox_id, message_id=message_id) + + +def get_html_message(sandbox_id: int, message_id: str) -> str: + return messages_api.get_html_message(sandbox_id=sandbox_id, message_id=message_id) + + +def get_message_as_eml(sandbox_id: int, message_id: str) -> str: + return messages_api.get_message_as_eml(sandbox_id=sandbox_id, message_id=message_id) + + +def get_mail_headers(sandbox_id: int, message_id: str) -> str: + return messages_api.get_mail_headers(sandbox_id=sandbox_id, message_id=message_id) + + +if __name__ == "__main__": + messages = list_messages(sandbox_id=SANDBOX_ID) + print(messages) + if messages: + msg_id = messages[0].id + message = get_message(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(message) + + updated = update_message(sandbox_id=SANDBOX_ID, message_id=msg_id, is_read=True) + print(updated) + + forwarded = forward_message( + sandbox_id=SANDBOX_ID, + message_id=msg_id, + email="example@example.com", + ) + print(forwarded) + + spam = get_spam_report(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(spam) + + html_analysis = get_html_analysis(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(html_analysis) + + text = get_text_message(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(text) + + raw = get_raw_message(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(raw) + + html_src = get_html_source(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(html_src) + + html_msg = get_html_message(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(html_msg) + + eml = get_message_as_eml(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(eml) + + headers = get_mail_headers(sandbox_id=SANDBOX_ID, message_id=msg_id) + print(headers) diff --git a/examples/testing/projects.py b/examples/sandbox/projects.py similarity index 96% rename from examples/testing/projects.py rename to examples/sandbox/projects.py index 5b7fcfc..21f43e9 100644 --- a/examples/testing/projects.py +++ b/examples/sandbox/projects.py @@ -7,7 +7,7 @@ ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) -projects_api = client.testing_api.projects +projects_api = client.sandbox_api.projects def list_projects() -> list[Project]: diff --git a/examples/sandbox/sandboxes.py b/examples/sandbox/sandboxes.py new file mode 100644 index 0000000..2ec7a25 --- /dev/null +++ b/examples/sandbox/sandboxes.py @@ -0,0 +1,93 @@ +import os +from typing import Optional + +import mailtrap as mt +from mailtrap.models.sandboxes import Sandbox + +API_KEY = os.environ["MAILTRAP_API_KEY"] +ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] + +client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) +sandboxes_api = client.sandbox_api.sandboxes + + +def list_sandboxes() -> list[Sandbox]: + return sandboxes_api.get_list() + + +def create_sandbox(project_id: int, sandbox_name: str) -> Sandbox: + return sandboxes_api.create( + project_id=project_id, sandbox_params=mt.CreateSandboxParams(name=sandbox_name) + ) + + +def get_sandbox_by_id(sandbox_id: int) -> Sandbox: + return sandboxes_api.get_by_id(sandbox_id) + + +def update_sandbox( + sandbox_id: int, + new_name: Optional[str] = None, + new_email_username: Optional[str] = None, +) -> Sandbox: + return sandboxes_api.update( + sandbox_id, + mt.UpdateSandboxParams(name=new_name, email_username=new_email_username), + ) + + +def clean_sandbox(sandbox_id: int) -> Sandbox: + return sandboxes_api.clean(sandbox_id) + + +def mark_sandbox_as_read(sandbox_id: int) -> Sandbox: + return sandboxes_api.mark_as_read(sandbox_id) + + +def reset_sandbox_credentials(sandbox_id: int) -> Sandbox: + return sandboxes_api.reset_credentials(sandbox_id) + + +def enable_sandbox_email_address(sandbox_id: int) -> Sandbox: + return sandboxes_api.enable_email_address(sandbox_id) + + +def reset_sandbox_email_username(sandbox_id: int) -> Sandbox: + return sandboxes_api.reset_email_username(sandbox_id) + + +def delete_sandbox(sandbox_id: int): + return sandboxes_api.delete(sandbox_id) + + +if __name__ == "__main__": + sandboxes = list_sandboxes() + print(sandboxes) + + created = create_sandbox(project_id=1, sandbox_name="example-created-sandbox") + print(created) + + sandbox_id = created.id + sandbox = get_sandbox_by_id(sandbox_id=sandbox_id) + print(sandbox) + + updated = update_sandbox(sandbox_id=sandbox_id, new_name=f"{sandbox.name}-updated") + print(updated) + + cleaned = clean_sandbox(sandbox_id=sandbox_id) + print(cleaned) + + marked = mark_sandbox_as_read(sandbox_id=sandbox_id) + print(marked) + + reset_creds = reset_sandbox_credentials(sandbox_id=sandbox_id) + print(reset_creds) + + enabled = enable_sandbox_email_address(sandbox_id=sandbox_id) + print(enabled) + + reset_username = reset_sandbox_email_username(sandbox_id=sandbox_id) + print(reset_username) + + deleted = delete_sandbox(sandbox_id=sandbox_id) + print(deleted) diff --git a/examples/sending/advanced_sending.py b/examples/sending/advanced_sending.py index ad29859..4be25a7 100644 --- a/examples/sending/advanced_sending.py +++ b/examples/sending/advanced_sending.py @@ -22,7 +22,7 @@ def get_client(type_: SendingType) -> mt.MailtrapClient: return mt.MailtrapClient( token=API_KEY, sandbox=True, - inbox_id=os.environ["MAILTRAP_INBOX_ID"], + sandbox_id=os.environ["MAILTRAP_SANDBOX_ID"], ) raise ValueError(f"Invalid sending type: {type_}") diff --git a/examples/sending/batch_advanced_sending.py b/examples/sending/batch_advanced_sending.py index ccfcc14..7a72655 100644 --- a/examples/sending/batch_advanced_sending.py +++ b/examples/sending/batch_advanced_sending.py @@ -22,7 +22,7 @@ def get_client(type_: SendingType) -> mt.MailtrapClient: return mt.MailtrapClient( token=API_KEY, sandbox=True, - inbox_id=os.environ["MAILTRAP_INBOX_ID"], + sandbox_id=os.environ["MAILTRAP_SANDBOX_ID"], ) raise ValueError(f"Invalid sending type: {type_}") diff --git a/examples/sending/batch_minimal_sending.py b/examples/sending/batch_minimal_sending.py index 5902ddb..3aa7d01 100644 --- a/examples/sending/batch_minimal_sending.py +++ b/examples/sending/batch_minimal_sending.py @@ -19,7 +19,7 @@ def get_client(type_: SendingType) -> mt.MailtrapClient: return mt.MailtrapClient(token=API_KEY, bulk=True) elif type_ == SendingType.SANDBOX: return mt.MailtrapClient( - token=API_KEY, sandbox=True, inbox_id=os.environ["MAILTRAP_INBOX_ID"] + token=API_KEY, sandbox=True, sandbox_id=os.environ["MAILTRAP_SANDBOX_ID"] ) raise ValueError(f"Invalid sending type: {type_}") diff --git a/examples/sending/batch_sending_with_template.py b/examples/sending/batch_sending_with_template.py index 0d9be9c..48fcc76 100644 --- a/examples/sending/batch_sending_with_template.py +++ b/examples/sending/batch_sending_with_template.py @@ -18,7 +18,7 @@ def get_client(type_: SendingType) -> mt.MailtrapClient: return mt.MailtrapClient(token=API_KEY, bulk=True) elif type_ == SendingType.SANDBOX: return mt.MailtrapClient( - token=API_KEY, sandbox=True, inbox_id=os.environ["MAILTRAP_INBOX_ID"] + token=API_KEY, sandbox=True, sandbox_id=os.environ["MAILTRAP_SANDBOX_ID"] ) raise ValueError(f"Invalid sending type: {type_}") diff --git a/examples/sending/minimal_sending.py b/examples/sending/minimal_sending.py index b0e0755..0d861bc 100644 --- a/examples/sending/minimal_sending.py +++ b/examples/sending/minimal_sending.py @@ -19,7 +19,7 @@ def get_client(type_: SendingType) -> mt.MailtrapClient: return mt.MailtrapClient(token=API_KEY, bulk=True) elif type_ == SendingType.SANDBOX: return mt.MailtrapClient( - token=API_KEY, sandbox=True, inbox_id=os.environ["MAILTRAP_INBOX_ID"] + token=API_KEY, sandbox=True, sandbox_id=os.environ["MAILTRAP_SANDBOX_ID"] ) raise ValueError(f"Invalid sending type: {type_}") diff --git a/examples/sending/sending_with_template.py b/examples/sending/sending_with_template.py index 026dfa3..b58569b 100644 --- a/examples/sending/sending_with_template.py +++ b/examples/sending/sending_with_template.py @@ -18,7 +18,7 @@ def get_client(type_: SendingType) -> mt.MailtrapClient: return mt.MailtrapClient(token=API_KEY, bulk=True) elif type_ == SendingType.SANDBOX: return mt.MailtrapClient( - token=API_KEY, sandbox=True, inbox_id=os.environ["MAILTRAP_INBOX_ID"] + token=API_KEY, sandbox=True, sandbox_id=os.environ["MAILTRAP_SANDBOX_ID"] ) raise ValueError(f"Invalid sending type: {type_}") diff --git a/examples/sending_domains/sending_domains.py b/examples/sending_domains/sending_domains.py deleted file mode 100644 index 32513bb..0000000 --- a/examples/sending_domains/sending_domains.py +++ /dev/null @@ -1,65 +0,0 @@ -import os - -import mailtrap as mt -from mailtrap.models.common import DeletedObject -from mailtrap.models.sending_domains import SendingDomain -from mailtrap.models.sending_domains import SendSetupInstructionsResponse - -API_KEY = os.environ["MAILTRAP_API_KEY"] -ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] - -client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) -sending_domains_api = client.sending_domains_api.sending_domains - - -def list_sending_domains() -> list[SendingDomain]: - return sending_domains_api.get_list() - - -def get_sending_domain(domain_id: int) -> SendingDomain: - return sending_domains_api.get_by_id(domain_id) - - -def create_sending_domain(domain_name: str) -> SendingDomain: - params = mt.CreateSendingDomainParams(domain_name=domain_name) - return sending_domains_api.create(params) - - -def update_sending_domain(domain_id: int) -> SendingDomain: - params = mt.UpdateSendingDomainParams( - open_tracking_enabled=True, - click_tracking_enabled=True, - tracking_opt_out_enabled=True, - auto_unsubscribe_link_enabled=False, - inbound_enabled=False, - ) - return sending_domains_api.update(domain_id, params) - - -def delete_sending_domain(domain_id: int) -> DeletedObject: - return sending_domains_api.delete(domain_id) - - -def send_setup_instructions(domain_id: int, email: str) -> SendSetupInstructionsResponse: - params = mt.SendSetupInstructionsParams(email=email) - return sending_domains_api.send_setup_instructions(domain_id, params) - - -if __name__ == "__main__": - new_domain = create_sending_domain("example.com") - print(new_domain) - - domains = list_sending_domains() - print(domains) - - domain = get_sending_domain(new_domain.id) - print(domain) - - updated_domain = update_sending_domain(new_domain.id) - print(updated_domain) - - response = send_setup_instructions(new_domain.id, "example@mail.com") - print(response) - - deleted_domain = delete_sending_domain(new_domain.id) - print(deleted_domain) diff --git a/examples/testing/inboxes.py b/examples/testing/inboxes.py deleted file mode 100644 index 9733bc6..0000000 --- a/examples/testing/inboxes.py +++ /dev/null @@ -1,92 +0,0 @@ -import os -from typing import Optional - -import mailtrap as mt -from mailtrap.models.inboxes import Inbox - -API_KEY = os.environ["MAILTRAP_API_KEY"] -ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] - -client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) -inboxes_api = client.testing_api.inboxes - - -def list_inboxes() -> list[Inbox]: - return inboxes_api.get_list() - - -def create_inbox(project_id: int, inbox_name: str) -> Inbox: - return inboxes_api.create( - project_id=project_id, inbox_params=mt.CreateInboxParams(name=inbox_name) - ) - - -def get_inbox_by_id(inbox_id: int) -> Inbox: - return inboxes_api.get_by_id(inbox_id) - - -def update_inbox( - inbox_id: int, - new_name: Optional[str] = None, - new_email_username: Optional[str] = None, -) -> Inbox: - return inboxes_api.update( - inbox_id, mt.UpdateInboxParams(name=new_name, email_username=new_email_username) - ) - - -def clean_inbox(inbox_id: int) -> Inbox: - return inboxes_api.clean(inbox_id) - - -def mark_inbox_as_read(inbox_id: int) -> Inbox: - return inboxes_api.mark_as_read(inbox_id) - - -def reset_inbox_credentials(inbox_id: int) -> Inbox: - return inboxes_api.reset_credentials(inbox_id) - - -def enable_inbox_email_address(inbox_id: int) -> Inbox: - return inboxes_api.enable_email_address(inbox_id) - - -def reset_inbox_email_username(inbox_id: int) -> Inbox: - return inboxes_api.reset_email_username(inbox_id) - - -def delete_inbox(inbox_id: int): - return inboxes_api.delete(inbox_id) - - -if __name__ == "__main__": - inboxes = list_inboxes() - print(inboxes) - - created = create_inbox(project_id=1, inbox_name="example-created-inbox") - print(created) - - inbox_id = created.id - inbox = get_inbox_by_id(inbox_id=inbox_id) - print(inbox) - - updated = update_inbox(inbox_id=inbox_id, new_name=f"{inbox.name}-updated") - print(updated) - - cleaned = clean_inbox(inbox_id=inbox_id) - print(cleaned) - - marked = mark_inbox_as_read(inbox_id=inbox_id) - print(marked) - - reset_creds = reset_inbox_credentials(inbox_id=inbox_id) - print(reset_creds) - - enabled = enable_inbox_email_address(inbox_id=inbox_id) - print(enabled) - - reset_username = reset_inbox_email_username(inbox_id=inbox_id) - print(reset_username) - - deleted = delete_inbox(inbox_id=inbox_id) - print(deleted) diff --git a/examples/testing/messages.py b/examples/testing/messages.py deleted file mode 100644 index 27ef77c..0000000 --- a/examples/testing/messages.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from typing import Optional - -import mailtrap as mt -from mailtrap.models.messages import AnalysisReport -from mailtrap.models.messages import EmailMessage -from mailtrap.models.messages import ForwardedMessage -from mailtrap.models.messages import SpamReport - -API_KEY = os.environ["MAILTRAP_API_KEY"] -ACCOUNT_ID = os.environ["MAILTRAP_ACCOUNT_ID"] -INBOX_ID = os.environ["MAILTRAP_INBOX_ID"] - -client = mt.MailtrapClient(token=API_KEY, account_id=ACCOUNT_ID) -messages_api = client.testing_api.messages - - -def get_message(inbox_id: int, message_id: int) -> EmailMessage: - return messages_api.show_message(inbox_id=inbox_id, message_id=message_id) - - -def update_message(inbox_id: int, message_id: int, is_read: bool) -> EmailMessage: - return messages_api.update( - inbox_id=inbox_id, - message_id=message_id, - message_params=mt.UpdateEmailMessageParams(is_read=is_read), - ) - - -def delete_message(inbox_id: int, message_id: int) -> EmailMessage: - return messages_api.delete(inbox_id=inbox_id, message_id=message_id) - - -def list_messages( - inbox_id: int, - search: Optional[str] = None, - last_id: Optional[int] = None, - page: Optional[int] = None, -) -> list[EmailMessage]: - return messages_api.get_list( - inbox_id=inbox_id, search=search, last_id=last_id, page=page - ) - - -def forward_message(inbox_id: int, message_id: int, email: str) -> ForwardedMessage: - return messages_api.forward(inbox_id=inbox_id, message_id=message_id, email=email) - - -def get_spam_report(inbox_id: int, message_id: str) -> SpamReport: - return messages_api.get_spam_report(inbox_id=inbox_id, message_id=message_id) - - -def get_html_analysis(inbox_id: int, message_id: str) -> AnalysisReport: - return messages_api.get_html_analysis(inbox_id=inbox_id, message_id=message_id) - - -def get_text_message(inbox_id: int, message_id: str) -> str: - return messages_api.get_text_message(inbox_id=inbox_id, message_id=message_id) - - -def get_raw_message(inbox_id: int, message_id: str) -> str: - return messages_api.get_raw_message(inbox_id=inbox_id, message_id=message_id) - - -def get_html_source(inbox_id: int, message_id: str) -> str: - return messages_api.get_html_source(inbox_id=inbox_id, message_id=message_id) - - -def get_html_message(inbox_id: int, message_id: str) -> str: - return messages_api.get_html_message(inbox_id=inbox_id, message_id=message_id) - - -def get_message_as_eml(inbox_id: int, message_id: str) -> str: - return messages_api.get_message_as_eml(inbox_id=inbox_id, message_id=message_id) - - -def get_mail_headers(inbox_id: int, message_id: str) -> str: - return messages_api.get_mail_headers(inbox_id=inbox_id, message_id=message_id) - - -if __name__ == "__main__": - messages = list_messages(inbox_id=INBOX_ID) - print(messages) - if messages: - msg_id = messages[0].id - message = get_message(inbox_id=INBOX_ID, message_id=msg_id) - print(message) - - updated = update_message(inbox_id=INBOX_ID, message_id=msg_id, is_read=True) - print(updated) - - forwarded = forward_message( - inbox_id=INBOX_ID, - message_id=msg_id, - email="example@example.com", - ) - print(forwarded) - - spam = get_spam_report(inbox_id=INBOX_ID, message_id=msg_id) - print(spam) - - html_analysis = get_html_analysis(inbox_id=INBOX_ID, message_id=msg_id) - print(html_analysis) - - text = get_text_message(inbox_id=INBOX_ID, message_id=msg_id) - print(text) - - raw = get_raw_message(inbox_id=INBOX_ID, message_id=msg_id) - print(raw) - - html_src = get_html_source(inbox_id=INBOX_ID, message_id=msg_id) - print(html_src) - - html_msg = get_html_message(inbox_id=INBOX_ID, message_id=msg_id) - print(html_msg) - - eml = get_message_as_eml(inbox_id=INBOX_ID, message_id=msg_id) - print(eml) - - headers = get_mail_headers(inbox_id=INBOX_ID, message_id=msg_id) - print(headers) diff --git a/mailtrap/__init__.py b/mailtrap/__init__.py index 852b53c..8c246e9 100644 --- a/mailtrap/__init__.py +++ b/mailtrap/__init__.py @@ -22,6 +22,9 @@ from .models.contacts import ImportContactParams from .models.contacts import UpdateContactFieldParams from .models.contacts import UpdateContactParams +from .models.domains import CreateDomainParams +from .models.domains import SendSetupInstructionsParams +from .models.domains import UpdateDomainParams from .models.email_campaigns import CreateEmailCampaignParams from .models.email_campaigns import CreateEmailCampaignTemplateAttributes from .models.email_campaigns import EmailCampaign @@ -70,8 +73,9 @@ from .models.paginated_templates import UpdateTemplateParams from .models.permissions import PermissionResourceParams from .models.projects import ProjectParams +from .models.sandboxes import CreateSandboxParams +from .models.sandboxes import UpdateSandboxParams from .models.sending_domains import CreateSendingDomainParams -from .models.sending_domains import SendSetupInstructionsParams from .models.sending_domains import UpdateSendingDomainParams from .models.stats import StatsFilterParams from .models.suppressions import CreateSuppressionParams diff --git a/mailtrap/_deprecation.py b/mailtrap/_deprecation.py new file mode 100644 index 0000000..e89007c --- /dev/null +++ b/mailtrap/_deprecation.py @@ -0,0 +1,77 @@ +import functools +import warnings +from typing import Callable +from typing import Optional +from typing import ParamSpec +from typing import TypeVar + +from mailtrap.exceptions import ClientConfigurationError + +P = ParamSpec("P") +R = TypeVar("R") +T = TypeVar("T") + + +def warn_deprecated(old: str, new: str, stacklevel: int = 3) -> None: + """ + Emit a DeprecationWarning for a renamed name. + + The default `stacklevel` points at the caller of the function that calls this one, + so the warning shows the user's line. Python reports a warning once per call site. + """ + warnings.warn( + f"{old} is deprecated and will be removed in a future major release; " + f"use {new} instead.", + DeprecationWarning, + stacklevel=stacklevel, + ) + + +def renamed_kwargs(**old_to_new: str) -> Callable[[Callable[P, R]], Callable[P, R]]: + """ + Accept the old keyword names of renamed parameters. + + An old keyword is passed on under its new name with a DeprecationWarning; passing + both raises TypeError. Static type checkers see only the new signature. + """ + + def decorator(func: Callable[P, R]) -> Callable[P, R]: + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + for old, new in old_to_new.items(): + if old not in kwargs: + continue + if new in kwargs: + raise TypeError( + f"{func.__qualname__}() got both '{old}' and '{new}'; " + f"pass only '{new}'" + ) + warn_deprecated( + f"{func.__qualname__}({old}=...)", f"{func.__qualname__}({new}=...)" + ) + kwargs[new] = kwargs.pop(old) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def resolve_renamed_arg( + owner: str, old: str, old_value: Optional[T], new: str, new_value: Optional[T] +) -> Optional[T]: + """ + Return the value of a renamed constructor argument whose old name stays in the + signature, so code passing the old keyword still type-checks. + + Passing the old one warns; passing both with different values raises + ClientConfigurationError. + """ + if old_value is None: + return new_value + warn_deprecated(f"{owner}({old}=...)", f"{owner}({new}=...)", stacklevel=4) + if new_value is not None and new_value != old_value: + raise ClientConfigurationError( + f"`{old}` and `{new}` have different values; pass only `{new}`" + ) + return old_value diff --git a/mailtrap/api/domains.py b/mailtrap/api/domains.py new file mode 100644 index 0000000..f43e5bf --- /dev/null +++ b/mailtrap/api/domains.py @@ -0,0 +1,12 @@ +from mailtrap.api.resources.domains import DomainsApi +from mailtrap.http import HttpClient + + +class DomainsBaseApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + @property + def domains(self) -> DomainsApi: + return DomainsApi(account_id=self._account_id, client=self._client) diff --git a/mailtrap/api/resources/attachments.py b/mailtrap/api/resources/attachments.py index cb66357..b68fb4f 100644 --- a/mailtrap/api/resources/attachments.py +++ b/mailtrap/api/resources/attachments.py @@ -1,5 +1,6 @@ from typing import Optional +from mailtrap._deprecation import renamed_kwargs from mailtrap.http import HttpClient from mailtrap.models.attachments import Attachment @@ -9,34 +10,36 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._account_id = account_id self._client = client + @renamed_kwargs(inbox_id="sandbox_id") def get_list( self, - inbox_id: int, + sandbox_id: int, message_id: int, ) -> list[Attachment]: """Lists attachments with their details and download paths.""" - response = self._client.get(self._api_path(inbox_id, message_id)) + response = self._client.get(self._api_path(sandbox_id, message_id)) return [Attachment(**attachment) for attachment in response] + @renamed_kwargs(inbox_id="sandbox_id") def get( self, - inbox_id: int, + sandbox_id: int, message_id: int, attachment_id: int, ) -> Attachment: - """Get message single attachment by inbox_id, message_id and attachment_id.""" - response = self._client.get(self._api_path(inbox_id, message_id, attachment_id)) + """Get message single attachment by sandbox_id, message_id and attachment_id.""" + response = self._client.get(self._api_path(sandbox_id, message_id, attachment_id)) return Attachment(**response) def _api_path( self, - inbox_id: int, + sandbox_id: int, message_id: int, attachment_id: Optional[int] = None, ) -> str: path = ( f"/api/accounts/{self._account_id}" - f"/inboxes/{inbox_id}" + f"/inboxes/{sandbox_id}" f"/messages/{message_id}" "/attachments" ) diff --git a/mailtrap/api/resources/billing.py b/mailtrap/api/resources/billing.py index 68825ed..73597c2 100644 --- a/mailtrap/api/resources/billing.py +++ b/mailtrap/api/resources/billing.py @@ -7,6 +7,6 @@ def __init__(self, client: HttpClient) -> None: self._client = client def get_current_billing_usage(self, account_id: int) -> BillingCycleUsage: - """Get current billing cycle usage for Email Testing and Email Sending.""" + """Get current billing cycle usage for Email Sandbox and Email Sending.""" response = self._client.get(f"/api/accounts/{account_id}/billing/usage") return BillingCycleUsage(**response) diff --git a/mailtrap/api/resources/company_info.py b/mailtrap/api/resources/company_info.py index 58cf241..9142224 100644 --- a/mailtrap/api/resources/company_info.py +++ b/mailtrap/api/resources/company_info.py @@ -1,3 +1,4 @@ +from mailtrap._deprecation import renamed_kwargs from mailtrap.http import HttpClient from mailtrap.models.company_info import CompanyInfo from mailtrap.models.company_info import CompanyInfoResponse @@ -9,39 +10,42 @@ class CompanyInfoApi: def __init__(self, client: HttpClient) -> None: self._client = client - def get(self, sending_domain_id: int) -> CompanyInfo: + @renamed_kwargs(sending_domain_id="domain_id") + def get(self, domain_id: int) -> CompanyInfo: """ - Get the company info associated with a sending domain. + Get the company info associated with a domain. """ - response = self._client.get(self._api_path(sending_domain_id)) + response = self._client.get(self._api_path(domain_id)) return CompanyInfoResponse(**response).data + @renamed_kwargs(sending_domain_id="domain_id") def create( - self, sending_domain_id: int, company_info_params: CreateCompanyInfoParams + self, domain_id: int, company_info_params: CreateCompanyInfoParams ) -> CompanyInfo: """ - Create the company info for a sending domain. Company info is required + Create the company info for a domain. Company info is required for domain compliance verification. """ response = self._client.post( - self._api_path(sending_domain_id), + self._api_path(domain_id), json={"company_info": company_info_params.api_data}, ) return CompanyInfoResponse(**response).data + @renamed_kwargs(sending_domain_id="domain_id") def update( - self, sending_domain_id: int, company_info_params: UpdateCompanyInfoParams + self, domain_id: int, company_info_params: UpdateCompanyInfoParams ) -> CompanyInfo: """ - Update the company info for a sending domain. Only the fields supplied + Update the company info for a domain. Only the fields supplied in `company_info_params` are sent to the API. """ response = self._client.patch( - self._api_path(sending_domain_id), + self._api_path(domain_id), json={"company_info": company_info_params.api_data}, ) return CompanyInfoResponse(**response).data @staticmethod - def _api_path(sending_domain_id: int) -> str: - return f"/api/domains/{sending_domain_id}/company_info" + def _api_path(domain_id: int) -> str: + return f"/api/domains/{domain_id}/company_info" diff --git a/mailtrap/api/resources/domains.py b/mailtrap/api/resources/domains.py new file mode 100644 index 0000000..104059d --- /dev/null +++ b/mailtrap/api/resources/domains.py @@ -0,0 +1,86 @@ +from typing import Optional + +from mailtrap._deprecation import renamed_kwargs +from mailtrap.http import HttpClient +from mailtrap.models.common import DeletedObject +from mailtrap.models.domains import CreateDomainParams +from mailtrap.models.domains import Domain +from mailtrap.models.domains import SendSetupInstructionsParams +from mailtrap.models.domains import SendSetupInstructionsResponse +from mailtrap.models.domains import UpdateDomainParams + + +class DomainsApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + def get_list(self) -> list[Domain]: + """ + Get domains and their statuses. + """ + response = self._client.get(self._api_path()) + domains = response.get("data", []) + return [Domain(**domain) for domain in domains] + + @renamed_kwargs(sending_domain_id="domain_id") + def get_by_id(self, domain_id: int) -> Domain: + """ + Get domain data and its status. + """ + response = self._client.get(self._api_path(domain_id)) + return Domain(**response) + + def create(self, domain_params: CreateDomainParams) -> Domain: + """ + Create a domain. To later check the status of the newly created domain, + review the compliance_status and dns_verified fields in the response + of the Get domain by ID or Get domains endpoints. + """ + response = self._client.post( + self._api_path(), json={"sending_domain": domain_params.api_data} + ) + return Domain(**response) + + @renamed_kwargs(sending_domain_id="domain_id") + def update(self, domain_id: int, domain_params: UpdateDomainParams) -> Domain: + """ + Update configuration settings for a domain. Only the fields + supplied in `domain_params` are sent to the API. + """ + response = self._client.patch( + self._api_path(domain_id), + json={"sending_domain": domain_params.api_data}, + ) + return Domain(**response) + + @renamed_kwargs(sending_domain_id="domain_id") + def delete(self, domain_id: int) -> DeletedObject: + """ + Delete a domain. + """ + self._client.delete(self._api_path(domain_id)) + return DeletedObject(id=domain_id) + + @renamed_kwargs(sending_domain_id="domain_id") + def send_setup_instructions( + self, + domain_id: int, + instructions_params: SendSetupInstructionsParams, + ) -> SendSetupInstructionsResponse: + """ + Send domain setup instructions. + """ + self._client.post( + f"{self._api_path(domain_id)}/send_setup_instructions", + json=instructions_params.api_data, + ) + return SendSetupInstructionsResponse( + message="Instructions email has been sent successfully" + ) + + def _api_path(self, domain_id: Optional[int] = None) -> str: + path = f"/api/accounts/{self._account_id}/sending_domains" + if domain_id is not None: + path = f"{path}/{domain_id}" + return path diff --git a/mailtrap/api/resources/email_campaigns.py b/mailtrap/api/resources/email_campaigns.py index e750850..6f0f101 100644 --- a/mailtrap/api/resources/email_campaigns.py +++ b/mailtrap/api/resources/email_campaigns.py @@ -40,7 +40,7 @@ def get_by_id(self, email_campaign_id: int) -> EmailCampaign: def create(self, campaign_params: CreateEmailCampaignParams) -> EmailCampaign: """ Create a new email campaign in the ``draft`` state. The campaign must - reference an existing sending domain via ``domain_id`` and + reference an existing domain via ``domain_id`` and include a template ``subject`` within ``template_attributes``. """ response = self._client.post(self._api_path(), json=campaign_params.api_data) diff --git a/mailtrap/api/resources/inboxes.py b/mailtrap/api/resources/inboxes.py index 5ca92e9..63f0870 100644 --- a/mailtrap/api/resources/inboxes.py +++ b/mailtrap/api/resources/inboxes.py @@ -1,74 +1,14 @@ -from typing import Optional - +from mailtrap._deprecation import warn_deprecated +from mailtrap.api.resources.sandboxes import SandboxesApi from mailtrap.http import HttpClient -from mailtrap.models.inboxes import CreateInboxParams -from mailtrap.models.inboxes import Inbox -from mailtrap.models.inboxes import UpdateInboxParams - - -class InboxesApi: - def __init__(self, client: HttpClient, account_id: str) -> None: - self._account_id = account_id - self._client = client - - def get_list(self) -> list[Inbox]: - """Get a list of inboxes.""" - response = self._client.get(self._api_path()) - return [Inbox(**inbox) for inbox in response] - - def get_by_id(self, inbox_id: int) -> Inbox: - """Get inbox attributes by inbox id.""" - response = self._client.get(self._api_path(inbox_id)) - return Inbox(**response) - - def create(self, project_id: int, inbox_params: CreateInboxParams) -> Inbox: - """Create an inbox in a project.""" - response = self._client.post( - f"/api/accounts/{self._account_id}/projects/{project_id}/inboxes", - json={"inbox": inbox_params.api_data}, - ) - return Inbox(**response) - def update(self, inbox_id: int, inbox_params: UpdateInboxParams) -> Inbox: - """Update inbox name, inbox email username.""" - response = self._client.patch( - self._api_path(inbox_id), - json={"inbox": inbox_params.api_data}, - ) - return Inbox(**response) - def delete(self, inbox_id: int) -> Inbox: - """Delete an inbox with all its emails.""" - response = self._client.delete(self._api_path(inbox_id)) - return Inbox(**response) +class InboxesApi(SandboxesApi): + """ + .. deprecated:: + Use :class:`mailtrap.api.resources.sandboxes.SandboxesApi` instead. + """ - def clean(self, inbox_id: int) -> Inbox: - """Delete all messages (emails) from inbox.""" - response = self._client.patch(f"{self._api_path(inbox_id)}/clean") - return Inbox(**response) - - def mark_as_read(self, inbox_id: int) -> Inbox: - """Mark all messages in the inbox as read.""" - response = self._client.patch(f"{self._api_path(inbox_id)}/all_read") - return Inbox(**response) - - def reset_credentials(self, inbox_id: int) -> Inbox: - """Reset SMTP credentials of the inbox.""" - response = self._client.patch(f"{self._api_path(inbox_id)}/reset_credentials") - return Inbox(**response) - - def enable_email_address(self, inbox_id: int) -> Inbox: - """Turn the email address of the inbox on/off.""" - response = self._client.patch(f"{self._api_path(inbox_id)}/toggle_email_username") - return Inbox(**response) - - def reset_email_username(self, inbox_id: int) -> Inbox: - """Reset username of email address per inbox.""" - response = self._client.patch(f"{self._api_path(inbox_id)}/reset_email_username") - return Inbox(**response) - - def _api_path(self, inbox_id: Optional[int] = None) -> str: - path = f"/api/accounts/{self._account_id}/inboxes" - if inbox_id: - return f"{path}/{inbox_id}" - return path + def __init__(self, client: HttpClient, account_id: str) -> None: + warn_deprecated("InboxesApi", "SandboxesApi") + super().__init__(client=client, account_id=account_id) diff --git a/mailtrap/api/resources/messages.py b/mailtrap/api/resources/messages.py index d6f15dc..348ce28 100644 --- a/mailtrap/api/resources/messages.py +++ b/mailtrap/api/resources/messages.py @@ -2,6 +2,7 @@ from typing import Optional from typing import cast +from mailtrap._deprecation import renamed_kwargs from mailtrap.http import HttpClient from mailtrap.models.messages import AnalysisReport from mailtrap.models.messages import AnalysisReportResponse @@ -16,44 +17,48 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._account_id = account_id self._client = client - def show_message(self, inbox_id: int, message_id: int) -> EmailMessage: + @renamed_kwargs(inbox_id="sandbox_id") + def show_message(self, sandbox_id: int, message_id: int) -> EmailMessage: """Get email message by ID.""" - response = self._client.get(self._api_path(inbox_id, message_id)) + response = self._client.get(self._api_path(sandbox_id, message_id)) return EmailMessage(**response) + @renamed_kwargs(inbox_id="sandbox_id") def update( - self, inbox_id: int, message_id: int, message_params: UpdateEmailMessageParams + self, sandbox_id: int, message_id: int, message_params: UpdateEmailMessageParams ) -> EmailMessage: """ Update message attributes (right now only the **is_read** attribute is available for modification). """ response = self._client.patch( - self._api_path(inbox_id, message_id), + self._api_path(sandbox_id, message_id), json={"message": message_params.api_data}, ) return EmailMessage(**response) - def delete(self, inbox_id: int, message_id: int) -> EmailMessage: - """Delete message from inbox.""" - response = self._client.delete(self._api_path(inbox_id, message_id)) + @renamed_kwargs(inbox_id="sandbox_id") + def delete(self, sandbox_id: int, message_id: int) -> EmailMessage: + """Delete message from sandbox.""" + response = self._client.delete(self._api_path(sandbox_id, message_id)) return EmailMessage(**response) + @renamed_kwargs(inbox_id="sandbox_id") def get_list( self, - inbox_id: int, + sandbox_id: int, search: Optional[str] = None, last_id: Optional[int] = None, page: Optional[int] = None, ) -> list[EmailMessage]: """ - Get messages from the inbox. + Get messages from the sandbox. The response contains up to 30 messages per request. You can use pagination parameters (`last_id` or `page`) to retrieve additional results. Args: - inbox_id (int): ID of the inbox to retrieve messages from. + sandbox_id (int): ID of the sandbox to retrieve messages from. search (Optional[str]): Search query string. Matches `subject`, `to_email`, and `to_name`. Example: `"welcome"` @@ -84,69 +89,80 @@ def get_list( if page: params["page"] = page - response = self._client.get(self._api_path(inbox_id), params=params) + response = self._client.get(self._api_path(sandbox_id), params=params) return [EmailMessage(**message) for message in response] - def forward(self, inbox_id: int, message_id: int, email: str) -> ForwardedMessage: + @renamed_kwargs(inbox_id="sandbox_id") + def forward(self, sandbox_id: int, message_id: int, email: str) -> ForwardedMessage: """ Forward message to an email address. The email address must be confirmed by the recipient in advance. """ response = self._client.post( - f"{self._api_path(inbox_id, message_id)}/forward", json={"email": email} + f"{self._api_path(sandbox_id, message_id)}/forward", json={"email": email} ) return ForwardedMessage(**response) - def get_spam_report(self, inbox_id: int, message_id: int) -> SpamReport: + @renamed_kwargs(inbox_id="sandbox_id") + def get_spam_report(self, sandbox_id: int, message_id: int) -> SpamReport: """Get a brief spam report by message ID.""" - response = self._client.get(f"{self._api_path(inbox_id, message_id)}/spam_report") + response = self._client.get( + f"{self._api_path(sandbox_id, message_id)}/spam_report" + ) return SpamReport(**response["report"]) - def get_html_analysis(self, inbox_id: int, message_id: int) -> AnalysisReport: + @renamed_kwargs(inbox_id="sandbox_id") + def get_html_analysis(self, sandbox_id: int, message_id: int) -> AnalysisReport: """Get a brief HTML report by message ID.""" - response = self._client.get(f"{self._api_path(inbox_id, message_id)}/analyze") + response = self._client.get(f"{self._api_path(sandbox_id, message_id)}/analyze") return AnalysisReportResponse(**response).report - def get_text_message(self, inbox_id: int, message_id: int) -> str: + @renamed_kwargs(inbox_id="sandbox_id") + def get_text_message(self, sandbox_id: int, message_id: int) -> str: """Get text email body, if it exists.""" return cast( - str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.txt") + str, self._client.get(f"{self._api_path(sandbox_id, message_id)}/body.txt") ) - def get_raw_message(self, inbox_id: int, message_id: int) -> str: + @renamed_kwargs(inbox_id="sandbox_id") + def get_raw_message(self, sandbox_id: int, message_id: int) -> str: """Get raw email body.""" return cast( - str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.raw") + str, self._client.get(f"{self._api_path(sandbox_id, message_id)}/body.raw") ) - def get_html_source(self, inbox_id: int, message_id: int) -> str: + @renamed_kwargs(inbox_id="sandbox_id") + def get_html_source(self, sandbox_id: int, message_id: int) -> str: """Get HTML source of email.""" return cast( str, - self._client.get(f"{self._api_path(inbox_id, message_id)}/body.htmlsource"), + self._client.get(f"{self._api_path(sandbox_id, message_id)}/body.htmlsource"), ) - def get_html_message(self, inbox_id: int, message_id: int) -> str: + @renamed_kwargs(inbox_id="sandbox_id") + def get_html_message(self, sandbox_id: int, message_id: int) -> str: """Get formatted HTML email body. Not applicable for plain text emails.""" return cast( - str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.html") + str, self._client.get(f"{self._api_path(sandbox_id, message_id)}/body.html") ) - def get_message_as_eml(self, inbox_id: int, message_id: int) -> str: + @renamed_kwargs(inbox_id="sandbox_id") + def get_message_as_eml(self, sandbox_id: int, message_id: int) -> str: """Get email message in .eml format.""" return cast( - str, self._client.get(f"{self._api_path(inbox_id, message_id)}/body.eml") + str, self._client.get(f"{self._api_path(sandbox_id, message_id)}/body.eml") ) - def get_mail_headers(self, inbox_id: int, message_id: int) -> dict[str, Any]: + @renamed_kwargs(inbox_id="sandbox_id") + def get_mail_headers(self, sandbox_id: int, message_id: int) -> dict[str, Any]: """Get mail headers of a message.""" response = self._client.get( - f"{self._api_path(inbox_id, message_id)}/mail_headers" + f"{self._api_path(sandbox_id, message_id)}/mail_headers" ) return cast(dict[str, Any], response["headers"]) - def _api_path(self, inbox_id: int, message_id: Optional[int] = None) -> str: - path = f"/api/accounts/{self._account_id}/inboxes/{inbox_id}/messages" + def _api_path(self, sandbox_id: int, message_id: Optional[int] = None) -> str: + path = f"/api/accounts/{self._account_id}/inboxes/{sandbox_id}/messages" if message_id: return f"{path}/{message_id}" return path diff --git a/mailtrap/api/resources/permissions.py b/mailtrap/api/resources/permissions.py index 5ef4414..3f9c4a4 100644 --- a/mailtrap/api/resources/permissions.py +++ b/mailtrap/api/resources/permissions.py @@ -10,7 +10,7 @@ def __init__(self, client: HttpClient) -> None: def get_resources(self, account_id: int) -> list[PermissionResource]: """ - Get all resources in your account (Inboxes, Projects, Domains, + Get all resources in your account (Sandboxes, Projects, Domains, Email Campaigns, Billing and Account itself) to which the token has admin access. """ diff --git a/mailtrap/api/resources/projects.py b/mailtrap/api/resources/projects.py index 34df6d5..789337e 100644 --- a/mailtrap/api/resources/projects.py +++ b/mailtrap/api/resources/projects.py @@ -12,12 +12,12 @@ def __init__(self, client: HttpClient, account_id: str) -> None: self._client = client def get_list(self) -> list[Project]: - """List projects and their inboxes to which the API token has access.""" + """List projects and their sandboxes to which the API token has access.""" response = self._client.get(self._api_path()) return [Project(**project) for project in response] def get_by_id(self, project_id: int) -> Project: - """Get the project and its inboxes.""" + """Get the project and its sandboxes.""" response = self._client.get(self._api_path(project_id)) return Project(**response) @@ -44,7 +44,7 @@ def update(self, project_id: int, project_params: ProjectParams) -> Project: return Project(**response) def delete(self, project_id: int) -> DeletedObject: - """Delete project and its inboxes.""" + """Delete project and its sandboxes.""" response = self._client.delete(self._api_path(project_id)) return DeletedObject(**response) diff --git a/mailtrap/api/resources/sandboxes.py b/mailtrap/api/resources/sandboxes.py new file mode 100644 index 0000000..5dc1aa8 --- /dev/null +++ b/mailtrap/api/resources/sandboxes.py @@ -0,0 +1,88 @@ +from typing import Optional + +from mailtrap._deprecation import renamed_kwargs +from mailtrap.http import HttpClient +from mailtrap.models.sandboxes import CreateSandboxParams +from mailtrap.models.sandboxes import Sandbox +from mailtrap.models.sandboxes import UpdateSandboxParams + + +class SandboxesApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + def get_list(self) -> list[Sandbox]: + """Get a list of sandboxes.""" + response = self._client.get(self._api_path()) + return [Sandbox(**sandbox) for sandbox in response] + + @renamed_kwargs(inbox_id="sandbox_id") + def get_by_id(self, sandbox_id: int) -> Sandbox: + """Get sandbox attributes by sandbox id.""" + response = self._client.get(self._api_path(sandbox_id)) + return Sandbox(**response) + + @renamed_kwargs(inbox_params="sandbox_params") + def create(self, project_id: int, sandbox_params: CreateSandboxParams) -> Sandbox: + """Create a sandbox in a project.""" + response = self._client.post( + f"/api/accounts/{self._account_id}/projects/{project_id}/inboxes", + json={"inbox": sandbox_params.api_data}, + ) + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id", inbox_params="sandbox_params") + def update(self, sandbox_id: int, sandbox_params: UpdateSandboxParams) -> Sandbox: + """Update sandbox name, sandbox email username.""" + response = self._client.patch( + self._api_path(sandbox_id), + json={"inbox": sandbox_params.api_data}, + ) + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id") + def delete(self, sandbox_id: int) -> Sandbox: + """Delete a sandbox with all its emails.""" + response = self._client.delete(self._api_path(sandbox_id)) + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id") + def clean(self, sandbox_id: int) -> Sandbox: + """Delete all messages (emails) from sandbox.""" + response = self._client.patch(f"{self._api_path(sandbox_id)}/clean") + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id") + def mark_as_read(self, sandbox_id: int) -> Sandbox: + """Mark all messages in the sandbox as read.""" + response = self._client.patch(f"{self._api_path(sandbox_id)}/all_read") + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id") + def reset_credentials(self, sandbox_id: int) -> Sandbox: + """Reset SMTP credentials of the sandbox.""" + response = self._client.patch(f"{self._api_path(sandbox_id)}/reset_credentials") + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id") + def enable_email_address(self, sandbox_id: int) -> Sandbox: + """Turn the email address of the sandbox on/off.""" + response = self._client.patch( + f"{self._api_path(sandbox_id)}/toggle_email_username" + ) + return Sandbox(**response) + + @renamed_kwargs(inbox_id="sandbox_id") + def reset_email_username(self, sandbox_id: int) -> Sandbox: + """Reset username of email address per sandbox.""" + response = self._client.patch( + f"{self._api_path(sandbox_id)}/reset_email_username" + ) + return Sandbox(**response) + + def _api_path(self, sandbox_id: Optional[int] = None) -> str: + path = f"/api/accounts/{self._account_id}/inboxes" + if sandbox_id: + return f"{path}/{sandbox_id}" + return path diff --git a/mailtrap/api/resources/sending_domains.py b/mailtrap/api/resources/sending_domains.py index cffbd9f..3556544 100644 --- a/mailtrap/api/resources/sending_domains.py +++ b/mailtrap/api/resources/sending_domains.py @@ -1,83 +1,14 @@ -from typing import Optional - +from mailtrap._deprecation import warn_deprecated +from mailtrap.api.resources.domains import DomainsApi from mailtrap.http import HttpClient -from mailtrap.models.common import DeletedObject -from mailtrap.models.sending_domains import CreateSendingDomainParams -from mailtrap.models.sending_domains import SendingDomain -from mailtrap.models.sending_domains import SendSetupInstructionsParams -from mailtrap.models.sending_domains import SendSetupInstructionsResponse -from mailtrap.models.sending_domains import UpdateSendingDomainParams - - -class SendingDomainsApi: - def __init__(self, client: HttpClient, account_id: str) -> None: - self._account_id = account_id - self._client = client - - def get_list(self) -> list[SendingDomain]: - """ - Get sending domains and their statuses. - """ - response = self._client.get(self._api_path()) - domains = response.get("data", []) - return [SendingDomain(**domain) for domain in domains] - def get_by_id(self, sending_domain_id: int) -> SendingDomain: - """ - Get domain data and its status. - """ - response = self._client.get(self._api_path(sending_domain_id)) - return SendingDomain(**response) - def create(self, domain_params: CreateSendingDomainParams) -> SendingDomain: - """ - Create a sending domain. To later check the status of the newly created domain, - review the compliance_status and dns_verified fields in the response - of the Get domain by ID or Get sending domains endpoints. - """ - response = self._client.post( - self._api_path(), json={"sending_domain": domain_params.api_data} - ) - return SendingDomain(**response) +class SendingDomainsApi(DomainsApi): + """ + .. deprecated:: + Use :class:`mailtrap.api.resources.domains.DomainsApi` instead. + """ - def update( - self, sending_domain_id: int, domain_params: UpdateSendingDomainParams - ) -> SendingDomain: - """ - Update configuration settings for a sending domain. Only the fields - supplied in `domain_params` are sent to the API. - """ - response = self._client.patch( - self._api_path(sending_domain_id), - json={"sending_domain": domain_params.api_data}, - ) - return SendingDomain(**response) - - def delete(self, sending_domain_id: int) -> DeletedObject: - """ - Delete a sending domain. - """ - self._client.delete(self._api_path(sending_domain_id)) - return DeletedObject(id=sending_domain_id) - - def send_setup_instructions( - self, - sending_domain_id: int, - instructions_params: SendSetupInstructionsParams, - ) -> SendSetupInstructionsResponse: - """ - Send sending domain setup instructions. - """ - self._client.post( - f"{self._api_path(sending_domain_id)}/send_setup_instructions", - json=instructions_params.api_data, - ) - return SendSetupInstructionsResponse( - message="Instructions email has been sent successfully" - ) - - def _api_path(self, sending_domain_id: Optional[int] = None) -> str: - path = f"/api/accounts/{self._account_id}/sending_domains" - if sending_domain_id is not None: - path = f"{path}/{sending_domain_id}" - return path + def __init__(self, client: HttpClient, account_id: str) -> None: + warn_deprecated("SendingDomainsApi", "DomainsApi") + super().__init__(client=client, account_id=account_id) diff --git a/mailtrap/api/resources/tracking_opt_outs.py b/mailtrap/api/resources/tracking_opt_outs.py index 0c904e4..3565efd 100644 --- a/mailtrap/api/resources/tracking_opt_outs.py +++ b/mailtrap/api/resources/tracking_opt_outs.py @@ -26,7 +26,7 @@ def get_list( def create(self, params: CreateTrackingOptOutParams) -> TrackingOptOut: """ - Add an email address to the tracking opt-out list for a sending domain. + Add an email address to the tracking opt-out list for a domain. """ response = self._client.post(self._api_path(), json=params.api_data) return TrackingOptOutResponse(**response).data diff --git a/mailtrap/api/sandbox.py b/mailtrap/api/sandbox.py new file mode 100644 index 0000000..aae65c1 --- /dev/null +++ b/mailtrap/api/sandbox.py @@ -0,0 +1,27 @@ +from mailtrap.api.resources.attachments import AttachmentsApi +from mailtrap.api.resources.messages import MessagesApi +from mailtrap.api.resources.projects import ProjectsApi +from mailtrap.api.resources.sandboxes import SandboxesApi +from mailtrap.http import HttpClient + + +class SandboxApi: + def __init__(self, client: HttpClient, account_id: str) -> None: + self._account_id = account_id + self._client = client + + @property + def projects(self) -> ProjectsApi: + return ProjectsApi(account_id=self._account_id, client=self._client) + + @property + def sandboxes(self) -> SandboxesApi: + return SandboxesApi(account_id=self._account_id, client=self._client) + + @property + def messages(self) -> MessagesApi: + return MessagesApi(account_id=self._account_id, client=self._client) + + @property + def attachments(self) -> AttachmentsApi: + return AttachmentsApi(account_id=self._account_id, client=self._client) diff --git a/mailtrap/api/sending.py b/mailtrap/api/sending.py index 6837e0d..6205b08 100644 --- a/mailtrap/api/sending.py +++ b/mailtrap/api/sending.py @@ -1,5 +1,6 @@ from typing import Optional +from mailtrap._deprecation import resolve_renamed_arg from mailtrap.http import HttpClient from mailtrap.models.mail import BaseMail from mailtrap.models.mail import SendingMailResponse @@ -8,13 +9,24 @@ class SendingApi: - def __init__(self, client: HttpClient, inbox_id: Optional[str] = None) -> None: - self._inbox_id = inbox_id + def __init__( + self, + client: HttpClient, + inbox_id: Optional[str] = None, + sandbox_id: Optional[str] = None, + ) -> None: + """ + .. deprecated:: + The `inbox_id` argument; use `sandbox_id` instead. + """ + self._sandbox_id = resolve_renamed_arg( + "SendingApi", "inbox_id", inbox_id, "sandbox_id", sandbox_id + ) self._client = client def _get_api_url(self, base_url: str) -> str: - if self._inbox_id: - return f"{base_url}/{self._inbox_id}" + if self._sandbox_id: + return f"{base_url}/{self._sandbox_id}" return base_url def send(self, mail: BaseMail) -> SendingMailResponse: diff --git a/mailtrap/api/sending_domains.py b/mailtrap/api/sending_domains.py index b12b107..236f0d4 100644 --- a/mailtrap/api/sending_domains.py +++ b/mailtrap/api/sending_domains.py @@ -1,12 +1,19 @@ -from mailtrap.api.resources.sending_domains import SendingDomainsApi -from mailtrap.http import HttpClient +from mailtrap._deprecation import warn_deprecated +from mailtrap.api.domains import DomainsBaseApi +from mailtrap.api.resources.domains import DomainsApi -class SendingDomainsBaseApi: - def __init__(self, client: HttpClient, account_id: str) -> None: - self._account_id = account_id - self._client = client +class SendingDomainsBaseApi(DomainsBaseApi): + """ + .. deprecated:: + Use :class:`mailtrap.api.domains.DomainsBaseApi` instead. + """ @property - def sending_domains(self) -> SendingDomainsApi: - return SendingDomainsApi(account_id=self._account_id, client=self._client) + def sending_domains(self) -> DomainsApi: + """ + .. deprecated:: + Use :attr:`domains` instead. + """ + warn_deprecated("SendingDomainsBaseApi.sending_domains", "DomainsBaseApi.domains") + return self.domains diff --git a/mailtrap/api/testing.py b/mailtrap/api/testing.py index e522056..b1c1778 100644 --- a/mailtrap/api/testing.py +++ b/mailtrap/api/testing.py @@ -1,32 +1,27 @@ from typing import Optional -from mailtrap.api.resources.attachments import AttachmentsApi -from mailtrap.api.resources.inboxes import InboxesApi -from mailtrap.api.resources.messages import MessagesApi -from mailtrap.api.resources.projects import ProjectsApi +from mailtrap._deprecation import warn_deprecated +from mailtrap.api.resources.sandboxes import SandboxesApi +from mailtrap.api.sandbox import SandboxApi from mailtrap.http import HttpClient -class TestingApi: +class TestingApi(SandboxApi): + """ + .. deprecated:: + Use :class:`mailtrap.api.sandbox.SandboxApi` instead. + """ + 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) - - @property - def inboxes(self) -> InboxesApi: - return InboxesApi(account_id=self._account_id, client=self._client) - - @property - def messages(self) -> MessagesApi: - return MessagesApi(account_id=self._account_id, client=self._client) + super().__init__(client=client, account_id=account_id) @property - def attachments(self) -> AttachmentsApi: - return AttachmentsApi(account_id=self._account_id, client=self._client) + def inboxes(self) -> SandboxesApi: + """ + .. deprecated:: + Use :attr:`sandboxes` instead. + """ + warn_deprecated("TestingApi.inboxes", "SandboxApi.sandboxes") + return self.sandboxes diff --git a/mailtrap/client.py b/mailtrap/client.py index d62cf49..909848e 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -6,8 +6,11 @@ from pydantic import TypeAdapter +from mailtrap._deprecation import resolve_renamed_arg +from mailtrap._deprecation import warn_deprecated from mailtrap.api.company_info import CompanyInfoBaseApi from mailtrap.api.contacts import ContactsBaseApi +from mailtrap.api.domains import DomainsBaseApi from mailtrap.api.email_campaigns import EmailCampaignsBaseApi from mailtrap.api.email_logs import EmailLogsBaseApi from mailtrap.api.general import GeneralApi @@ -16,6 +19,7 @@ from mailtrap.api.organizations import OrganizationsBaseApi from mailtrap.api.paginated_templates import TemplatesBaseApi from mailtrap.api.resources.stats import StatsApi +from mailtrap.api.sandbox import SandboxApi from mailtrap.api.sending import SendingApi from mailtrap.api.sending_domains import SendingDomainsBaseApi from mailtrap.api.suppressions import SuppressionsBaseApi @@ -61,14 +65,21 @@ def __init__( inbox_id: Optional[str] = None, organization_id: Optional[str] = None, user_agent: Optional[str] = None, + sandbox_id: Optional[str] = None, ) -> None: + """ + .. deprecated:: + The `inbox_id` argument; use `sandbox_id` instead. + """ self.token = token self.api_host = api_host self.api_port = api_port self.bulk = bulk self.sandbox = sandbox self.account_id = account_id - self.inbox_id = inbox_id + self.sandbox_id = resolve_renamed_arg( + "MailtrapClient", "inbox_id", inbox_id, "sandbox_id", sandbox_id + ) self.organization_id = organization_id self._user_agent = ( user_agent if user_agent is not None else self.DEFAULT_USER_AGENT @@ -88,12 +99,24 @@ def inbound_api(self) -> InboundBaseApi: client=HttpClient(host=GENERAL_HOST, headers=self.headers), ) + @property + def sandbox_api(self) -> SandboxApi: + self._validate_account_id("Sandbox API") + return SandboxApi( + account_id=cast(str, self.account_id), + client=HttpClient(host=GENERAL_HOST, headers=self.headers), + ) + @property def testing_api(self) -> TestingApi: - self._validate_account_id() + """ + .. deprecated:: + Use :attr:`sandbox_api` instead. + """ + warn_deprecated("MailtrapClient.testing_api", "MailtrapClient.sandbox_api") + self._validate_account_id("Sandbox API") return TestingApi( account_id=cast(str, self.account_id), - inbox_id=self.inbox_id, client=HttpClient(host=GENERAL_HOST, headers=self.headers), ) @@ -129,9 +152,24 @@ def suppressions_api(self) -> SuppressionsBaseApi: client=HttpClient(host=GENERAL_HOST, headers=self.headers), ) + @property + def domains_api(self) -> DomainsBaseApi: + self._validate_account_id("Domains API") + return DomainsBaseApi( + account_id=cast(str, self.account_id), + client=HttpClient(host=GENERAL_HOST, headers=self.headers), + ) + @property def sending_domains_api(self) -> SendingDomainsBaseApi: - self._validate_account_id("Sending Domains API") + """ + .. deprecated:: + Use :attr:`domains_api` instead. + """ + warn_deprecated( + "MailtrapClient.sending_domains_api", "MailtrapClient.domains_api" + ) + self._validate_account_id("Domains API") return SendingDomainsBaseApi( account_id=cast(str, self.account_id), client=HttpClient(host=GENERAL_HOST, headers=self.headers), @@ -188,7 +226,7 @@ def webhooks_api(self) -> WebhooksBaseApi: @property def sending_api(self) -> SendingApi: http_client = HttpClient(host=self._sending_api_host, headers=self.headers) - return SendingApi(client=http_client, inbox_id=self.inbox_id) + return SendingApi(client=http_client, sandbox_id=self.sandbox_id) @property def stats_api(self) -> StatsApi: @@ -227,10 +265,24 @@ def api_send_url(self) -> str: stacklevel=2, ) url = f"{self.base_url}/api/send" - if self.sandbox and self.inbox_id: - return f"{url}/{self.inbox_id}" + if self.sandbox and self.sandbox_id: + return f"{url}/{self.sandbox_id}" return url + @property + def inbox_id(self) -> Optional[str]: + """ + .. deprecated:: + Use :attr:`sandbox_id` instead. + """ + warn_deprecated("MailtrapClient.inbox_id", "MailtrapClient.sandbox_id") + return self.sandbox_id + + @inbox_id.setter + def inbox_id(self, value: Optional[str]) -> None: + warn_deprecated("MailtrapClient.inbox_id", "MailtrapClient.sandbox_id") + self.sandbox_id = value + @property def headers(self) -> dict[str, str]: return { @@ -249,7 +301,7 @@ def _sending_api_host(self) -> str: return BULK_HOST return SENDING_HOST - def _validate_account_id(self, api_name: str = "Testing API") -> None: + def _validate_account_id(self, api_name: str) -> None: if not self.account_id: raise ClientConfigurationError(f"`account_id` is required for {api_name}") @@ -260,12 +312,12 @@ def _validate_organization_id(self, api_name: str) -> None: ) def _validate_itself(self) -> None: - if self.sandbox and not self.inbox_id: - raise ClientConfigurationError("`inbox_id` is required for sandbox mode") + if self.sandbox and not self.sandbox_id: + raise ClientConfigurationError("`sandbox_id` is required for sandbox mode") - if not self.sandbox and self.inbox_id: + if not self.sandbox and self.sandbox_id: raise ClientConfigurationError( - "`inbox_id` is not allowed in non-sandbox mode" + "`sandbox_id` is not allowed in non-sandbox mode" ) if self.bulk and self.sandbox: diff --git a/mailtrap/models/domains.py b/mailtrap/models/domains.py new file mode 100644 index 0000000..a1b45c1 --- /dev/null +++ b/mailtrap/models/domains.py @@ -0,0 +1,69 @@ +from typing import Optional + +from pydantic import Field +from pydantic.dataclasses import dataclass + +from mailtrap.models.common import RequestParams + + +@dataclass +class DomainPermissions: + can_read: bool + can_update: bool + can_destroy: bool + + +@dataclass +class DnsRecord: + key: str + domain: str + type: str + value: str + status: str + name: str + + +@dataclass +class Domain: + id: int + domain_name: str + demo: bool + compliance_status: str + dns_verified: bool + open_tracking_enabled: bool + click_tracking_enabled: bool + auto_unsubscribe_link_enabled: bool + custom_domain_tracking_enabled: bool + health_alerts_enabled: bool + critical_alerts_enabled: bool + permissions: DomainPermissions + alert_recipient_email: Optional[str] = None + dns_verified_at: Optional[str] = None + dns_records: list[DnsRecord] = Field(default_factory=list) + tracking_opt_out_enabled: Optional[bool] = None + inbound_enabled: Optional[bool] = None + inbound_verified: Optional[bool] = None + + +@dataclass +class CreateDomainParams(RequestParams): + domain_name: str + + +@dataclass +class UpdateDomainParams(RequestParams): + open_tracking_enabled: Optional[bool] = None + click_tracking_enabled: Optional[bool] = None + tracking_opt_out_enabled: Optional[bool] = None + auto_unsubscribe_link_enabled: Optional[bool] = None + inbound_enabled: Optional[bool] = None + + +@dataclass +class SendSetupInstructionsParams(RequestParams): + email: str + + +@dataclass +class SendSetupInstructionsResponse: + message: str diff --git a/mailtrap/models/inboxes.py b/mailtrap/models/inboxes.py index c62b1ea..24aabb9 100644 --- a/mailtrap/models/inboxes.py +++ b/mailtrap/models/inboxes.py @@ -1,57 +1,16 @@ -from typing import Optional - -from pydantic.dataclasses import dataclass - -from mailtrap.models.common import RequestParams -from mailtrap.models.permissions import Permissions - - -@dataclass -class Inbox: - 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 - api_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 - - -@dataclass -class CreateInboxParams(RequestParams): - name: str - - -@dataclass -class UpdateInboxParams(RequestParams): - name: Optional[str] = None - email_username: Optional[str] = None - - def __post_init__(self) -> None: - if all( - value is None - for value in [ - self.name, - self.email_username, - ] - ): - raise ValueError("At least one field must be provided for update action") +""" +Deprecated aliases of the models in `mailtrap.models.sandboxes`. + +.. deprecated:: + `Inbox`, `CreateInboxParams` and `UpdateInboxParams` will be removed in a future + major release; use `Sandbox`, `CreateSandboxParams` and `UpdateSandboxParams` + from `mailtrap.models.sandboxes` instead. +""" + +from mailtrap.models.sandboxes import CreateSandboxParams +from mailtrap.models.sandboxes import Sandbox +from mailtrap.models.sandboxes import UpdateSandboxParams + +Inbox = Sandbox +CreateInboxParams = CreateSandboxParams +UpdateInboxParams = UpdateSandboxParams diff --git a/mailtrap/models/projects.py b/mailtrap/models/projects.py index 1e7364d..68f2eb0 100644 --- a/mailtrap/models/projects.py +++ b/mailtrap/models/projects.py @@ -3,8 +3,8 @@ from pydantic.dataclasses import dataclass from mailtrap.models.common import RequestParams -from mailtrap.models.inboxes import Inbox from mailtrap.models.permissions import Permissions +from mailtrap.models.sandboxes import Sandbox @dataclass @@ -17,7 +17,7 @@ class ShareLinks: class Project: id: int name: str - inboxes: list[Inbox] + inboxes: list[Sandbox] permissions: Permissions share_links: Optional[ShareLinks] = None diff --git a/mailtrap/models/sandboxes.py b/mailtrap/models/sandboxes.py new file mode 100644 index 0000000..9f71b7a --- /dev/null +++ b/mailtrap/models/sandboxes.py @@ -0,0 +1,57 @@ +from typing import Optional + +from pydantic.dataclasses import dataclass + +from mailtrap.models.common import RequestParams +from mailtrap.models.permissions import Permissions + + +@dataclass +class Sandbox: + 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 + api_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 sandbox. + ) + last_message_sent_at: Optional[str] = None + + +@dataclass +class CreateSandboxParams(RequestParams): + name: str + + +@dataclass +class UpdateSandboxParams(RequestParams): + name: Optional[str] = None + email_username: Optional[str] = None + + def __post_init__(self) -> None: + if all( + value is None + for value in [ + self.name, + self.email_username, + ] + ): + raise ValueError("At least one field must be provided for update action") diff --git a/mailtrap/models/sending_domains.py b/mailtrap/models/sending_domains.py index d1f064f..0d3de67 100644 --- a/mailtrap/models/sending_domains.py +++ b/mailtrap/models/sending_domains.py @@ -1,69 +1,32 @@ -from typing import Optional - -from pydantic import Field -from pydantic.dataclasses import dataclass - -from mailtrap.models.common import RequestParams - - -@dataclass -class SendingDomainPermissions: - can_read: bool - can_update: bool - can_destroy: bool - - -@dataclass -class DnsRecord: - key: str - domain: str - type: str - value: str - status: str - name: str - - -@dataclass -class SendingDomain: - id: int - domain_name: str - demo: bool - compliance_status: str - dns_verified: bool - open_tracking_enabled: bool - click_tracking_enabled: bool - auto_unsubscribe_link_enabled: bool - custom_domain_tracking_enabled: bool - health_alerts_enabled: bool - critical_alerts_enabled: bool - permissions: SendingDomainPermissions - alert_recipient_email: Optional[str] = None - dns_verified_at: Optional[str] = None - dns_records: list[DnsRecord] = Field(default_factory=list) - tracking_opt_out_enabled: Optional[bool] = None - inbound_enabled: Optional[bool] = None - inbound_verified: Optional[bool] = None - - -@dataclass -class CreateSendingDomainParams(RequestParams): - domain_name: str - - -@dataclass -class UpdateSendingDomainParams(RequestParams): - open_tracking_enabled: Optional[bool] = None - click_tracking_enabled: Optional[bool] = None - tracking_opt_out_enabled: Optional[bool] = None - auto_unsubscribe_link_enabled: Optional[bool] = None - inbound_enabled: Optional[bool] = None - - -@dataclass -class SendSetupInstructionsParams(RequestParams): - email: str - - -@dataclass -class SendSetupInstructionsResponse: - message: str +""" +Deprecated aliases of the models in `mailtrap.models.domains`. + +.. deprecated:: + `SendingDomain`, `SendingDomainPermissions`, `CreateSendingDomainParams` and + `UpdateSendingDomainParams` will be removed in a future major release; use + `Domain`, `DomainPermissions`, `CreateDomainParams` and `UpdateDomainParams` + from `mailtrap.models.domains` instead. +""" + +from mailtrap.models.domains import CreateDomainParams +from mailtrap.models.domains import DnsRecord +from mailtrap.models.domains import Domain +from mailtrap.models.domains import DomainPermissions +from mailtrap.models.domains import SendSetupInstructionsParams +from mailtrap.models.domains import SendSetupInstructionsResponse +from mailtrap.models.domains import UpdateDomainParams + +SendingDomain = Domain +SendingDomainPermissions = DomainPermissions +CreateSendingDomainParams = CreateDomainParams +UpdateSendingDomainParams = UpdateDomainParams + +__all__ = [ + "CreateSendingDomainParams", + "DnsRecord", + "SendSetupInstructionsParams", + "SendSetupInstructionsResponse", + "SendingDomain", + "SendingDomainPermissions", + "UpdateSendingDomainParams", +] diff --git a/tests/unit/api/sending_domains/__init__.py b/tests/unit/api/domains/__init__.py similarity index 100% rename from tests/unit/api/sending_domains/__init__.py rename to tests/unit/api/domains/__init__.py diff --git a/tests/unit/api/sending_domains/test_sending_domains.py b/tests/unit/api/domains/test_domains.py similarity index 74% rename from tests/unit/api/sending_domains/test_sending_domains.py rename to tests/unit/api/domains/test_domains.py index c848dd9..d77a341 100644 --- a/tests/unit/api/sending_domains/test_sending_domains.py +++ b/tests/unit/api/domains/test_domains.py @@ -4,16 +4,16 @@ import pytest import responses -from mailtrap.api.resources.sending_domains import SendingDomainsApi +from mailtrap.api.resources.domains import DomainsApi from mailtrap.config import GENERAL_HOST from mailtrap.exceptions import APIError from mailtrap.http import HttpClient from mailtrap.models.common import DeletedObject -from mailtrap.models.sending_domains import CreateSendingDomainParams -from mailtrap.models.sending_domains import SendingDomain -from mailtrap.models.sending_domains import SendSetupInstructionsParams -from mailtrap.models.sending_domains import SendSetupInstructionsResponse -from mailtrap.models.sending_domains import UpdateSendingDomainParams +from mailtrap.models.domains import CreateDomainParams +from mailtrap.models.domains import Domain +from mailtrap.models.domains import SendSetupInstructionsParams +from mailtrap.models.domains import SendSetupInstructionsResponse +from mailtrap.models.domains import UpdateDomainParams from tests import conftest ACCOUNT_ID = "1234567" @@ -24,12 +24,12 @@ @pytest.fixture -def sending_domains_api() -> SendingDomainsApi: - return SendingDomainsApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST)) +def domains_api() -> DomainsApi: + return DomainsApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST)) @pytest.fixture -def sample_sending_domain_dict() -> dict[str, Any]: +def sample_domain_dict() -> dict[str, Any]: return { "id": DOMAIN_ID, "domain_name": "example.com", @@ -62,7 +62,7 @@ def sample_sending_domain_dict() -> dict[str, Any]: } -class TestSendingDomainsApi: +class TestDomainsApi: @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -80,9 +80,9 @@ class TestSendingDomainsApi: ], ) @responses.activate - def test_get_sending_domains_should_raise_api_errors( + def test_get_domains_should_raise_api_errors( self, - sending_domains_api: SendingDomainsApi, + domains_api: DomainsApi, status_code: int, response_json: dict, expected_error_message: str, @@ -94,24 +94,24 @@ def test_get_sending_domains_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - sending_domains_api.get_list() + domains_api.get_list() assert expected_error_message in str(exc_info.value) @responses.activate - def test_get_sending_domains_should_return_domains_list( - self, sending_domains_api: SendingDomainsApi, sample_sending_domain_dict: dict + def test_get_domains_should_return_domains_list( + self, domains_api: DomainsApi, sample_domain_dict: dict ) -> None: responses.get( BASE_SENDING_DOMAINS_URL, - json={"data": [sample_sending_domain_dict]}, + json={"data": [sample_domain_dict]}, status=200, ) - domains = sending_domains_api.get_list() + domains = domains_api.get_list() assert isinstance(domains, list) - assert all(isinstance(d, SendingDomain) for d in domains) + assert all(isinstance(d, Domain) for d in domains) assert domains[0].id == DOMAIN_ID assert domains[0].domain_name == "example.com" assert domains[0].dns_verified is True @@ -137,9 +137,9 @@ def test_get_sending_domains_should_return_domains_list( ], ) @responses.activate - def test_get_sending_domain_by_id_should_raise_api_errors( + def test_get_domain_by_id_should_raise_api_errors( self, - sending_domains_api: SendingDomainsApi, + domains_api: DomainsApi, status_code: int, response_json: dict, expected_error_message: str, @@ -151,23 +151,23 @@ def test_get_sending_domain_by_id_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - sending_domains_api.get_by_id(DOMAIN_ID) + domains_api.get_by_id(DOMAIN_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_get_sending_domain_by_id_should_return_sending_domain( - self, sending_domains_api: SendingDomainsApi, sample_sending_domain_dict: dict + def test_get_domain_by_id_should_return_domain( + self, domains_api: DomainsApi, sample_domain_dict: dict ) -> None: responses.get( f"{BASE_SENDING_DOMAINS_URL}/{DOMAIN_ID}", - json=sample_sending_domain_dict, + json=sample_domain_dict, status=200, ) - domain = sending_domains_api.get_by_id(DOMAIN_ID) + domain = domains_api.get_by_id(DOMAIN_ID) - assert isinstance(domain, SendingDomain) + assert isinstance(domain, Domain) assert domain.id == DOMAIN_ID assert domain.domain_name == "example.com" assert domain.compliance_status == "verified" @@ -198,9 +198,9 @@ def test_get_sending_domain_by_id_should_return_sending_domain( ], ) @responses.activate - def test_create_sending_domain_should_raise_api_errors( + def test_create_domain_should_raise_api_errors( self, - sending_domains_api: SendingDomainsApi, + domains_api: DomainsApi, status_code: int, response_json: dict, expected_error_message: str, @@ -212,25 +212,25 @@ def test_create_sending_domain_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - params = CreateSendingDomainParams(domain_name="example.com") - sending_domains_api.create(params) + params = CreateDomainParams(domain_name="example.com") + domains_api.create(params) assert expected_error_message in str(exc_info.value) @responses.activate - def test_create_sending_domain_should_return_created_domain( - self, sending_domains_api: SendingDomainsApi, sample_sending_domain_dict: dict + def test_create_domain_should_return_created_domain( + self, domains_api: DomainsApi, sample_domain_dict: dict ) -> None: responses.post( BASE_SENDING_DOMAINS_URL, - json=sample_sending_domain_dict, + json=sample_domain_dict, status=200, ) - params = CreateSendingDomainParams(domain_name="example.com") - domain = sending_domains_api.create(params) + params = CreateDomainParams(domain_name="example.com") + domain = domains_api.create(params) - assert isinstance(domain, SendingDomain) + assert isinstance(domain, Domain) assert domain.id == DOMAIN_ID assert domain.domain_name == "example.com" @@ -265,9 +265,9 @@ def test_create_sending_domain_should_return_created_domain( ], ) @responses.activate - def test_update_sending_domain_should_raise_api_errors( + def test_update_domain_should_raise_api_errors( self, - sending_domains_api: SendingDomainsApi, + domains_api: DomainsApi, status_code: int, response_json: dict, expected_error_message: str, @@ -279,33 +279,33 @@ def test_update_sending_domain_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - params = UpdateSendingDomainParams(open_tracking_enabled=True) - sending_domains_api.update(DOMAIN_ID, params) + params = UpdateDomainParams(open_tracking_enabled=True) + domains_api.update(DOMAIN_ID, params) assert expected_error_message in str(exc_info.value) @responses.activate - def test_update_sending_domain_should_return_updated_domain( - self, sending_domains_api: SendingDomainsApi, sample_sending_domain_dict: dict + def test_update_domain_should_return_updated_domain( + self, domains_api: DomainsApi, sample_domain_dict: dict ) -> None: - sample_sending_domain_dict["tracking_opt_out_enabled"] = True - sample_sending_domain_dict["auto_unsubscribe_link_enabled"] = False + sample_domain_dict["tracking_opt_out_enabled"] = True + sample_domain_dict["auto_unsubscribe_link_enabled"] = False responses.patch( f"{BASE_SENDING_DOMAINS_URL}/{DOMAIN_ID}", - json=sample_sending_domain_dict, + json=sample_domain_dict, status=200, ) - params = UpdateSendingDomainParams( + params = UpdateDomainParams( open_tracking_enabled=True, click_tracking_enabled=True, tracking_opt_out_enabled=True, auto_unsubscribe_link_enabled=False, inbound_enabled=False, ) - domain = sending_domains_api.update(DOMAIN_ID, params) + domain = domains_api.update(DOMAIN_ID, params) - assert isinstance(domain, SendingDomain) + assert isinstance(domain, Domain) assert domain.tracking_opt_out_enabled is True assert domain.auto_unsubscribe_link_enabled is False assert responses.calls[0].request.body is not None @@ -320,17 +320,17 @@ def test_update_sending_domain_should_return_updated_domain( } @responses.activate - def test_update_sending_domain_should_send_only_supplied_fields( - self, sending_domains_api: SendingDomainsApi, sample_sending_domain_dict: dict + def test_update_domain_should_send_only_supplied_fields( + self, domains_api: DomainsApi, sample_domain_dict: dict ) -> None: responses.patch( f"{BASE_SENDING_DOMAINS_URL}/{DOMAIN_ID}", - json=sample_sending_domain_dict, + json=sample_domain_dict, status=200, ) - params = UpdateSendingDomainParams(tracking_opt_out_enabled=True) - sending_domains_api.update(DOMAIN_ID, params) + params = UpdateDomainParams(tracking_opt_out_enabled=True) + domains_api.update(DOMAIN_ID, params) assert responses.calls[0].request.body is not None assert json.loads(responses.calls[0].request.body) == { @@ -363,9 +363,9 @@ def test_update_sending_domain_should_send_only_supplied_fields( ], ) @responses.activate - def test_delete_sending_domain_should_raise_api_errors( + def test_delete_domain_should_raise_api_errors( self, - sending_domains_api: SendingDomainsApi, + domains_api: DomainsApi, status_code: int, response_json: dict, expected_error_message: str, @@ -377,20 +377,20 @@ def test_delete_sending_domain_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - sending_domains_api.delete(DOMAIN_ID) + domains_api.delete(DOMAIN_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_delete_sending_domain_should_return_deleted_object( - self, sending_domains_api: SendingDomainsApi + def test_delete_domain_should_return_deleted_object( + self, domains_api: DomainsApi ) -> None: responses.delete( f"{BASE_SENDING_DOMAINS_URL}/{DOMAIN_ID}", status=204, ) - deleted_domain = sending_domains_api.delete(DOMAIN_ID) + deleted_domain = domains_api.delete(DOMAIN_ID) assert isinstance(deleted_domain, DeletedObject) assert deleted_domain.id == DOMAIN_ID @@ -418,7 +418,7 @@ def test_delete_sending_domain_should_return_deleted_object( @responses.activate def test_send_setup_instructions_should_raise_api_errors( self, - sending_domains_api: SendingDomainsApi, + domains_api: DomainsApi, status_code: int, response_json: dict, expected_error_message: str, @@ -431,13 +431,13 @@ def test_send_setup_instructions_should_raise_api_errors( with pytest.raises(APIError) as exc_info: params = SendSetupInstructionsParams(email="admin@example.com") - sending_domains_api.send_setup_instructions(DOMAIN_ID, params) + domains_api.send_setup_instructions(DOMAIN_ID, params) assert expected_error_message in str(exc_info.value) @responses.activate def test_send_setup_instructions_should_return_success_response( - self, sending_domains_api: SendingDomainsApi + self, domains_api: DomainsApi ) -> None: responses.post( f"{BASE_SENDING_DOMAINS_URL}/{DOMAIN_ID}/send_setup_instructions", @@ -445,7 +445,7 @@ def test_send_setup_instructions_should_return_success_response( ) params = SendSetupInstructionsParams(email="admin@example.com") - response = sending_domains_api.send_setup_instructions(DOMAIN_ID, params) + response = domains_api.send_setup_instructions(DOMAIN_ID, params) assert isinstance(response, SendSetupInstructionsResponse) assert response.message == "Instructions email has been sent successfully" diff --git a/tests/unit/api/testing/__init__.py b/tests/unit/api/sandbox/__init__.py similarity index 100% rename from tests/unit/api/testing/__init__.py rename to tests/unit/api/sandbox/__init__.py diff --git a/tests/unit/api/testing/test_attachments.py b/tests/unit/api/sandbox/test_attachments.py similarity index 100% rename from tests/unit/api/testing/test_attachments.py rename to tests/unit/api/sandbox/test_attachments.py diff --git a/tests/unit/api/testing/test_messages.py b/tests/unit/api/sandbox/test_messages.py similarity index 100% rename from tests/unit/api/testing/test_messages.py rename to tests/unit/api/sandbox/test_messages.py diff --git a/tests/unit/api/testing/test_projects.py b/tests/unit/api/sandbox/test_projects.py similarity index 100% rename from tests/unit/api/testing/test_projects.py rename to tests/unit/api/sandbox/test_projects.py diff --git a/tests/unit/api/testing/test_inboxes.py b/tests/unit/api/sandbox/test_sandboxes.py similarity index 70% rename from tests/unit/api/testing/test_inboxes.py rename to tests/unit/api/sandbox/test_sandboxes.py index 9f8b56b..1e05e1c 100644 --- a/tests/unit/api/testing/test_inboxes.py +++ b/tests/unit/api/sandbox/test_sandboxes.py @@ -3,17 +3,17 @@ import pytest import responses -from mailtrap.api.resources.inboxes import InboxesApi +from mailtrap.api.resources.sandboxes import SandboxesApi from mailtrap.config import GENERAL_HOST from mailtrap.exceptions import APIError from mailtrap.http import HttpClient -from mailtrap.models.inboxes import CreateInboxParams -from mailtrap.models.inboxes import Inbox -from mailtrap.models.inboxes import UpdateInboxParams +from mailtrap.models.sandboxes import CreateSandboxParams +from mailtrap.models.sandboxes import Sandbox +from mailtrap.models.sandboxes import UpdateSandboxParams from tests import conftest ACCOUNT_ID = "321" -INBOX_ID = 3538 +SANDBOX_ID = 3538 PROJECT_ID = 2293 BASE_INBOXES_URL = f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/inboxes" BASE_PROJECT_INBOXES_URL = ( @@ -22,15 +22,15 @@ @pytest.fixture -def client() -> InboxesApi: - return InboxesApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST)) +def client() -> SandboxesApi: + return SandboxesApi(account_id=ACCOUNT_ID, client=HttpClient(GENERAL_HOST)) @pytest.fixture -def sample_inbox_dict() -> dict[str, Any]: +def sample_sandbox_dict() -> dict[str, Any]: return { - "id": INBOX_ID, - "name": "Admin Inbox", + "id": SANDBOX_ID, + "name": "Admin Sandbox", "username": "b3a87978452ae1", "password": "6be9fcfc613a7c", "max_size": 0, @@ -61,7 +61,7 @@ def sample_inbox_dict() -> dict[str, Any]: } -class TestInboxesApi: +class TestSandboxesApi: @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -81,7 +81,7 @@ class TestInboxesApi: @responses.activate def test_get_list_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, @@ -98,20 +98,20 @@ def test_get_list_should_raise_api_errors( assert expected_error_message in str(exc_info.value) @responses.activate - def test_get_list_should_return_inbox_list( - self, client: InboxesApi, sample_inbox_dict: dict + def test_get_list_should_return_sandbox_list( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: responses.get( BASE_INBOXES_URL, - json=[sample_inbox_dict], + json=[sample_sandbox_dict], status=200, ) - inboxes = client.get_list() + sandboxes = client.get_list() - assert isinstance(inboxes, list) - assert all(isinstance(i, Inbox) for i in inboxes) - assert inboxes[0].id == INBOX_ID + assert isinstance(sandboxes, list) + assert all(isinstance(i, Sandbox) for i in sandboxes) + assert sandboxes[0].id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -136,12 +136,12 @@ def test_get_list_should_return_inbox_list( @responses.activate def test_get_by_id_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}" responses.get( url, status=status_code, @@ -149,25 +149,25 @@ def test_get_by_id_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.get_by_id(INBOX_ID) + client.get_by_id(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_get_by_id_should_return_single_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_get_by_id_should_return_single_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}" responses.get( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - inbox = client.get_by_id(INBOX_ID) + sandbox = client.get_by_id(SANDBOX_ID) - assert isinstance(inbox, Inbox) - assert inbox.id == INBOX_ID + assert isinstance(sandbox, Sandbox) + assert sandbox.id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -192,7 +192,7 @@ def test_get_by_id_should_return_single_inbox( @responses.activate def test_create_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, @@ -205,27 +205,28 @@ def test_create_should_raise_api_errors( with pytest.raises(APIError) as exc_info: client.create( - project_id=PROJECT_ID, inbox_params=CreateInboxParams(name="New Inbox") + project_id=PROJECT_ID, + sandbox_params=CreateSandboxParams(name="New Sandbox"), ) assert expected_error_message in str(exc_info.value) @responses.activate - def test_create_should_return_new_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_create_should_return_new_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: responses.post( BASE_PROJECT_INBOXES_URL, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=201, ) - inbox = client.create( - project_id=PROJECT_ID, inbox_params=CreateInboxParams(name="New Inbox") + sandbox = client.create( + project_id=PROJECT_ID, sandbox_params=CreateSandboxParams(name="New Sandbox") ) - assert isinstance(inbox, Inbox) - assert inbox.name == "Admin Inbox" + assert isinstance(sandbox, Sandbox) + assert sandbox.name == "Admin Sandbox" @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -250,12 +251,12 @@ def test_create_should_return_new_inbox( @responses.activate def test_update_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}" responses.patch( url, status=status_code, @@ -264,30 +265,33 @@ def test_update_should_raise_api_errors( with pytest.raises(APIError) as exc_info: _ = client.update( - INBOX_ID, inbox_params=UpdateInboxParams(name="Updated Inbox Name") + SANDBOX_ID, + sandbox_params=UpdateSandboxParams(name="Updated Sandbox Name"), ) assert expected_error_message in str(exc_info.value) @responses.activate - def test_update_should_return_updated_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_update_should_return_updated_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}" - updated_name = "Updated Inbox" - updated_inbox_dict = sample_inbox_dict.copy() - updated_inbox_dict["name"] = updated_name + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}" + updated_name = "Updated Sandbox" + updated_sandbox_dict = sample_sandbox_dict.copy() + updated_sandbox_dict["name"] = updated_name responses.patch( url, - json=updated_inbox_dict, + json=updated_sandbox_dict, status=200, ) - inbox = client.update(INBOX_ID, inbox_params=UpdateInboxParams(name=updated_name)) + sandbox = client.update( + SANDBOX_ID, sandbox_params=UpdateSandboxParams(name=updated_name) + ) - assert isinstance(inbox, Inbox) - assert inbox.name == updated_name + assert isinstance(sandbox, Sandbox) + assert sandbox.name == updated_name @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -312,12 +316,12 @@ def test_update_should_return_updated_inbox( @responses.activate def test_delete_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}" responses.delete( url, status=status_code, @@ -325,25 +329,25 @@ def test_delete_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.delete(INBOX_ID) + client.delete(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_delete_should_return_deleted_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_delete_should_return_deleted_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}" responses.delete( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - result = client.delete(INBOX_ID) + result = client.delete(SANDBOX_ID) - assert isinstance(result, Inbox) - assert result.id == INBOX_ID + assert isinstance(result, Sandbox) + assert result.id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -368,12 +372,12 @@ def test_delete_should_return_deleted_inbox( @responses.activate def test_clean_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/clean" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/clean" responses.patch( url, status=status_code, @@ -381,25 +385,25 @@ def test_clean_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.clean(INBOX_ID) + client.clean(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_clean_should_return_cleaned_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_clean_should_return_cleaned_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/clean" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/clean" responses.patch( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - result = client.clean(INBOX_ID) + result = client.clean(SANDBOX_ID) - assert isinstance(result, Inbox) - assert result.id == INBOX_ID + assert isinstance(result, Sandbox) + assert result.id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -424,12 +428,12 @@ def test_clean_should_return_cleaned_inbox( @responses.activate def test_mark_as_read_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/all_read" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/all_read" responses.patch( url, status=status_code, @@ -437,25 +441,25 @@ def test_mark_as_read_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.mark_as_read(INBOX_ID) + client.mark_as_read(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_mark_as_read_should_return_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_mark_as_read_should_return_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/all_read" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/all_read" responses.patch( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - result = client.mark_as_read(INBOX_ID) + result = client.mark_as_read(SANDBOX_ID) - assert isinstance(result, Inbox) - assert result.id == INBOX_ID + assert isinstance(result, Sandbox) + assert result.id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -480,12 +484,12 @@ def test_mark_as_read_should_return_inbox( @responses.activate def test_reset_credentials_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_credentials" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/reset_credentials" responses.patch( url, status=status_code, @@ -493,25 +497,25 @@ def test_reset_credentials_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.reset_credentials(INBOX_ID) + client.reset_credentials(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_reset_credentials_should_return_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_reset_credentials_should_return_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_credentials" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/reset_credentials" responses.patch( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - result = client.reset_credentials(INBOX_ID) + result = client.reset_credentials(SANDBOX_ID) - assert isinstance(result, Inbox) - assert result.id == INBOX_ID + assert isinstance(result, Sandbox) + assert result.id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -536,12 +540,12 @@ def test_reset_credentials_should_return_inbox( @responses.activate def test_enable_email_address_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/toggle_email_username" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/toggle_email_username" responses.patch( url, status=status_code, @@ -549,25 +553,25 @@ def test_enable_email_address_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.enable_email_address(INBOX_ID) + client.enable_email_address(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_enable_email_address_should_return_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_enable_email_address_should_return_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/toggle_email_username" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/toggle_email_username" responses.patch( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - result = client.enable_email_address(INBOX_ID) + result = client.enable_email_address(SANDBOX_ID) - assert isinstance(result, Inbox) - assert result.id == INBOX_ID + assert isinstance(result, Sandbox) + assert result.id == SANDBOX_ID @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -592,12 +596,12 @@ def test_enable_email_address_should_return_inbox( @responses.activate def test_reset_email_username_should_raise_api_errors( self, - client: InboxesApi, + client: SandboxesApi, status_code: int, response_json: dict, expected_error_message: str, ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_email_username" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/reset_email_username" responses.patch( url, status=status_code, @@ -605,22 +609,22 @@ def test_reset_email_username_should_raise_api_errors( ) with pytest.raises(APIError) as exc_info: - client.reset_email_username(INBOX_ID) + client.reset_email_username(SANDBOX_ID) assert expected_error_message in str(exc_info.value) @responses.activate - def test_reset_email_username_should_return_inbox( - self, client: InboxesApi, sample_inbox_dict: dict + def test_reset_email_username_should_return_sandbox( + self, client: SandboxesApi, sample_sandbox_dict: dict ) -> None: - url = f"{BASE_INBOXES_URL}/{INBOX_ID}/reset_email_username" + url = f"{BASE_INBOXES_URL}/{SANDBOX_ID}/reset_email_username" responses.patch( url, - json=sample_inbox_dict, + json=sample_sandbox_dict, status=200, ) - result = client.reset_email_username(INBOX_ID) + result = client.reset_email_username(SANDBOX_ID) - assert isinstance(result, Inbox) - assert result.id == INBOX_ID + assert isinstance(result, Sandbox) + assert result.id == SANDBOX_ID diff --git a/tests/unit/api/test_sending.py b/tests/unit/api/test_sending.py index 3de20c4..f5187bc 100644 --- a/tests/unit/api/test_sending.py +++ b/tests/unit/api/test_sending.py @@ -15,7 +15,7 @@ ACCOUNT_ID = "321" PROJECT_ID = 123 -INBOX_ID = "456" +SANDBOX_ID = "456" DUMMY_ADDRESS = mt.Address(email="joe@mail.com") DUMMY_MAIL = mt.Mail( @@ -189,3 +189,29 @@ def test_batch_send_should_handle_partial_failure_response(self) -> None: assert result.responses[0].message_ids == ["12345"] assert result.responses[1].success is False assert result.responses[1].errors == ["Invalid email address"] + + @responses.activate + def test_send_should_post_to_sandbox_url_when_sandbox_id_is_set(self) -> None: + responses.post( + f"{SEND_FULL_URL}/{SANDBOX_ID}", + json={"success": True, "message_ids": ["12345"]}, + ) + api = SendingApi(client=HttpClient(SENDING_HOST), sandbox_id=SANDBOX_ID) + + result = api.send(DUMMY_MAIL) + + assert result.success is True + + @responses.activate + def test_batch_send_should_post_to_sandbox_url_when_sandbox_id_is_set( + self, + ) -> None: + responses.post( + f"{BATCH_SEND_FULL_URL}/{SANDBOX_ID}", + json={"success": True, "responses": []}, + ) + api = SendingApi(client=HttpClient(SENDING_HOST), sandbox_id=SANDBOX_ID) + + result = api.batch_send(DUMMY_BATCH_PARAMS) + + assert result.success is True diff --git a/tests/unit/models/test_sending_domains.py b/tests/unit/models/test_domains.py similarity index 73% rename from tests/unit/models/test_sending_domains.py rename to tests/unit/models/test_domains.py index 6e699c1..d5ece6d 100644 --- a/tests/unit/models/test_sending_domains.py +++ b/tests/unit/models/test_domains.py @@ -1,9 +1,9 @@ -from mailtrap.models.sending_domains import CreateSendingDomainParams -from mailtrap.models.sending_domains import SendingDomain -from mailtrap.models.sending_domains import SendSetupInstructionsParams +from mailtrap.models.domains import CreateDomainParams +from mailtrap.models.domains import Domain +from mailtrap.models.domains import SendSetupInstructionsParams -def _sending_domain_dict() -> dict: +def _domain_dict() -> dict: return { "id": 1, "domain_name": "example.com", @@ -24,26 +24,26 @@ def _sending_domain_dict() -> dict: } -class TestSendingDomain: +class TestDomain: def test_parses_inbound_flags_when_present(self) -> None: data = { - **_sending_domain_dict(), + **_domain_dict(), "inbound_enabled": True, "inbound_verified": False, } - domain = SendingDomain(**data) + domain = Domain(**data) assert domain.inbound_enabled is True assert domain.inbound_verified is False def test_inbound_flags_default_when_absent(self) -> None: - domain = SendingDomain(**_sending_domain_dict()) + domain = Domain(**_domain_dict()) assert domain.inbound_enabled is None assert domain.inbound_verified is None -class TestCreateSendingDomainParams: +class TestCreateDomainParams: def test_api_data_should_return_dict_with_all_props(self) -> None: - entity = CreateSendingDomainParams(domain_name="test.co") + entity = CreateDomainParams(domain_name="test.co") assert entity.api_data == {"domain_name": "test.co"} diff --git a/tests/unit/models/test_inboxes.py b/tests/unit/models/test_sandboxes.py similarity index 59% rename from tests/unit/models/test_inboxes.py rename to tests/unit/models/test_sandboxes.py index 483c30b..424e2a3 100644 --- a/tests/unit/models/test_inboxes.py +++ b/tests/unit/models/test_sandboxes.py @@ -1,29 +1,29 @@ import pytest -from mailtrap.models.inboxes import CreateInboxParams -from mailtrap.models.inboxes import UpdateInboxParams +from mailtrap.models.sandboxes import CreateSandboxParams +from mailtrap.models.sandboxes import UpdateSandboxParams -class TestCreateInboxParams: +class TestCreateSandboxParams: def test_api_data_should_return_dict_with_all_props(self) -> None: - entity = CreateInboxParams(name="test") + entity = CreateSandboxParams(name="test") assert entity.api_data == {"name": "test"} -class TestUpdateInboxParams: +class TestUpdateSandboxParams: def test_raise_error_when_all_fields_are_missing(self) -> None: with pytest.raises( ValueError, match="At least one field must be provided for update action" ): - _ = UpdateInboxParams() + _ = UpdateSandboxParams() def test_api_data_should_return_dict_with_all_props(self) -> None: - entity = UpdateInboxParams(name="test", email_username="test_username") + entity = UpdateSandboxParams(name="test", email_username="test_username") assert entity.api_data == {"name": "test", "email_username": "test_username"} def test_api_data_should_return_dict_with_required_props_only(self) -> None: - entity = UpdateInboxParams(name="test") + entity = UpdateSandboxParams(name="test") assert entity.api_data == {"name": "test"} - entity = UpdateInboxParams(email_username="test_username") + entity = UpdateSandboxParams(email_username="test_username") assert entity.api_data == {"email_username": "test_username"} diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index a0e1bc7..24bcc56 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -29,23 +29,35 @@ def get_client(**kwargs: Any) -> mt.MailtrapClient: return mt.MailtrapClient(**props) @pytest.mark.parametrize( - "arguments", + "arguments, expected_message", [ - {"sandbox": True}, - {"inbox_id": "12345"}, - {"bulk": True, "sandbox": True, "inbox_id": "12345"}, + ({"sandbox": True}, "`sandbox_id` is required for sandbox mode"), + ({"sandbox_id": "12345"}, "`sandbox_id` is not allowed in non-sandbox mode"), + ( + {"bulk": True, "sandbox": True, "sandbox_id": "12345"}, + "bulk mode is not allowed in sandbox mode", + ), ], ) - def test_client_validation(self, arguments: dict[str, Any]) -> None: - with pytest.raises(mt.ClientConfigurationError): + def test_client_validation( + self, arguments: dict[str, Any], expected_message: str + ) -> None: + with pytest.raises(mt.ClientConfigurationError, match=expected_message): self.get_client(**arguments) - def test_get_testing_api_validation(self) -> None: + def test_sandbox_api_requires_account_id(self) -> None: + client = self.get_client() + with pytest.raises(mt.ClientConfigurationError) as exc_info: + _ = client.sandbox_api + + assert "`account_id` is required for Sandbox API" in str(exc_info.value) + + def test_domains_api_requires_account_id(self) -> None: client = self.get_client() with pytest.raises(mt.ClientConfigurationError) as exc_info: - _ = client.testing_api + _ = client.domains_api - assert "`account_id` is required for Testing API" in str(exc_info.value) + assert "`account_id` is required for Domains API" in str(exc_info.value) def test_organizations_api_requires_organization_id(self) -> None: client = self.get_client() @@ -89,7 +101,7 @@ def test_marketing_stats_api_does_not_require_account_id(self) -> None: "https://example.send.com:543/api/send", ), ( - {"api_host": "example.send.com", "sandbox": True, "inbox_id": "12345"}, + {"api_host": "example.send.com", "sandbox": True, "sandbox_id": "12345"}, "https://example.send.com:443/api/send/12345", ), ( @@ -97,7 +109,7 @@ def test_marketing_stats_api_does_not_require_account_id(self) -> None: "https://example.send.com:443/api/send", ), ( - {"sandbox": True, "inbox_id": "12345"}, + {"sandbox": True, "sandbox_id": "12345"}, "https://sandbox.api.mailtrap.io:443/api/send/12345", ), ( diff --git a/tests/unit/test_deprecations.py b/tests/unit/test_deprecations.py new file mode 100644 index 0000000..079fc7d --- /dev/null +++ b/tests/unit/test_deprecations.py @@ -0,0 +1,228 @@ +""" +The Testing, Inbox and SendingDomain names are deprecated aliases of the Sandbox +and Domain names. Each test checks that an old name still works and warns. +""" + +import pytest +import responses + +import mailtrap as mt +from mailtrap._deprecation import renamed_kwargs +from mailtrap.api import testing # module import: pytest would collect TestingApi +from mailtrap.api.domains import DomainsBaseApi +from mailtrap.api.resources.company_info import CompanyInfoApi +from mailtrap.api.resources.domains import DomainsApi +from mailtrap.api.resources.inboxes import InboxesApi +from mailtrap.api.resources.messages import MessagesApi +from mailtrap.api.resources.sandboxes import SandboxesApi +from mailtrap.api.resources.sending_domains import SendingDomainsApi +from mailtrap.api.sandbox import SandboxApi +from mailtrap.api.sending import SendingApi +from mailtrap.api.sending_domains import SendingDomainsBaseApi +from mailtrap.config import GENERAL_HOST +from mailtrap.config import SENDING_HOST +from mailtrap.http import HttpClient +from mailtrap.models import domains as domain_models +from mailtrap.models import inboxes as inbox_models +from mailtrap.models import sandboxes as sandbox_models +from mailtrap.models import sending_domains as sending_domain_models + +ACCOUNT_ID = "321" +SANDBOX_ID = 3538 +DOMAIN_ID = 432 +MESSAGES_URL = ( + f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/inboxes/{SANDBOX_ID}/messages" +) +COMPANY_INFO_URL = f"https://{GENERAL_HOST}/api/domains/{DOMAIN_ID}/company_info" + + +@pytest.fixture +def client_with_account() -> mt.MailtrapClient: + return mt.MailtrapClient(token="fake_token", account_id=ACCOUNT_ID) + + +def test_testing_api_is_deprecated_alias_of_sandbox_api( + client_with_account: mt.MailtrapClient, +) -> None: + with pytest.warns( + DeprecationWarning, match=r"testing_api is deprecated.*sandbox_api" + ) as record: + api = client_with_account.testing_api + + assert isinstance(api, SandboxApi) + assert record[0].filename == __file__ # points at the caller's line + + +def test_testing_api_inboxes_is_deprecated_alias_of_sandboxes() -> None: + testing_api = testing.TestingApi( + client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID + ) + + with pytest.warns(DeprecationWarning, match=r"inboxes is deprecated.*sandboxes"): + api = testing_api.inboxes + + assert isinstance(api, SandboxesApi) + + +def test_inboxes_api_is_deprecated_subclass_of_sandboxes_api() -> None: + with pytest.warns( + DeprecationWarning, match=r"InboxesApi is deprecated.*SandboxesApi" + ): + api = InboxesApi(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) + + assert isinstance(api, SandboxesApi) + + +def test_sending_domains_api_is_deprecated_alias_of_domains_api( + client_with_account: mt.MailtrapClient, +) -> None: + with pytest.warns( + DeprecationWarning, match=r"sending_domains_api is deprecated.*domains_api" + ): + api = client_with_account.sending_domains_api + + assert isinstance(api, DomainsBaseApi) + + +def test_sending_domains_is_deprecated_alias_of_domains() -> None: + sending_domains_api = SendingDomainsBaseApi( + client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID + ) + + with pytest.warns( + DeprecationWarning, match=r"sending_domains is deprecated.*domains" + ): + api = sending_domains_api.sending_domains + + assert isinstance(api, DomainsApi) + + +def test_sending_domains_api_class_is_deprecated_subclass_of_domains_api() -> None: + with pytest.warns( + DeprecationWarning, match=r"SendingDomainsApi is deprecated.*DomainsApi" + ): + api = SendingDomainsApi(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) + + assert isinstance(api, DomainsApi) + + +def test_client_inbox_id_argument_is_deprecated_alias_of_sandbox_id() -> None: + with pytest.warns(DeprecationWarning, match=r"inbox_id=.*is deprecated.*sandbox_id"): + client = mt.MailtrapClient(token="fake_token", sandbox=True, inbox_id="12345") + + assert client.sandbox_id == "12345" + + +def test_client_inbox_id_argument_still_validates_sandbox_mode() -> None: + with pytest.warns(DeprecationWarning, match="inbox_id"): + with pytest.raises(mt.ClientConfigurationError, match="sandbox_id"): + mt.MailtrapClient(token="fake_token", inbox_id="12345") + + +def test_client_accepts_same_inbox_id_and_sandbox_id() -> None: + with pytest.warns(DeprecationWarning, match="inbox_id"): + client = mt.MailtrapClient( + token="fake_token", sandbox=True, inbox_id="12345", sandbox_id="12345" + ) + + assert client.sandbox_id == "12345" + + +def test_client_rejects_different_inbox_id_and_sandbox_id() -> None: + with pytest.warns(DeprecationWarning, match="inbox_id"): + with pytest.raises(mt.ClientConfigurationError, match="pass only `sandbox_id`"): + mt.MailtrapClient( + token="fake_token", sandbox=True, inbox_id="1", sandbox_id="2" + ) + + +def test_client_inbox_id_property_is_deprecated_alias_of_sandbox_id() -> None: + client = mt.MailtrapClient(token="fake_token", sandbox=True, sandbox_id="12345") + + with pytest.warns(DeprecationWarning, match=r"inbox_id is deprecated.*sandbox_id"): + assert client.inbox_id == "12345" + + with pytest.warns(DeprecationWarning, match=r"inbox_id is deprecated.*sandbox_id"): + client.inbox_id = "67890" + + assert client.sandbox_id == "67890" + + +@responses.activate +def test_sending_api_inbox_id_argument_is_deprecated_alias_of_sandbox_id() -> None: + responses.post( + f"https://{SENDING_HOST}/api/send/12345", + json={"success": True, "message_ids": ["1"]}, + ) + mail = mt.Mail( + sender=mt.Address(email="joe@mail.com"), + to=[mt.Address(email="joe@mail.com")], + subject="Email subject", + text="email text", + ) + + with pytest.warns(DeprecationWarning, match=r"inbox_id=.*is deprecated.*sandbox_id"): + api = SendingApi(client=HttpClient(SENDING_HOST), inbox_id="12345") + + assert api.send(mail).success is True + + +@responses.activate +def test_messages_inbox_id_keyword_is_deprecated_alias_of_sandbox_id() -> None: + responses.get(MESSAGES_URL, json=[]) + api = MessagesApi(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) + + with pytest.warns( + DeprecationWarning, match=r"get_list\(inbox_id=.*is deprecated.*sandbox_id" + ): + messages = api.get_list(inbox_id=SANDBOX_ID) # type: ignore[call-arg] + + assert messages == [] + assert len(responses.calls) == 1 + + +def test_renamed_keyword_rejects_old_and_new_together() -> None: + api = MessagesApi(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) + + with pytest.raises(TypeError, match="got both 'inbox_id' and 'sandbox_id'"): + api.get_list(inbox_id=1, sandbox_id=1) # type: ignore[call-arg] + + +def test_renamed_kwargs_maps_each_old_keyword() -> None: + @renamed_kwargs(inbox_id="sandbox_id", inbox_params="sandbox_params") + def update(sandbox_id: int, sandbox_params: str) -> tuple[int, str]: + return sandbox_id, sandbox_params + + with pytest.warns(DeprecationWarning) as record: + result = update(inbox_id=1, inbox_params="params") # type: ignore[call-arg] + + assert result == (1, "params") + assert len(record) == 2 + + +@responses.activate +def test_company_info_sending_domain_id_keyword_is_deprecated() -> None: + responses.get(COMPANY_INFO_URL, json={"data": {}}) + api = CompanyInfoApi(client=HttpClient(GENERAL_HOST)) + + with pytest.warns( + DeprecationWarning, match=r"get\(sending_domain_id=.*is deprecated.*domain_id" + ): + api.get(sending_domain_id=DOMAIN_ID) # type: ignore[call-arg] + + assert len(responses.calls) == 1 + + +def test_inbox_models_are_aliases_of_sandbox_models() -> None: + assert inbox_models.Inbox is sandbox_models.Sandbox + assert mt.CreateInboxParams is mt.CreateSandboxParams + assert mt.UpdateInboxParams is mt.UpdateSandboxParams + + +def test_sending_domain_models_are_aliases_of_domain_models() -> None: + assert sending_domain_models.SendingDomain is domain_models.Domain + assert ( + sending_domain_models.SendingDomainPermissions is domain_models.DomainPermissions + ) + assert mt.CreateSendingDomainParams is mt.CreateDomainParams + assert mt.UpdateSendingDomainParams is mt.UpdateDomainParams From 08797e63a571d875955d79a7d7d369f72fc64345 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 3 Nov 2026 11:04:24 +0100 Subject: [PATCH 2/4] Tighten renamed-ID handling, tests and examples after review Decisions: - treat inbox_id and sandbox_id given as the same int and string as one value, and keep the new name's value - build the sandbox path for ID 0 like the domains path does - assert request bodies so the tests check the wire keys the rename must keep - README no longer says the plain model aliases warn - examples use a real project ID and the headers' dict type Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- examples/sandbox/messages.py | 3 ++- examples/sandbox/sandboxes.py | 5 ++++- mailtrap/_deprecation.py | 6 ++++-- mailtrap/api/resources/sandboxes.py | 2 +- tests/unit/api/domains/test_domains.py | 4 ++++ tests/unit/api/sandbox/test_sandboxes.py | 9 +++++++++ tests/unit/test_deprecations.py | 12 ++++++++++++ 8 files changed, 38 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f315ec2..8a34a52 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,8 @@ The same situation applies to both `client.batch_send()` and `client.sending_api ## Renamed APIs Email Sandbox inboxes are now called sandboxes, and sending domains are now called domains. -The old names still work, but they raise a `DeprecationWarning` and will be removed in a future major release. +The old names still work and will be removed in a future major release. +The old client properties, methods, constructor arguments and keyword arguments raise a `DeprecationWarning`; the old model and params names are plain aliases and don't warn. API requests are the same with either name. | Old name | New name | diff --git a/examples/sandbox/messages.py b/examples/sandbox/messages.py index 0092ef3..3068272 100644 --- a/examples/sandbox/messages.py +++ b/examples/sandbox/messages.py @@ -1,4 +1,5 @@ import os +from typing import Any from typing import Optional import mailtrap as mt @@ -74,7 +75,7 @@ def get_message_as_eml(sandbox_id: int, message_id: str) -> str: return messages_api.get_message_as_eml(sandbox_id=sandbox_id, message_id=message_id) -def get_mail_headers(sandbox_id: int, message_id: str) -> str: +def get_mail_headers(sandbox_id: int, message_id: str) -> dict[str, Any]: return messages_api.get_mail_headers(sandbox_id=sandbox_id, message_id=message_id) diff --git a/examples/sandbox/sandboxes.py b/examples/sandbox/sandboxes.py index 2ec7a25..a6abc48 100644 --- a/examples/sandbox/sandboxes.py +++ b/examples/sandbox/sandboxes.py @@ -64,7 +64,10 @@ def delete_sandbox(sandbox_id: int): sandboxes = list_sandboxes() print(sandboxes) - created = create_sandbox(project_id=1, sandbox_name="example-created-sandbox") + project_id = client.sandbox_api.projects.get_list()[0].id + created = create_sandbox( + project_id=project_id, sandbox_name="example-created-sandbox" + ) print(created) sandbox_id = created.id diff --git a/mailtrap/_deprecation.py b/mailtrap/_deprecation.py index e89007c..789180c 100644 --- a/mailtrap/_deprecation.py +++ b/mailtrap/_deprecation.py @@ -70,8 +70,10 @@ def resolve_renamed_arg( if old_value is None: return new_value warn_deprecated(f"{owner}({old}=...)", f"{owner}({new}=...)", stacklevel=4) - if new_value is not None and new_value != old_value: + if new_value is None: + return old_value + if str(new_value) != str(old_value): raise ClientConfigurationError( f"`{old}` and `{new}` have different values; pass only `{new}`" ) - return old_value + return new_value diff --git a/mailtrap/api/resources/sandboxes.py b/mailtrap/api/resources/sandboxes.py index 5dc1aa8..e242540 100644 --- a/mailtrap/api/resources/sandboxes.py +++ b/mailtrap/api/resources/sandboxes.py @@ -83,6 +83,6 @@ def reset_email_username(self, sandbox_id: int) -> Sandbox: def _api_path(self, sandbox_id: Optional[int] = None) -> str: path = f"/api/accounts/{self._account_id}/inboxes" - if sandbox_id: + if sandbox_id is not None: return f"{path}/{sandbox_id}" return path diff --git a/tests/unit/api/domains/test_domains.py b/tests/unit/api/domains/test_domains.py index d77a341..e81339a 100644 --- a/tests/unit/api/domains/test_domains.py +++ b/tests/unit/api/domains/test_domains.py @@ -233,6 +233,10 @@ def test_create_domain_should_return_created_domain( assert isinstance(domain, Domain) assert domain.id == DOMAIN_ID assert domain.domain_name == "example.com" + assert responses.calls[0].request.body is not None + assert json.loads(responses.calls[0].request.body) == { + "sending_domain": {"domain_name": "example.com"} + } @pytest.mark.parametrize( "status_code,response_json,expected_error_message", diff --git a/tests/unit/api/sandbox/test_sandboxes.py b/tests/unit/api/sandbox/test_sandboxes.py index 1e05e1c..f693078 100644 --- a/tests/unit/api/sandbox/test_sandboxes.py +++ b/tests/unit/api/sandbox/test_sandboxes.py @@ -1,3 +1,4 @@ +import json from typing import Any import pytest @@ -227,6 +228,10 @@ def test_create_should_return_new_sandbox( assert isinstance(sandbox, Sandbox) assert sandbox.name == "Admin Sandbox" + assert responses.calls[0].request.body is not None + assert json.loads(responses.calls[0].request.body) == { + "inbox": {"name": "New Sandbox"} + } @pytest.mark.parametrize( "status_code,response_json,expected_error_message", @@ -292,6 +297,10 @@ def test_update_should_return_updated_sandbox( assert isinstance(sandbox, Sandbox) assert sandbox.name == updated_name + assert responses.calls[0].request.body is not None + assert json.loads(responses.calls[0].request.body) == { + "inbox": {"name": updated_name} + } @pytest.mark.parametrize( "status_code,response_json,expected_error_message", diff --git a/tests/unit/test_deprecations.py b/tests/unit/test_deprecations.py index 079fc7d..c100c45 100644 --- a/tests/unit/test_deprecations.py +++ b/tests/unit/test_deprecations.py @@ -128,6 +128,18 @@ def test_client_accepts_same_inbox_id_and_sandbox_id() -> None: assert client.sandbox_id == "12345" +def test_client_accepts_same_inbox_id_and_sandbox_id_of_different_types() -> None: + with pytest.warns(DeprecationWarning, match="inbox_id"): + client = mt.MailtrapClient( + token="fake_token", + sandbox=True, + inbox_id=12345, # type: ignore[arg-type] + sandbox_id="12345", + ) + + assert client.sandbox_id == "12345" + + def test_client_rejects_different_inbox_id_and_sandbox_id() -> None: with pytest.warns(DeprecationWarning, match="inbox_id"): with pytest.raises(mt.ClientConfigurationError, match="pass only `sandbox_id`"): From 9d71c4527f4756a85e92af1da24bd81440216178 Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Tue, 3 Nov 2026 12:43:16 +0100 Subject: [PATCH 3/4] Guard empty project list in sandbox example, test sandbox ID 0 Decisions: - An account with no projects made the example fail with a bare IndexError; exit with a clear message instead - Pin the explicit None check in SandboxesApi._api_path so a truthiness check can't return unnoticed Co-Authored-By: Claude Opus 5.5 --- examples/sandbox/sandboxes.py | 6 ++++-- tests/unit/api/sandbox/test_sandboxes.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/sandbox/sandboxes.py b/examples/sandbox/sandboxes.py index a6abc48..58f9f08 100644 --- a/examples/sandbox/sandboxes.py +++ b/examples/sandbox/sandboxes.py @@ -64,9 +64,11 @@ def delete_sandbox(sandbox_id: int): sandboxes = list_sandboxes() print(sandboxes) - project_id = client.sandbox_api.projects.get_list()[0].id + projects = client.sandbox_api.projects.get_list() + if not projects: + raise SystemExit("Create a project before running this example.") created = create_sandbox( - project_id=project_id, sandbox_name="example-created-sandbox" + project_id=projects[0].id, sandbox_name="example-created-sandbox" ) print(created) diff --git a/tests/unit/api/sandbox/test_sandboxes.py b/tests/unit/api/sandbox/test_sandboxes.py index f693078..ea283b1 100644 --- a/tests/unit/api/sandbox/test_sandboxes.py +++ b/tests/unit/api/sandbox/test_sandboxes.py @@ -170,6 +170,16 @@ def test_get_by_id_should_return_single_sandbox( assert isinstance(sandbox, Sandbox) assert sandbox.id == SANDBOX_ID + @responses.activate + def test_get_by_id_should_request_sandbox_url_for_id_zero( + self, client: SandboxesApi, sample_sandbox_dict: dict + ) -> None: + responses.get(f"{BASE_INBOXES_URL}/0", json=sample_sandbox_dict, status=200) + + sandbox = client.get_by_id(0) + + assert isinstance(sandbox, Sandbox) + @pytest.mark.parametrize( "status_code,response_json,expected_error_message", [ From 7f16ce46d8a21735e5b7afc7534c775d537ef22a Mon Sep 17 00:00:00 2001 From: Maciej Walusiak Date: Thu, 5 Nov 2026 09:17:21 +0100 Subject: [PATCH 4/4] Return the old API classes from deprecated properties Decisions: - testing_api.inboxes and sending_domains_api.sending_domains return InboxesApi and SendingDomainsApi again, so isinstance checks, annotations and mocks of the old classes keep working - all four old API classes warn when built directly, so code using any of them hears about the removal - a ContextVar silences the constructor's warning when a deprecated property builds the class, so each access warns once; warnings.catch_warnings changes process-wide filters and isn't thread-safe before Python 3.14 - old keyword arguments stay type errors rather than deprecated overloads on 31 methods; the README now says so Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +- mailtrap/_deprecation.py | 19 +++++++ mailtrap/api/sending_domains.py | 14 ++++- mailtrap/api/testing.py | 10 +++- mailtrap/client.py | 7 ++- tests/unit/test_deprecations.py | 97 ++++++++++++++++++++------------- 6 files changed, 104 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 8a34a52..0feff6c 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,9 @@ The same situation applies to both `client.batch_send()` and `client.sending_api Email Sandbox inboxes are now called sandboxes, and sending domains are now called domains. The old names still work and will be removed in a future major release. -The old client properties, methods, constructor arguments and keyword arguments raise a `DeprecationWarning`; the old model and params names are plain aliases and don't warn. +The old client properties, API classes, constructor arguments and keyword arguments raise a `DeprecationWarning`; the old model and params names are plain aliases and don't warn. API requests are the same with either name. +Type checkers such as mypy and pyright report the old keyword arguments of the renamed methods (`inbox_id=`, `inbox_params=`, `sending_domain_id=`) as errors, because those methods' signatures show only the new names; rename them to type-check. | Old name | New name | |---|---| diff --git a/mailtrap/_deprecation.py b/mailtrap/_deprecation.py index 789180c..6b12317 100644 --- a/mailtrap/_deprecation.py +++ b/mailtrap/_deprecation.py @@ -1,5 +1,6 @@ import functools import warnings +from contextvars import ContextVar from typing import Callable from typing import Optional from typing import ParamSpec @@ -11,6 +12,8 @@ R = TypeVar("R") T = TypeVar("T") +_silenced: ContextVar[bool] = ContextVar("_silenced", default=False) + def warn_deprecated(old: str, new: str, stacklevel: int = 3) -> None: """ @@ -19,6 +22,8 @@ def warn_deprecated(old: str, new: str, stacklevel: int = 3) -> None: The default `stacklevel` points at the caller of the function that calls this one, so the warning shows the user's line. Python reports a warning once per call site. """ + if _silenced.get(): + return warnings.warn( f"{old} is deprecated and will be removed in a future major release; " f"use {new} instead.", @@ -27,6 +32,20 @@ def warn_deprecated(old: str, new: str, stacklevel: int = 3) -> None: ) +def build_without_warning(cls: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: + """ + Build a deprecated class without the DeprecationWarning its constructor raises. + Every warn_deprecated call is silenced while `cls(...)` runs. + + For deprecated properties that have already warned, so a user's call warns once. + """ + token = _silenced.set(True) + try: + return cls(*args, **kwargs) + finally: + _silenced.reset(token) + + def renamed_kwargs(**old_to_new: str) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Accept the old keyword names of renamed parameters. diff --git a/mailtrap/api/sending_domains.py b/mailtrap/api/sending_domains.py index 236f0d4..d433c29 100644 --- a/mailtrap/api/sending_domains.py +++ b/mailtrap/api/sending_domains.py @@ -1,6 +1,8 @@ +from mailtrap._deprecation import build_without_warning from mailtrap._deprecation import warn_deprecated from mailtrap.api.domains import DomainsBaseApi -from mailtrap.api.resources.domains import DomainsApi +from mailtrap.api.resources.sending_domains import SendingDomainsApi +from mailtrap.http import HttpClient class SendingDomainsBaseApi(DomainsBaseApi): @@ -9,11 +11,17 @@ class SendingDomainsBaseApi(DomainsBaseApi): Use :class:`mailtrap.api.domains.DomainsBaseApi` instead. """ + def __init__(self, client: HttpClient, account_id: str) -> None: + warn_deprecated("SendingDomainsBaseApi", "DomainsBaseApi") + super().__init__(client=client, account_id=account_id) + @property - def sending_domains(self) -> DomainsApi: + def sending_domains(self) -> SendingDomainsApi: """ .. deprecated:: Use :attr:`domains` instead. """ warn_deprecated("SendingDomainsBaseApi.sending_domains", "DomainsBaseApi.domains") - return self.domains + return build_without_warning( + SendingDomainsApi, client=self._client, account_id=self._account_id + ) diff --git a/mailtrap/api/testing.py b/mailtrap/api/testing.py index b1c1778..60a117b 100644 --- a/mailtrap/api/testing.py +++ b/mailtrap/api/testing.py @@ -1,7 +1,8 @@ from typing import Optional +from mailtrap._deprecation import build_without_warning from mailtrap._deprecation import warn_deprecated -from mailtrap.api.resources.sandboxes import SandboxesApi +from mailtrap.api.resources.inboxes import InboxesApi from mailtrap.api.sandbox import SandboxApi from mailtrap.http import HttpClient @@ -15,13 +16,16 @@ class TestingApi(SandboxApi): def __init__( self, client: HttpClient, account_id: str, inbox_id: Optional[str] = None ) -> None: + warn_deprecated("TestingApi", "SandboxApi") super().__init__(client=client, account_id=account_id) @property - def inboxes(self) -> SandboxesApi: + def inboxes(self) -> InboxesApi: """ .. deprecated:: Use :attr:`sandboxes` instead. """ warn_deprecated("TestingApi.inboxes", "SandboxApi.sandboxes") - return self.sandboxes + return build_without_warning( + InboxesApi, client=self._client, account_id=self._account_id + ) diff --git a/mailtrap/client.py b/mailtrap/client.py index 909848e..5feb9c1 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -6,6 +6,7 @@ from pydantic import TypeAdapter +from mailtrap._deprecation import build_without_warning from mailtrap._deprecation import resolve_renamed_arg from mailtrap._deprecation import warn_deprecated from mailtrap.api.company_info import CompanyInfoBaseApi @@ -115,7 +116,8 @@ def testing_api(self) -> TestingApi: """ warn_deprecated("MailtrapClient.testing_api", "MailtrapClient.sandbox_api") self._validate_account_id("Sandbox API") - return TestingApi( + return build_without_warning( + TestingApi, account_id=cast(str, self.account_id), client=HttpClient(host=GENERAL_HOST, headers=self.headers), ) @@ -170,7 +172,8 @@ def sending_domains_api(self) -> SendingDomainsBaseApi: "MailtrapClient.sending_domains_api", "MailtrapClient.domains_api" ) self._validate_account_id("Domains API") - return SendingDomainsBaseApi( + return build_without_warning( + SendingDomainsBaseApi, account_id=cast(str, self.account_id), client=HttpClient(host=GENERAL_HOST, headers=self.headers), ) diff --git a/tests/unit/test_deprecations.py b/tests/unit/test_deprecations.py index c100c45..8883f05 100644 --- a/tests/unit/test_deprecations.py +++ b/tests/unit/test_deprecations.py @@ -3,6 +3,8 @@ and Domain names. Each test checks that an old name still works and warns. """ +from typing import Any + import pytest import responses @@ -30,9 +32,9 @@ ACCOUNT_ID = "321" SANDBOX_ID = 3538 DOMAIN_ID = 432 -MESSAGES_URL = ( - f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/inboxes/{SANDBOX_ID}/messages" -) +SANDBOXES_URL = f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/inboxes" +MESSAGES_URL = f"{SANDBOXES_URL}/{SANDBOX_ID}/messages" +DOMAINS_URL = f"https://{GENERAL_HOST}/api/accounts/{ACCOUNT_ID}/sending_domains" COMPANY_INFO_URL = f"https://{GENERAL_HOST}/api/domains/{DOMAIN_ID}/company_info" @@ -41,36 +43,58 @@ def client_with_account() -> mt.MailtrapClient: return mt.MailtrapClient(token="fake_token", account_id=ACCOUNT_ID) -def test_testing_api_is_deprecated_alias_of_sandbox_api( - client_with_account: mt.MailtrapClient, +@pytest.mark.parametrize( + ("old_cls", "new_cls"), + [ + (testing.TestingApi, SandboxApi), + (InboxesApi, SandboxesApi), + (SendingDomainsBaseApi, DomainsBaseApi), + (SendingDomainsApi, DomainsApi), + ], +) +def test_old_api_class_is_deprecated_subclass_of_new_one( + old_cls: type[Any], new_cls: type[Any] ) -> None: with pytest.warns( - DeprecationWarning, match=r"testing_api is deprecated.*sandbox_api" + DeprecationWarning, + match=rf"^{old_cls.__name__} is deprecated.*use {new_cls.__name__} instead", ) as record: - api = client_with_account.testing_api + api = old_cls(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) - assert isinstance(api, SandboxApi) + assert isinstance(api, new_cls) assert record[0].filename == __file__ # points at the caller's line -def test_testing_api_inboxes_is_deprecated_alias_of_sandboxes() -> None: - testing_api = testing.TestingApi( - client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID - ) +def test_testing_api_is_deprecated_alias_of_sandbox_api( + client_with_account: mt.MailtrapClient, +) -> None: + with pytest.warns( + DeprecationWarning, match=r"testing_api is deprecated.*sandbox_api" + ) as record: + api = client_with_account.testing_api - with pytest.warns(DeprecationWarning, match=r"inboxes is deprecated.*sandboxes"): - api = testing_api.inboxes + assert isinstance(api, testing.TestingApi) + assert len(record) == 1 + assert record[0].filename == __file__ - assert isinstance(api, SandboxesApi) +@responses.activate +def test_testing_api_inboxes_is_deprecated_alias_of_sandboxes( + client_with_account: mt.MailtrapClient, +) -> None: + responses.get(SANDBOXES_URL, json=[]) + with pytest.warns(DeprecationWarning, match="testing_api"): + testing_api = client_with_account.testing_api -def test_inboxes_api_is_deprecated_subclass_of_sandboxes_api() -> None: with pytest.warns( - DeprecationWarning, match=r"InboxesApi is deprecated.*SandboxesApi" - ): - api = InboxesApi(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) + DeprecationWarning, match=r"inboxes is deprecated.*sandboxes" + ) as record: + api = testing_api.inboxes - assert isinstance(api, SandboxesApi) + assert isinstance(api, InboxesApi) + assert len(record) == 1 + assert api.get_list() == [] + assert len(responses.calls) == 1 def test_sending_domains_api_is_deprecated_alias_of_domains_api( @@ -78,32 +102,31 @@ def test_sending_domains_api_is_deprecated_alias_of_domains_api( ) -> None: with pytest.warns( DeprecationWarning, match=r"sending_domains_api is deprecated.*domains_api" - ): + ) as record: api = client_with_account.sending_domains_api - assert isinstance(api, DomainsBaseApi) + assert isinstance(api, SendingDomainsBaseApi) + assert len(record) == 1 + assert record[0].filename == __file__ -def test_sending_domains_is_deprecated_alias_of_domains() -> None: - sending_domains_api = SendingDomainsBaseApi( - client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID - ) +@responses.activate +def test_sending_domains_is_deprecated_alias_of_domains( + client_with_account: mt.MailtrapClient, +) -> None: + responses.get(DOMAINS_URL, json={"data": []}) + with pytest.warns(DeprecationWarning, match="sending_domains_api"): + sending_domains_api = client_with_account.sending_domains_api with pytest.warns( DeprecationWarning, match=r"sending_domains is deprecated.*domains" - ): + ) as record: api = sending_domains_api.sending_domains - assert isinstance(api, DomainsApi) - - -def test_sending_domains_api_class_is_deprecated_subclass_of_domains_api() -> None: - with pytest.warns( - DeprecationWarning, match=r"SendingDomainsApi is deprecated.*DomainsApi" - ): - api = SendingDomainsApi(client=HttpClient(GENERAL_HOST), account_id=ACCOUNT_ID) - - assert isinstance(api, DomainsApi) + assert isinstance(api, SendingDomainsApi) + assert len(record) == 1 + assert api.get_list() == [] + assert len(responses.calls) == 1 def test_client_inbox_id_argument_is_deprecated_alias_of_sandbox_id() -> None: