Skip to content

Conversation

Ihor-Bilous
Copy link
Collaborator

@Ihor-Bilous Ihor-Bilous commented Aug 18, 2025

Motivation

In this PR I added Sandbox sending email feature and refactored default sending functionality using new design structure.

Changes

  • Added SendindApi protocol and two APIs - DefaultSendingApi (used for emailing and bulk emailing), SandboxSendingApi (used for sandbox emailing).
  • MailtrapClient.send method adapted to new design, base_url and api_send_url are deprecated.
  • Added tests and examples

How to test

See examples/sending..py

Images and GIFs

Before After
link1 link2
link3 link4
link5 link6
link7 link8
link9 link10

Summary by CodeRabbit

  • New Features

    • Added a runnable sending example demonstrating standard, bulk and sandbox sends, including template-based emails.
  • Refactor

    • Sending flow reworked to a pluggable sending layer; mail data models consolidated into a new models package and legacy mail-package re-exports removed. Dedicated hosts for sending/bulk/sandbox added and legacy URL properties deprecated.
  • Tests

    • New unit tests covering sending API behavior and client error handling.

Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Refactors sending flow: introduces a SendingApi and HttpClient usage, moves mail models to pydantic dataclasses under mailtrap.models.mail, updates MailtrapClient to delegate sends, adds host constants, new examples and focused tests, and removes legacy mailtrap.mail implementations.

Changes

Cohort / File(s) Summary
Examples
examples/sending.py
New example demonstrating API_TOKEN/INBOX_ID, three MailtrapClient variants (default/bulk/sandbox), Mail and MailFromTemplate instances, send helper, placeholder batch_send, and a main guard.
API: sending
mailtrap/api/sending.py
New SendingApi class (constructed with HttpClient and optional inbox_id) that posts mail.api_data to the appropriate /api/send endpoint and returns SendingMailResponse.
Client refactor
mailtrap/client.py
MailtrapClient now exposes sending_api (built over HttpClient), delegates send() to it, defines SEND_ENDPOINT_RESPONSE alias, introduces _sending_api_host (config-driven), deprecates base_url/api_send_url, and adds _validate_account_id.
Config constants
mailtrap/config.py
Added SENDING_HOST, BULK_HOST, and SANDBOX_HOST constants.
Public root exports
mailtrap/__init__.py
Adjusted re-exports: added SEND_ENDPOINT_RESPONSE, removed legacy BaseEntity re-export, and re-exported mail models from mailtrap.models.mail.
New models (pydantic dataclasses)
mailtrap/models/common.py, mailtrap/models/mail/*
Added RequestModel with api_data; added pydantic-dataclass mail models: Address, Attachment (+Disposition), BaseMail, MailFromTemplate, Mail, and SendingMailResponse; package-level re-exports via mailtrap.models.mail.__init__.
Removed legacy mail package
mailtrap/mail/*
Removed legacy mailtrap.mail files and classes (BaseEntity, Address, Attachment/Disposition, BaseMail, MailFromTemplate, Mail) — replaced by mailtrap.models.mail.
Tests: sending & imports
tests/unit/api/test_sending.py, tests/unit/mail/*, tests/unit/test_client.py
Added SendingApi unit tests (success/error cases); updated tests to import models from mailtrap.models.mail; removed older responses-based client send E2E tests and adjusted client tests for new behavior.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MailtrapClient
  participant SendingApi
  participant HttpClient
  participant MailtrapAPI

  User->>MailtrapClient: send(mail)
  MailtrapClient->>SendingApi: send(mail)
  alt inbox-scoped
    SendingApi->>HttpClient: POST https://<SANDBOX_HOST>/api/send/{inbox_id}\njson=mail.api_data
  else global
    SendingApi->>HttpClient: POST https://<SENDING_HOST>/api/send\njson=mail.api_data
  end
  HttpClient->>MailtrapAPI: HTTP POST
  MailtrapAPI-->>HttpClient: JSON response
  HttpClient-->>SendingApi: response dict
  SendingApi-->>MailtrapClient: SendingMailResponse
  MailtrapClient-->>User: SEND_ENDPOINT_RESPONSE (serialized)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • VladimirTaytor
  • andrii-porokhnavets

Poem

I hop through fields of dataclass light,
New hosts and routes in morning bright.
I stitch the send from client to API,
A tiny rabbit sending mail sky-high.
Hip-hop — deliveries take flight! 🐇✉️

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ISSUE-25

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (21)
mailtrap/config.py (1)

2-4: Host constants look good; add typing Final for immutability and intent

These hosts are likely to be true constants. Annotating them as Final improves readability and tooling/static analysis.

Apply this diff within the selected lines:

-BULK_HOST = "bulk.api.mailtrap.io"
-SANDBOX_HOST = "sandbox.api.mailtrap.io"
-SENDING_HOST = "send.api.mailtrap.io"
+BULK_HOST: Final[str] = "bulk.api.mailtrap.io"
+SANDBOX_HOST: Final[str] = "sandbox.api.mailtrap.io"
+SENDING_HOST: Final[str] = "send.api.mailtrap.io"

And add this import at the top of the file (outside the selected range):

from typing import Final
tests/unit/mail/test_mail.py (1)

6-6: Rename test class to reflect subject under test

This class tests Mail, not Attachment. Renaming improves clarity for test reports and future maintenance.

Apply this diff:

-class TestAttachment:
+class TestMail:
tests/unit/mail/test_from_template.py (1)

6-6: Rename test class for accuracy

This suite validates MailFromTemplate. Rename for clearer test intent and discoverability.

Apply this diff:

-class TestAttachment:
+class TestMailFromTemplate:
mailtrap/models/mail/address.py (1)

1-11: Prefer EmailStr for validated email addresses

Using EmailStr adds lightweight validation and produces clearer API errors on invalid emails, with no changes required at call sites.

-from pydantic.dataclasses import dataclass
+from pydantic import EmailStr
+from pydantic.dataclasses import dataclass
@@
 class Address(RequestModel):
-    email: str
+    email: EmailStr
     name: Optional[str] = None
mailtrap/models/mail/mail.py (3)

9-11: Guard against dataclass field-ordering pitfall (make subclass fields keyword-only)

BaseMail defines fields with defaults (e.g., cc, bcc, ...). Adding a required field (subject) in a subclass can trigger "non-default argument follows default argument" in dataclasses. Marking subclass fields kw-only avoids this class of issues across Python versions.

-@dataclass
+@dataclass(kw_only=True)
 class Mail(BaseMail):
     subject: str = Field(...)  # type:ignore

If BaseMail is already declared with kw_only=True, this change is optional; otherwise it’s a safe guardrail.


1-4: Remove type:ignore by using Annotated with Field for a required attribute

This keeps type-checkers happy without suppressing checks.

-from pydantic import Field
+from pydantic import Field
+from typing_extensions import Annotated
@@
-    subject: str = Field(...)  # type:ignore
+    subject: Annotated[str, Field(...)]

Also applies to: 11-11


12-14: Optional: enforce that at least one of text or html is provided

If your API requires at least one body variant, enforce it at model level for earlier feedback.

 class Mail(BaseMail):
     subject: str = Field(...)  # type:ignore
     text: Optional[str] = None
     html: Optional[str] = None
     category: Optional[str] = None
+    def __post_init_post_parse__(self) -> None:
+        if self.text is None and self.html is None:
+            raise ValueError("At least one of `text` or `html` must be provided")

Note: post_init_post_parse is called after Pydantic validation in dataclass mode. If you prefer not to couple domain rules to the model, consider validating in the sending layer instead.

mailtrap/models/mail/from_template.py (1)

10-13: Apply the same kw_only safeguard to avoid dataclass ordering issues

As with Mail, this subclass adds a required field while BaseMail has defaults; kw_only avoids "non-default follows default" problems.

-@dataclass
+@dataclass(kw_only=True)
 class MailFromTemplate(BaseMail):
     template_uuid: str = Field(...)  # type:ignore
     template_variables: Optional[dict[str, Any]] = None
mailtrap/models/common.py (1)

1-5: Micro-optimization: cache TypeAdapter per class

Avoids re-creating TypeAdapter on every api_data call, which can add up in hot paths (bulk sends).

 from typing import Any
 from typing import TypeVar
 from typing import cast
 
 from pydantic import TypeAdapter
+from functools import lru_cache
@@
 class RequestModel:
     @property
     def api_data(self: T) -> dict[str, Any]:
-        return cast(
-            dict[str, Any],
-            TypeAdapter(type(self)).dump_python(self, by_alias=True, exclude_none=True),
-        )
+        return cast(
+            dict[str, Any],
+            _adapter_for(type(self)).dump_python(self, by_alias=True, exclude_none=True),
+        )
+
+@lru_cache(maxsize=None)
+def _adapter_for(tp: type[Any]) -> TypeAdapter:
+    return TypeAdapter(tp)

Also applies to: 14-18

tests/unit/test_client.py (1)

52-65: Parametric validation of sending_api selection looks comprehensive

Covers default, bulk, and sandbox flows. One additional test worth considering: verify that MailtrapClient.send delegates to client.sending_api.send for both default and sandbox cases (using a stubbed/mocked SendingApi). This would guard against regressions if the delegation changes.

I can draft a unit test using monkeypatch to stub DefaultSendingApi/SandboxSendingApi.send and assert it’s called with mail.api_data. Want me to add that?

mailtrap/__init__.py (1)

1-12: Explicitly declare public API to satisfy linters and clarify exports

Ruff flags all these imports as unused because they are only for re-export. Define __all__ to make the intent explicit and silence F401. This also documents the public surface.

Apply this diff to declare the public API:

 from .api.sending import SEND_ENDPOINT_RESPONSE
 from .client import MailtrapClient
 from .exceptions import APIError
 from .exceptions import AuthorizationError
 from .exceptions import ClientConfigurationError
 from .exceptions import MailtrapError
 from .models.mail import Address
 from .models.mail import Attachment
 from .models.mail import BaseMail
 from .models.mail import Disposition
 from .models.mail import Mail
 from .models.mail import MailFromTemplate
+
+__all__ = [
+    "SEND_ENDPOINT_RESPONSE",
+    "MailtrapClient",
+    "MailtrapError",
+    "ClientConfigurationError",
+    "APIError",
+    "AuthorizationError",
+    "Address",
+    "Attachment",
+    "BaseMail",
+    "Disposition",
+    "Mail",
+    "MailFromTemplate",
+]
mailtrap/models/mail/base.py (1)

12-20: Verify Pydantic dataclass Field usage and alias behavior

Using pydantic.Field(...) directly as the default in a pydantic.dataclasses.dataclass is unconventional; in v2 the recommended pattern is dataclasses.field(metadata={'pydantic': Field(...)}). Please verify that:

  • mail.api_data includes the key "from" (alias for sender)
  • None fields are excluded as expected (exclude_none) for all optional attributes

If the alias isn’t being applied, consider switching to either:

  • dataclasses.field(metadata={'pydantic': Field(serialization_alias="from")}) for dataclass fields, or
  • Pydantic BaseModel, which works seamlessly with Field(...) and field_serializer.

I can provide a concrete refactor for either approach if you prefer.

tests/unit/api/test_sending.py (2)

15-17: Remove unused constants to avoid lint warnings

ACCOUNT_ID and PROJECT_ID are not used. Drop them to keep tests tidy.

-ACCOUNT_ID = "321"
-PROJECT_ID = 123
 INBOX_ID = "456"

120-123: Compare JSON payloads semantically, not by raw bytes

Stringifying dicts can differ in key ordering and whitespace. Decode and json.loads the request body and compare dictionaries for a more robust assertion.

-        request = responses.calls[0].request  # type: ignore
-        assert request.body == json.dumps(mail.api_data).encode()
+        request = responses.calls[0].request  # type: ignore
+        assert json.loads(request.body.decode()) == mail.api_data
examples/sending.py (2)

3-4: Fix placeholder typos for clarity

Minor typos in placeholders.

-API_TOKEN = "<YOU_API_TOKEN>"
+API_TOKEN = "<YOUR_API_TOKEN>"
 INBOX_ID = "<YOUR_INBOX_ID>"

22-22: Fix template UUID placeholder typo

-    template_uuid="<YOUT_TEMPLATE_UUID>",
+    template_uuid="<YOUR_TEMPLATE_UUID>",
mailtrap/api/sending.py (2)

8-8: Use a TypedDict for the response type instead of a loose dict alias

A TypedDict communicates expected keys clearly and gives better type safety at call sites without losing flexibility.

Apply:

-from typing import Protocol
+from typing import Protocol
+from typing import TypedDict
 from typing import Union
 from typing import cast
@@
-SEND_ENDPOINT_RESPONSE = dict[str, Union[bool, list[str]]]
+class _SendEndpointResponse(TypedDict, total=False):
+    success: bool
+    errors: list[str]
+    # Add optional keys here if the API returns more, e.g.:
+    # message_ids: list[str]
+
+SEND_ENDPOINT_RESPONSE = _SendEndpointResponse

15-23: Small DRY: share the send path assembly

Default and Sandbox senders only differ by path. A tiny helper reduces duplication and centralizes the endpoint shape.

Example refactor:

 class DefaultSendingApi:
     def __init__(self, client: HttpClient) -> None:
         self._client = client
 
     def send(self, mail: BaseMail) -> SEND_ENDPOINT_RESPONSE:
-        return cast(
-            SEND_ENDPOINT_RESPONSE, self._client.post("/api/send", json=mail.api_data)
-        )
+        return cast(
+            SEND_ENDPOINT_RESPONSE, self._post("/api/send", mail)
+        )
+    def _post(self, path: str, mail: BaseMail) -> SEND_ENDPOINT_RESPONSE:
+        return cast(SEND_ENDPOINT_RESPONSE, self._client.post(path, json=mail.api_data))
@@
 class SandboxSendingApi:
     def __init__(self, inbox_id: str, client: HttpClient) -> None:
         self.inbox_id = inbox_id
         self._client = client
 
     def send(self, mail: BaseMail) -> SEND_ENDPOINT_RESPONSE:
-        return cast(
-            SEND_ENDPOINT_RESPONSE,
-            self._client.post(f"/api/send/{self.inbox_id}", json=mail.api_data),
-        )
+        return cast(
+            SEND_ENDPOINT_RESPONSE,
+            self._client.post(f"/api/send/{self.inbox_id}", json=mail.api_data),
+        )

Or, derive Sandbox from Default and override just the path.

Also applies to: 25-34

mailtrap/client.py (3)

51-59: Reuse a single HttpClient/session and cache the SendingApi

Currently a new HttpClient (and requests.Session) is created on every access, which loses connection reuse and adds overhead. Cache the constructed SendingApi.

Apply:

 @property
 def sending_api(self) -> SendingApi:
-    http_client = HttpClient(host=self._sending_api_host, headers=self.headers)
-    if self.sandbox:
-        return SandboxSendingApi(
-            inbox_id=cast(str, self.inbox_id), client=http_client
-        )
-    return DefaultSendingApi(client=http_client)
+    # Lazily construct and cache to reuse a single Session/connection pool
+    if hasattr(self, "_sending_api") and self._sending_api is not None:  # type: ignore[attr-defined]
+        return self._sending_api  # type: ignore[return-value]
+    http_client = HttpClient(host=self._sending_api_host, headers=self.headers)
+    if self.sandbox:
+        self._sending_api = SandboxSendingApi(  # type: ignore[attr-defined]
+            inbox_id=cast(str, self.inbox_id), client=http_client
+        )
+    else:
+        self._sending_api = DefaultSendingApi(client=http_client)  # type: ignore[attr-defined]
+    return self._sending_api  # type: ignore[return-value]

And initialize the cache in init (outside this hunk):

# in __init__
self._sending_api: Optional[SendingApi] = None

Note: If you intend to support mutating config (sandbox/inbox_id/api_host) after init, add invalidation logic.


60-61: Ensure callers receive the response; fix example to return the value

send() correctly returns the API response. The example in examples/sending.py doesn’t return it.

Proposed example fix (outside this file):

def send(client: mt.MailtrapClient, mail: mt.BaseMail) -> mt.SEND_ENDPOINT_RESPONSE:
    return client.send(mail)

65-71: api_port isn’t applied to live HTTPClient requests

HttpClient._url (mailtrap/http.py lines 47–48) always builds URLs as

f"https://{self._host}/{path.lstrip('/')}"

so any api_port passed into the client has no effect on real requests (it only applies to deprecated base_url/api_send_url).

Two ways to resolve:

  • Deprecate api_port in HttpClient.init:
    • Add a warnings.warn(..., DeprecationWarning, stacklevel=2) when api_port is provided.
    • Update docs to state that live requests ignore custom ports.

  • Respect api_port in HttpClient:
    • Change init to accept and store a port parameter (e.g. self._port).
    • Update _url to include it, e.g.:

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

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ac4ca01 and 7b1e44f.

📒 Files selected for processing (25)
  • examples/sending.py (1 hunks)
  • mailtrap/__init__.py (1 hunks)
  • mailtrap/api/sending.py (1 hunks)
  • mailtrap/client.py (3 hunks)
  • mailtrap/config.py (1 hunks)
  • mailtrap/mail/__init__.py (0 hunks)
  • mailtrap/mail/address.py (0 hunks)
  • mailtrap/mail/attachment.py (0 hunks)
  • mailtrap/mail/base.py (0 hunks)
  • mailtrap/mail/base_entity.py (0 hunks)
  • mailtrap/mail/from_template.py (0 hunks)
  • mailtrap/mail/mail.py (0 hunks)
  • mailtrap/models/common.py (1 hunks)
  • mailtrap/models/mail/__init__.py (1 hunks)
  • mailtrap/models/mail/address.py (1 hunks)
  • mailtrap/models/mail/attachment.py (1 hunks)
  • mailtrap/models/mail/base.py (1 hunks)
  • mailtrap/models/mail/from_template.py (1 hunks)
  • mailtrap/models/mail/mail.py (1 hunks)
  • tests/unit/api/test_sending.py (1 hunks)
  • tests/unit/mail/test_address.py (1 hunks)
  • tests/unit/mail/test_attachment.py (1 hunks)
  • tests/unit/mail/test_from_template.py (1 hunks)
  • tests/unit/mail/test_mail.py (1 hunks)
  • tests/unit/test_client.py (2 hunks)
💤 Files with no reviewable changes (7)
  • mailtrap/mail/from_template.py
  • mailtrap/mail/base_entity.py
  • mailtrap/mail/init.py
  • mailtrap/mail/base.py
  • mailtrap/mail/mail.py
  • mailtrap/mail/attachment.py
  • mailtrap/mail/address.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#31
File: mailtrap/config.py:1-1
Timestamp: 2025-08-12T23:07:25.653Z
Learning: In Mailtrap's API architecture, Testing API resources (Projects, Inboxes, etc.) use the main "mailtrap.io" host, while only email sending functionality uses "sandbox.api.mailtrap.io" as the host.
📚 Learning: 2025-08-12T23:07:25.653Z
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#31
File: mailtrap/config.py:1-1
Timestamp: 2025-08-12T23:07:25.653Z
Learning: In Mailtrap's API architecture, Testing API resources (Projects, Inboxes, etc.) use the main "mailtrap.io" host, while only email sending functionality uses "sandbox.api.mailtrap.io" as the host.

Applied to files:

  • mailtrap/config.py
  • mailtrap/api/sending.py
  • tests/unit/api/test_sending.py
  • examples/sending.py
  • mailtrap/client.py
🧬 Code Graph Analysis (16)
mailtrap/models/mail/base.py (4)
mailtrap/models/common.py (1)
  • RequestModel (12-18)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (1)
  • Attachment (18-27)
mailtrap/client.py (1)
  • headers (85-92)
tests/unit/mail/test_mail.py (3)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (1)
  • Attachment (18-27)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
tests/unit/mail/test_address.py (1)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/address.py (1)
mailtrap/models/common.py (1)
  • RequestModel (12-18)
mailtrap/models/mail/from_template.py (1)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
tests/unit/mail/test_from_template.py (3)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (1)
  • Attachment (18-27)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
tests/unit/test_client.py (2)
mailtrap/api/sending.py (2)
  • DefaultSendingApi (15-22)
  • SandboxSendingApi (25-34)
mailtrap/client.py (2)
  • testing_api (43-49)
  • sending_api (52-58)
tests/unit/mail/test_attachment.py (1)
mailtrap/models/mail/attachment.py (2)
  • Attachment (18-27)
  • Disposition (12-14)
mailtrap/api/sending.py (5)
mailtrap/http.py (2)
  • HttpClient (13-92)
  • post (31-33)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
examples/sending.py (1)
  • send (34-35)
mailtrap/client.py (1)
  • send (60-61)
mailtrap/models/common.py (1)
  • api_data (14-18)
mailtrap/models/mail/mail.py (1)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
mailtrap/models/mail/__init__.py (5)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (2)
  • Attachment (18-27)
  • Disposition (12-14)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
tests/unit/api/test_sending.py (8)
mailtrap/api/sending.py (6)
  • DefaultSendingApi (15-22)
  • SandboxSendingApi (25-34)
  • SendingApi (11-12)
  • send (12-12)
  • send (19-22)
  • send (30-34)
mailtrap/http.py (2)
  • HttpClient (13-92)
  • post (31-33)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
mailtrap/exceptions.py (2)
  • AuthorizationError (18-20)
  • APIError (10-15)
mailtrap/models/common.py (1)
  • api_data (14-18)
mailtrap/__init__.py (7)
mailtrap/client.py (1)
  • MailtrapClient (19-118)
mailtrap/exceptions.py (4)
  • APIError (10-15)
  • AuthorizationError (18-20)
  • ClientConfigurationError (5-7)
  • MailtrapError (1-2)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (2)
  • Attachment (18-27)
  • Disposition (12-14)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
examples/sending.py (6)
mailtrap/client.py (2)
  • MailtrapClient (19-118)
  • send (60-61)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
mailtrap/api/sending.py (3)
  • send (12-12)
  • send (19-22)
  • send (30-34)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
mailtrap/models/mail/attachment.py (1)
mailtrap/models/common.py (1)
  • RequestModel (12-18)
mailtrap/client.py (6)
mailtrap/api/sending.py (6)
  • DefaultSendingApi (15-22)
  • SandboxSendingApi (25-34)
  • SendingApi (11-12)
  • send (12-12)
  • send (19-22)
  • send (30-34)
mailtrap/api/testing.py (1)
  • TestingApi (7-17)
mailtrap/exceptions.py (1)
  • ClientConfigurationError (5-7)
mailtrap/http.py (1)
  • HttpClient (13-92)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
examples/sending.py (1)
  • send (34-35)
🪛 Ruff (0.12.2)
mailtrap/__init__.py

1-1: .api.sending.SEND_ENDPOINT_RESPONSE imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


2-2: .client.MailtrapClient imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


3-3: .exceptions.APIError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


4-4: .exceptions.AuthorizationError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


5-5: .exceptions.ClientConfigurationError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


6-6: .exceptions.MailtrapError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


7-7: .models.mail.Address imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


8-8: .models.mail.Attachment imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


9-9: .models.mail.BaseMail imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


10-10: .models.mail.Disposition imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


11-11: .models.mail.Mail imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


12-12: .models.mail.MailFromTemplate imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

🔇 Additional comments (14)
mailtrap/config.py (1)

2-4: Host-to-API mapping verified—no changes needed

  • GENERAL_HOST is used for Testing API (Projects, Inboxes, etc.) via HttpClient(GENERAL_HOST).
  • SANDBOX_HOST is used for sandbox sending.
  • SENDING_HOST is used for production sending.
  • BULK_HOST is returned when the client’s bulk flag is set (bulk sending).
  • Legacy base_url/api_send_url properties remain only to emit deprecation warnings and are exercised in tests—properly deprecated.
tests/unit/mail/test_mail.py (1)

1-3: Import path update aligns with the new models package

The move to mailtrap.models.mail keeps tests consistent with the refactor. No issues with the entities’ usage.

tests/unit/mail/test_attachment.py (1)

1-2: Import path update looks correct

The imports now point to the refactored models module; the assertions match the serializer behavior (mimetype alias “type”, disposition enum string values).

tests/unit/mail/test_address.py (1)

1-1: Import path update is consistent with the refactor

Address moved under mailtrap.models.mail.address; tests remain valid and focused on api_data behavior.

tests/unit/mail/test_from_template.py (1)

1-3: Model import updates are correct

Switching to mailtrap.models.mail for Address, Attachment, and MailFromTemplate aligns with the new module structure.

mailtrap/models/mail/address.py (1)

8-11: LGTM: Address model is clean and consistent with RequestModel serialization

Simple, minimal, and aligns with the new RequestModel serialization strategy. No concerns.

mailtrap/models/common.py (1)

11-19: Solid, uniform serialization via TypeAdapter with by_alias and exclude_none

This neatly centralizes serialization for all request models and respects Field aliases (e.g., serialization_alias="from").

tests/unit/test_client.py (2)

6-7: Good: asserting the concrete SendingApi implementation

Simple and effective check that the client selects the right SendingApi based on config.


48-48: Nice touch: assigning to '_' to avoid an unused variable warning

Keeps the intent clear while triggering the property access (and its validation).

mailtrap/models/mail/__init__.py (2)

1-6: Clean public surface for mail models — LGTM

Re-exports are coherent with the new models package. Import paths look consistent with the refactor and tests.


8-15: all is complete and explicit — LGTM

The explicit all keeps the package’s public API tight and predictable.

mailtrap/api/sending.py (1)

20-22: Confirmed: api_data is a @property
The api_data accessor is defined as a property in mailtrap/models/common.py (line 14), so passing json=mail.api_data is correct—no parentheses needed.

mailtrap/client.py (2)

95-103: Host resolution precedence — LGTM

api_host override, then sandbox, then bulk, then default sending host is a sensible order and aligns with the architecture (Testing API uses GENERAL_HOST; Sending uses SENDING_HOST/SANDBOX_HOST).


104-107: Testing API guardrail — LGTM

Requiring account_id when accessing Testing API prevents misconfiguration at runtime.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (8)
tests/unit/api/test_sending.py (3)

93-97: Make request-body assertion robust to JSON formatting differences

Requests may change JSON serialization details (e.g., spacing). Comparing parsed JSON avoids brittleness while still validating payload correctness.

Apply this diff:

-        request = responses.calls[0].request  # type: ignore
-        assert request.body == json.dumps(mail.api_data).encode()
+        request = responses.calls[0].request  # type: ignore
+        assert json.loads(request.body.decode()) == mail.api_data

12-15: Remove unused test constants

ACCOUNT_ID, PROJECT_ID, and INBOX_ID are defined but unused in this module. Trim to reduce noise and potential linter warnings.

-ACCOUNT_ID = "321"
-PROJECT_ID = 123
-INBOX_ID = "456"

38-52: Add coverage for inbox-scoped sending URL

Consider a test verifying that SendingApi(client, inbox_id="456") posts to /api/send/456. This guards against path regressions for sandbox inbox routing.

Here’s a test you can add:

@responses.activate
@pytest.mark.parametrize("mail", MAIL_ENTITIES)
def test_send_uses_inbox_scoped_path_when_inbox_id_provided(mail: mt.BaseMail) -> None:
    inbox_id = "456"
    scoped_url = f"{SEND_FULL_URL}/{inbox_id}"
    response_body = {"success": True, "message_ids": ["id-1"]}
    responses.post(scoped_url, json=response_body, status=200)

    api = SendingApi(client=HttpClient(SENDING_HOST), inbox_id=inbox_id)
    result = api.send(mail)

    assert isinstance(result, SendingMailResponse)
    assert result.success is True
    assert len(responses.calls) == 1
    assert responses.calls[0].request.url == scoped_url  # type: ignore
mailtrap/api/sending.py (1)

13-18: Nit: rename _api_url to _api_path for clarity

This property returns a path, not a full URL. Renaming improves readability and avoids confusion with HttpClient._url(...) that builds the full URL.

Apply this diff:

-    @property
-    def _api_url(self) -> str:
+    @property
+    def _api_path(self) -> str:
         url = "/api/send"
         if self._inbox_id:
             return f"{url}/{self._inbox_id}"
         return url
@@
-    def send(self, mail: BaseMail) -> SendingMailResponse:
-        response = self._client.post(self._api_url, json=mail.api_data)
+    def send(self, mail: BaseMail) -> SendingMailResponse:
+        response = self._client.post(self._api_path, json=mail.api_data)
         return SendingMailResponse(**response)

Also applies to: 20-22

mailtrap/client.py (4)

54-58: Consider reusing a single HttpClient/SendingApi instance to benefit from connection pooling

Constructing a new HttpClient (requests.Session) on every access can forfeit HTTP connection reuse and add overhead. If you don't rely on mutating token, sandbox, bulk, or inbox_id after construction, consider caching the instance.

Example change (keeps behavior if you later need to drop caching):

 @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)
+    try:
+        return self._sending_api  # type: ignore[attr-defined]
+    except AttributeError:
+        http_client = HttpClient(host=self._sending_api_host, headers=self.headers)
+        self._sending_api = SendingApi(client=http_client, inbox_id=self.inbox_id)  # type: ignore[attr-defined]
+        return self._sending_api

Note: If you do need to support token rotation or mode toggling post-init, skip caching or add an explicit refresh method.


19-19: Tighten typing for the public send() result using TypedDict (no runtime change)

Using a broad dict[str, Union[bool, list[str]]] loses key guarantees for success and message_ids. A TypedDict keeps the runtime as dict but gives precise typing to users.

Apply:

-SEND_ENDPOINT_RESPONSE = dict[str, Union[bool, list[str]]]
+class SEND_ENDPOINT_RESPONSE(TypedDict):
+    success: bool
+    message_ids: list[str]

Additionally add the import:

from typing import TypedDict

68-73: base_url deprecation warning is fine; watch for edge-case api_host values

The deprecation path looks good. One caveat: if callers pass api_host with scheme or with a port (e.g., localhost:8443), base_url will produce https://localhost:8443:443, which is odd even if deprecated.

If worth addressing despite deprecation, sanitize api_host or document “host only, no scheme/port” in the parameter docstring.


76-85: Avoid double deprecation warnings from api_send_url

Calling api_send_url triggers its warning and then calls base_url, which triggers another warning. Compose the URL directly here to emit a single warning.

     def api_send_url(self) -> str:
         warnings.warn(
             "api_send_url is deprecated and will be removed in a future release.",
             DeprecationWarning,
             stacklevel=2,
         )
-        url = f"{self.base_url}/api/send"
+        # Compose directly to avoid triggering base_url's deprecation warning too
+        url = f"https://{self._sending_api_host.rstrip('/')}:{self.api_port}/api/send"
         if self.sandbox and self.inbox_id:
             return f"{url}/{self.inbox_id}"
         return url
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 27e299c and c21e0fc.

📒 Files selected for processing (6)
  • mailtrap/__init__.py (1 hunks)
  • mailtrap/api/sending.py (1 hunks)
  • mailtrap/client.py (3 hunks)
  • mailtrap/models/mail/base.py (1 hunks)
  • tests/unit/api/test_sending.py (1 hunks)
  • tests/unit/test_client.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#31
File: mailtrap/config.py:1-1
Timestamp: 2025-08-12T23:07:25.653Z
Learning: In Mailtrap's API architecture, Testing API resources (Projects, Inboxes, etc.) use the main "mailtrap.io" host, while only email sending functionality uses "sandbox.api.mailtrap.io" as the host.
📚 Learning: 2025-08-12T23:07:25.653Z
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#31
File: mailtrap/config.py:1-1
Timestamp: 2025-08-12T23:07:25.653Z
Learning: In Mailtrap's API architecture, Testing API resources (Projects, Inboxes, etc.) use the main "mailtrap.io" host, while only email sending functionality uses "sandbox.api.mailtrap.io" as the host.

Applied to files:

  • tests/unit/api/test_sending.py
  • mailtrap/api/sending.py
🧬 Code Graph Analysis (6)
mailtrap/models/mail/base.py (3)
mailtrap/models/common.py (1)
  • RequestModel (12-18)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (1)
  • Attachment (18-27)
tests/unit/test_client.py (1)
mailtrap/client.py (1)
  • testing_api (46-52)
tests/unit/api/test_sending.py (8)
mailtrap/api/sending.py (2)
  • SendingApi (8-22)
  • send (20-22)
mailtrap/http.py (2)
  • HttpClient (13-92)
  • post (31-33)
mailtrap/models/mail/base.py (2)
  • SendingMailResponse (24-26)
  • BaseMail (13-20)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
mailtrap/exceptions.py (2)
  • AuthorizationError (18-20)
  • APIError (10-15)
mailtrap/client.py (1)
  • send (59-64)
mailtrap/models/common.py (1)
  • api_data (14-18)
mailtrap/__init__.py (6)
mailtrap/client.py (1)
  • MailtrapClient (22-121)
mailtrap/models/mail/address.py (1)
  • Address (9-11)
mailtrap/models/mail/attachment.py (2)
  • Attachment (18-27)
  • Disposition (12-14)
mailtrap/models/mail/base.py (1)
  • BaseMail (13-20)
mailtrap/models/mail/mail.py (1)
  • Mail (10-14)
mailtrap/models/mail/from_template.py (1)
  • MailFromTemplate (11-13)
mailtrap/client.py (5)
mailtrap/api/sending.py (2)
  • SendingApi (8-22)
  • send (20-22)
mailtrap/api/testing.py (1)
  • TestingApi (7-17)
mailtrap/exceptions.py (1)
  • ClientConfigurationError (5-7)
mailtrap/http.py (1)
  • HttpClient (13-92)
mailtrap/models/mail/base.py (2)
  • BaseMail (13-20)
  • SendingMailResponse (24-26)
mailtrap/api/sending.py (4)
mailtrap/http.py (2)
  • HttpClient (13-92)
  • post (31-33)
mailtrap/models/mail/base.py (2)
  • BaseMail (13-20)
  • SendingMailResponse (24-26)
mailtrap/client.py (1)
  • send (59-64)
mailtrap/models/common.py (1)
  • api_data (14-18)
🪛 Ruff (0.12.2)
mailtrap/__init__.py

1-1: .client.SEND_ENDPOINT_RESPONSE imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


2-2: .client.MailtrapClient imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


3-3: .exceptions.APIError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


4-4: .exceptions.AuthorizationError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


5-5: .exceptions.ClientConfigurationError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


6-6: .exceptions.MailtrapError imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


7-7: .models.mail.Address imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


8-8: .models.mail.Attachment imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


9-9: .models.mail.BaseMail imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


10-10: .models.mail.Disposition imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


11-11: .models.mail.Mail imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)


12-12: .models.mail.MailFromTemplate imported but unused; consider removing, adding to __all__, or using a redundant alias

(F401)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test python3.13 on windows-latest
  • GitHub Check: Test python3.12 on windows-latest
  • GitHub Check: Test python3.9 on macos-latest
  • GitHub Check: Test python3.11 on windows-latest
  • GitHub Check: Test python3.10 on windows-latest
  • GitHub Check: Test python3.9 on windows-latest
🔇 Additional comments (7)
tests/unit/test_client.py (2)

46-46: Good change: accessing the property triggers validation

Assigning client.testing_api to _ ensures the property getter runs and raises ClientConfigurationError as intended.


46-46: testing_api is correctly declared as a property
The testing_api accessor in MailtrapClient is preceded by @property (mailtrap/client.py:45), so client.testing_api is valid. No changes required.

mailtrap/models/mail/base.py (1)

12-26: LGTM: clean, minimal Pydantic dataclasses for request/response

  • serialization_alias="from" on sender aligns with API payload.
  • Optional fields are excluded when None via api_data, keeping payloads concise.
  • SendingMailResponse shape matches tests and API usage.
mailtrap/client.py (4)

45-52: Good: Testing API host selection with account_id guard is correct

Using GENERAL_HOST for Testing API and enforcing account_id upfront matches the product split (Testing API on main host vs. Sending on dedicated hosts).


59-64: BC preserved: send() still returns a dict-shaped payload

Delegating to SendingApi and serializing via Pydantic while returning a plain dict keeps existing callers unbroken. Good call given the prior discussion.


98-106: Host resolution precedence looks correct

Priority: explicit api_host > sandbox host > bulk host > default sending host. Combined with the constructor validation, this is clear and robust.


107-110: Good: Explicit validation for Testing API account_id

Clear error message and early validation improve DX.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
mailtrap/client.py (5)

19-19: Strengthen response typing with TypedDict instead of a loose dict alias

Using dict[str, Union[bool, list[str]]] loses key-level guarantees. A TypedDict conveys the exact response shape and improves IDE/helpfulness without breaking runtime.

Apply this diff to introduce a precise type:

-from typing import Optional
-from typing import Union
-from typing import cast
+from typing import Optional, TypedDict, Union, cast
@@
-SEND_ENDPOINT_RESPONSE = dict[str, Union[bool, list[str]]]
+class SEND_ENDPOINT_RESPONSE(TypedDict):
+    success: bool
+    message_ids: list[str]

79-88: api_send_url triggers two DeprecationWarnings from a single access

api_send_url warns, then calls base_url which warns again. One access produces two warnings; noisy for users.

Inline the URL build to avoid calling base_url:

 @property
 def api_send_url(self) -> str:
     warnings.warn(
         "api_send_url is deprecated and will be removed in a future release.",
         DeprecationWarning,
         stacklevel=2,
     )
-    url = f"{self.base_url}/api/send"
+    url = f"https://{self._sending_api_netloc}/api/send"
     if self.sandbox and self.inbox_id:
         return f"{url}/{self.inbox_id}"
     return url

10-13: Single source of truth for host constants to avoid drift

You now import BULK_HOST/SANDBOX_HOST/SENDING_HOST from config, but MailtrapClient still exposes public class attributes with the same intent (Lines 23–27). Keeping duplicate literals risks divergence.

Alias class attributes to the config constants and mark them for removal in the next major:

# add near imports
import mailtrap.config as _cfg
 class MailtrapClient:
-    DEFAULT_HOST = "send.api.mailtrap.io"
+    DEFAULT_HOST = _cfg.SENDING_HOST  # DEPRECATED: use mailtrap.config.SENDING_HOST
     DEFAULT_PORT = 443
-    BULK_HOST = "bulk.api.mailtrap.io"
-    SANDBOX_HOST = "sandbox.api.mailtrap.io"
+    BULK_HOST = _cfg.BULK_HOST        # DEPRECATED: use mailtrap.config.BULK_HOST
+    SANDBOX_HOST = _cfg.SANDBOX_HOST  # DEPRECATED: use mailtrap.config.SANDBOX_HOST

Optionally emit a DeprecationWarning on attribute access in a future PR.


101-109: Name suggests a host, but value is used as netloc

_sending_api_host returns just a hostname (no port). Given HttpClient expects a netloc (host[:port]), the name can mislead. Either return a netloc (as suggested) or rename to _sending_api_hostname to avoid ambiguity.

If you adopt _sending_api_netloc, keep _sending_api_host as-is and use the new property wherever a netloc is required.


62-67: Cache the TypeAdapter for SendingMailResponse
Since SendingMailResponse is defined as a @dataclass (mailtrap/models/mail/base.py:24–26), it doesn’t have a built-in model_dump(). Constructing a new TypeAdapter on every call adds unnecessary overhead. You can optimize by creating a single adapter at module scope and reusing it:

• At the top of mailtrap/client.py, add:

from typing import cast
from pydantic import TypeAdapter
from mailtrap.models.mail.base import SendingMailResponse, SEND_ENDPOINT_RESPONSE

_SENDING_MAIL_RESPONSE_ADAPTER = TypeAdapter(SendingMailResponse)

• Then update the send method (lines 62–67) to:

-    def send(self, mail: BaseMail) -> SEND_ENDPOINT_RESPONSE:
-        sending_response = self.sending_api.send(mail)
-        return cast(
-            SEND_ENDPOINT_RESPONSE,
-            TypeAdapter(SendingMailResponse).dump_python(sending_response),
-        )
+    def send(self, mail: BaseMail) -> SEND_ENDPOINT_RESPONSE:
+        sending_response = self.sending_api.send(mail)
+        return cast(
+            SEND_ENDPOINT_RESPONSE,
+            _SENDING_MAIL_RESPONSE_ADAPTER.dump_python(sending_response),
+        )

This change reduces per-call instantiation cost while preserving behavior.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c21e0fc and a7530ae.

📒 Files selected for processing (1)
  • mailtrap/client.py (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#31
File: mailtrap/config.py:1-1
Timestamp: 2025-08-12T23:07:25.653Z
Learning: In Mailtrap's API architecture, Testing API resources (Projects, Inboxes, etc.) use the main "mailtrap.io" host, while only email sending functionality uses "sandbox.api.mailtrap.io" as the host.
🧬 Code graph analysis (1)
mailtrap/client.py (4)
mailtrap/api/sending.py (2)
  • SendingApi (8-22)
  • send (20-22)
mailtrap/exceptions.py (1)
  • ClientConfigurationError (5-7)
mailtrap/http.py (1)
  • HttpClient (13-92)
mailtrap/models/mail/base.py (2)
  • BaseMail (13-20)
  • SendingMailResponse (24-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test python3.10 on windows-latest
  • GitHub Check: Test python3.13 on windows-latest
  • GitHub Check: Test python3.12 on windows-latest
  • GitHub Check: Test python3.9 on windows-latest
  • GitHub Check: Test python3.11 on windows-latest
  • GitHub Check: Test python3.9 on macos-latest
🔇 Additional comments (2)
mailtrap/client.py (2)

71-76: Deprecation policy: good use of DeprecationWarning and stacklevel

The deprecation warning reads clearly and uses the correct category and stacklevel. No action needed.


110-112: Validates Testing API preconditions up front

Requiring account_id before exposing testing_api matches the documented split between Testing API (mailtrap.io) and Sending API (sandbox/bulk). Good guardrail.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
mailtrap/client.py (1)

23-27: Public host aliases retained for backward compatibility — LGTM

Keeping DEFAULT_HOST, BULK_HOST, and SANDBOX_HOST as public class attributes mapped to config constants aligns with the prior decision to avoid breaking importers.

🧹 Nitpick comments (2)
mailtrap/client.py (2)

57-61: Avoid re-creating HttpClient/session on every access; cache SendingApi

Each call builds a new HttpClient (new requests.Session), defeating connection pooling. Cache the instance lazily inside the property to keep the same session across sends.

Apply this diff:

 @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)
+    api = getattr(self, "_sending_api_cached", None)
+    if api is None:
+        http_client = HttpClient(host=self._sending_api_host, headers=self.headers)
+        api = SendingApi(client=http_client, inbox_id=self.inbox_id)
+        setattr(self, "_sending_api_cached", api)
+    return api

Note: If you anticipate token rotation per client instance (unlikely), expose an explicit invalidate/close to rebuild the cached client.


1-4: Strengthen typing of SEND_ENDPOINT_RESPONSE with TypedDict (no runtime change)

Preserves the public “dict” shape while giving precise static types to callers and tests.

Apply these diffs:

-from typing import Optional
-from typing import Union
-from typing import cast
+from typing import Optional
+from typing import Union
+from typing import cast
+from typing import TypedDict
-SEND_ENDPOINT_RESPONSE = dict[str, Union[bool, list[str]]]
+class SendEndpointResponse(TypedDict):
+    success: bool
+    message_ids: list[str]
+
+SEND_ENDPOINT_RESPONSE = SendEndpointResponse

Optional (only if SendingMailResponse is a Pydantic BaseModel): you can simplify dumping in send() to sending_response.model_dump() and drop TypeAdapter for readability without changing output.

Also applies to: 19-19

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a7530ae and 2f2a229.

📒 Files selected for processing (1)
  • mailtrap/client.py (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#33
File: mailtrap/client.py:57-61
Timestamp: 2025-08-21T09:11:51.722Z
Learning: The api_port parameter in MailtrapClient is deprecated and will be removed soon. It's being kept temporarily for backward compatibility but is intentionally not being used in the new sending_api implementation.
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#31
File: mailtrap/config.py:1-1
Timestamp: 2025-08-12T23:07:25.653Z
Learning: In Mailtrap's API architecture, Testing API resources (Projects, Inboxes, etc.) use the main "mailtrap.io" host, while only email sending functionality uses "sandbox.api.mailtrap.io" as the host.
📚 Learning: 2025-08-21T09:11:51.722Z
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#33
File: mailtrap/client.py:57-61
Timestamp: 2025-08-21T09:11:51.722Z
Learning: The api_port parameter in MailtrapClient is deprecated and will be removed soon. It's being kept temporarily for backward compatibility but is intentionally not being used in the new sending_api implementation.

Applied to files:

  • mailtrap/client.py
🧬 Code graph analysis (1)
mailtrap/client.py (4)
mailtrap/api/sending.py (2)
  • SendingApi (8-22)
  • send (20-22)
mailtrap/exceptions.py (1)
  • ClientConfigurationError (5-7)
mailtrap/http.py (1)
  • HttpClient (13-92)
mailtrap/models/mail/base.py (2)
  • BaseMail (13-20)
  • SendingMailResponse (24-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Analyze (actions)
  • GitHub Check: Analyze (python)
  • GitHub Check: Test python3.13 on windows-latest
  • GitHub Check: Test python3.10 on macos-latest
  • GitHub Check: Test python3.12 on windows-latest
  • GitHub Check: Test python3.9 on macos-latest
  • GitHub Check: Test python3.11 on ubuntu-latest
  • GitHub Check: Test python3.10 on windows-latest
  • GitHub Check: Test python3.11 on windows-latest
  • GitHub Check: Test python3.9 on windows-latest
🔇 Additional comments (4)
mailtrap/client.py (4)

62-67: Send flow and backward-compatible dict response — LGTM

Delegating to SendingApi and serializing to the existing dict contract is clean and keeps the external API stable.


101-109: Host resolution logic is correct and aligns with config separation

Selecting SANDBOX_HOST for sandbox, BULK_HOST for bulk, otherwise SENDING_HOST, while allowing api_host override, matches the intended architecture.


110-112: Explicit guard for Testing API account_id — good

Clear configuration error improves DX when using Testing API.


80-88: [runnable_scripts]

#!/bin/bash
# Check parametrization for api_send_url tests to ensure sandbox/inbox_id case is covered.
rg -nC2 "@pytest.mark.parametrize" tests/unit/test_client.py

@Ihor-Bilous Ihor-Bilous merged commit 398ed79 into main Aug 21, 2025
19 checks passed
@Ihor-Bilous Ihor-Bilous deleted the ISSUE-25 branch August 21, 2025 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants