Skip to content

Fix issue #29. Add support of Emails Sandbox (Testing) API: Projects #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

Ihor-Bilous
Copy link

@Ihor-Bilous Ihor-Bilous commented Jul 28, 2025

Motivation

The first PR with base structure and api for projects.

Changes

  • In this PR, I modified the base structure slightly.
  • I created a separate API client for the Projects and Container API classes, which will contain the following API clients: Projects, Attachments, Messages, Inboxes, and TestEmails.
  • I also added an HttpClient that encapsulates all functionality related to requests and responses. HTTP error handling is also included.
  • I didn't change the current functionality for sending emails, but it could be refactored in the same way.
  • I used Pydantic models instead of classes or dataclasses. I think this is a good choice. For mapping responses, dataclasses are sufficient, but we also have "create" and "update" actions where we have user input. With Pydantic, we have type validation by default. It also allows us to easily add new validation.

How to test

In the terminal, run the following command from the root of the project: tox. This command is important because the Mailtrap SDK will be built and installed locally. Then, it will be possible to use Mailtrap in the examples.
In the file examples/testing/projects.py, change the values of the variables API_TOKEN and ACCOUNT_ID, then run the command python examples/testing/projects.py.

Summary by CodeRabbit

  • New Features

    • Client now exposes Testing API access (requires account ID).
    • Projects management: list, retrieve, create, update, delete.
  • New Models

    • Structured models for projects, inboxes, permissions, and deleted objects.
  • Examples

    • Added example demonstrating project CRUD flows via the Testing API.
  • Documentation

    • Raised minimum Python requirement to 3.9+.
  • Tests

    • Added comprehensive unit tests for Projects API and client validation.
  • Chores

    • Added pydantic dependency, unified dependency sourcing, and ignored editor settings.

Copy link

coderabbitai bot commented Jul 28, 2025

Walkthrough

Adds Testing API support: MailtrapClient gains an account_id parameter and a testing_api property that returns a TestingApi backed by a new HttpClient and GENERAL_HOST. Introduces ProjectsApi CRUD endpoints, Pydantic dataclasses, tests, an example script, config/constants, and dependency/tooling updates.

Changes

Cohort / File(s) Summary
Client integration
mailtrap/client.py, tests/unit/test_client.py
MailtrapClient constructor accepts and stores account_id; adds _validate_account_id() and a testing_api @Property that builds an HttpClient (GENERAL_HOST, headers) and returns TestingApi. Added test asserting validation error when account_id missing.
Testing API & Projects
mailtrap/api/testing.py, mailtrap/api/resources/projects.py, tests/unit/api/test_projects.py, examples/testing/projects.py
Adds TestingApi (wraps client, account_id, optional inbox_id) and ProjectsApi with methods: get_list, get_by_id, create, update, delete. Adds comprehensive unit tests and an example script demonstrating CRUD usage.
HTTP client & config
mailtrap/http.py, mailtrap/config.py
New HttpClient class centralizes request building, URL composition, methods (get/post/put/patch/delete), response parsing and error extraction; new constants GENERAL_HOST and DEFAULT_REQUEST_TIMEOUT.
Data models
mailtrap/models/common.py, mailtrap/models/projects.py, mailtrap/models/inboxes.py, mailtrap/models/permissions.py
New Pydantic dataclasses: DeletedObject, ShareLinks, Project, Inbox, and Permissions to model API responses.
Tests utilities
tests/conftest.py
Adds reusable constants and sample JSON payloads for Unauthorized (401), Forbidden (403), and Not Found (404) responses for tests.
Deps, tooling & docs
requirements.txt, requirements.test.txt, pyproject.toml, README.md, .gitignore
Adds pydantic dependency; test requirements include base requirements.txt; pyproject now sources dependencies dynamically from requirements.txt; README minimum Python bumped to 3.9; .vscode/ added to .gitignore.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MailtrapClient
  participant TestingApi
  participant ProjectsApi
  participant HttpClient
  participant MailtrapAPI

  User->>MailtrapClient: instantiate(token, account_id)
  User->>MailtrapClient: access .testing_api
  MailtrapClient->>MailtrapClient: _validate_account_id()
  MailtrapClient->>HttpClient: create(host=GENERAL_HOST, headers)
  MailtrapClient->>TestingApi: TestingApi(client, account_id, inbox_id?)
  User->>TestingApi: .projects
  TestingApi->>ProjectsApi: instantiate(client, account_id)
  User->>ProjectsApi: get_list() / create(...) / update(...) / delete(...)
  ProjectsApi->>HttpClient: HTTP request to /api/accounts/{account_id}/projects...
  HttpClient->>MailtrapAPI: send HTTP request
  MailtrapAPI-->>HttpClient: JSON response or error
  HttpClient-->>ProjectsApi: parsed data or raise APIError
  ProjectsApi-->>User: Project(s) / DeletedObject
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I twitch my whiskers at a testing trail,
account_id snug like a tiny tail.
I hop through projects, GET and POST,
patch and delete — I do the most.
A rabbit cheers: API hops boast.


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between ecae537 and 5c3f0e0.

📒 Files selected for processing (3)
  • examples/testing/projects.py (1 hunks)
  • mailtrap/api/resources/projects.py (1 hunks)
  • mailtrap/api/testing.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/testing/projects.py
  • mailtrap/api/testing.py
  • mailtrap/api/resources/projects.py
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 5

🧹 Nitpick comments (4)
mailtrap/models/common.py (1)

1-3: Consider enhancing the class for better maintainability.

The implementation is functional but could benefit from some optional improvements:

+from dataclasses import dataclass
+
+@dataclass
-class DeletedObject:
-    def __init__(self, id: str):
-        self.id = id
+class DeletedObject:
+    """Represents a deleted object with its identifier."""
+    id: str

Alternatively, if you prefer to keep the current approach, consider adding a __repr__ method for better debugging:

 class DeletedObject:
+    """Represents a deleted object with its identifier."""
+    
     def __init__(self, id: str):
         self.id = id
+    
+    def __repr__(self) -> str:
+        return f"DeletedObject(id='{self.id}')"
tests/unit/api/test_projects.py (1)

100-116: Consider validating the request payload.

The test correctly validates the response, but could also verify that the correct request payload is sent.

     @responses.activate
     def test_create_should_return_new_project(
         self,
         client: ProjectsApiClient, 
         sample_project_dict: Dict
     ) -> None:
         responses.add(
             responses.POST,
             BASE_URL,
             json=sample_project_dict,
             status=201,
         )

         project = client.create(ACCOUNT_ID, name="New Project")

         assert isinstance(project, Project)
         assert project.name == "Test Project"
+        # Verify the request payload
+        assert len(responses.calls) == 1
+        request_json = responses.calls[0].request.json()
+        assert request_json == {"project": {"name": "New Project"}}
mailtrap/models/projects.py (1)

1-50: Consider adding docstrings and repr methods for better maintainability.

While the implementation is solid, consider these optional improvements:

 class ShareLinks:
+    """Links for sharing project access."""
+    
     def __init__(self, admin: str, viewer: str):
         self.admin = admin
         self.viewer = viewer
+    
+    def __repr__(self) -> str:
+        return f"ShareLinks(admin='{self.admin}', viewer='{self.viewer}')"


 class Permissions:
+    """Permission flags for project access control."""
+    
     def __init__(
         self, 
         can_read: bool, 
         can_update: bool, 
         can_destroy: bool, 
         can_leave: bool
     ):
         self.can_read = can_read
         self.can_update = can_update
         self.can_destroy = can_destroy
         self.can_leave = can_leave
+    
+    def __repr__(self) -> str:
+        return (f"Permissions(can_read={self.can_read}, can_update={self.can_update}, "
+                f"can_destroy={self.can_destroy}, can_leave={self.can_leave})")


 class Project:
+    """Represents a Mailtrap project with its metadata and permissions."""
+    
     def __init__(
         self, 
         id: str, 
         name: str, 
         share_links: ShareLinks, 
         inboxes: List[Dict[str, Any]], 
         permissions: Permissions
     ):
         self.id = id
         self.name = name
         self.share_links = share_links
         self.inboxes = inboxes
         self.permissions = permissions
+    
+    def __repr__(self) -> str:
+        return f"Project(id='{self.id}', name='{self.name}')"
mailtrap/api/base.py (1)

44-46: Consider adding abstract methods to justify ABC inheritance.

The class inherits from ABC but has no abstract methods, which static analysis correctly flags. Consider whether this class should define abstract methods that subclasses must implement, or if ABC inheritance is necessary.

If you want to keep the ABC inheritance to enforce the pattern, consider adding an abstract method like:

from abc import ABC, abstractmethod

class BaseHttpApiClient(ABC):
    def __init__(self, session: Session):
        self.session = session
    
    @abstractmethod
    def _build_url(self, *args: Any) -> str:
        """Build API endpoint URL. Must be implemented by subclasses."""
        pass

Alternatively, if no abstract interface is needed, simply remove the ABC inheritance.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 709f096 and 16ac0ab.

📒 Files selected for processing (7)
  • mailtrap/api/base.py (1 hunks)
  • mailtrap/api/projects.py (1 hunks)
  • mailtrap/client.py (2 hunks)
  • mailtrap/constants.py (1 hunks)
  • mailtrap/models/common.py (1 hunks)
  • mailtrap/models/projects.py (1 hunks)
  • tests/unit/api/test_projects.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
mailtrap/client.py (3)
mailtrap/api/projects.py (2)
  • ProjectsApiClient (9-50)
  • update (37-43)
mailtrap/mail/base.py (2)
  • BaseMail (11-53)
  • api_data (33-44)
mailtrap/mail/base_entity.py (1)
  • api_data (9-10)
tests/unit/api/test_projects.py (4)
mailtrap/api/projects.py (6)
  • ProjectsApiClient (9-50)
  • get_list (15-20)
  • get_by_id (22-27)
  • create (29-35)
  • update (37-43)
  • delete (45-50)
mailtrap/exceptions.py (1)
  • APIError (10-15)
mailtrap/models/common.py (1)
  • DeletedObject (1-3)
mailtrap/models/projects.py (1)
  • Project (24-50)
mailtrap/api/base.py (2)
mailtrap/exceptions.py (2)
  • APIError (10-15)
  • AuthorizationError (18-20)
mailtrap/client.py (1)
  • _handle_failed_response (87-94)
mailtrap/api/projects.py (3)
mailtrap/api/base.py (3)
  • BaseHttpApiClient (44-70)
  • HttpMethod (12-17)
  • _request (48-54)
mailtrap/models/common.py (1)
  • DeletedObject (1-3)
mailtrap/models/projects.py (2)
  • Project (24-50)
  • from_dict (40-50)
🪛 Ruff (0.12.2)
mailtrap/api/base.py

44-44: BaseHttpApiClient is an abstract base class, but it has no abstract methods or properties

(B024)


63-63: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (23)
mailtrap/constants.py (1)

1-1: LGTM! Clean constant definition.

The constant is well-named and provides a centralized location for the Mailtrap host configuration.

mailtrap/client.py (4)

7-7: LGTM! Clean integration of the Projects API.

The import follows the existing pattern and properly integrates the new functionality.


38-39: Excellent improvement with persistent HTTP session.

Creating a persistent requests.Session and pre-configuring headers is a great optimization that will:

  • Enable connection pooling and reuse
  • Reduce overhead from repeated header configuration
  • Improve overall performance for multiple API calls

41-43: Clean property implementation for Projects API access.

The property provides a clean interface to access project-related functionality while sharing the same HTTP session for consistency.


46-46: Good use of the persistent session.

Using the session's post method instead of the standalone requests.post leverages the connection pooling and pre-configured headers effectively.

tests/unit/api/test_projects.py (4)

17-19: LGTM! Good fixture setup.

Clean fixture providing a client instance for testing.


22-38: Excellent comprehensive test data fixture.

The sample project dictionary covers all the required fields and provides realistic test data that matches the Project model structure.


41-58: Thorough test for get_list functionality.

Good coverage testing both the list structure and individual Project instances.


61-77: Good error handling test.

Properly tests the 404 error scenario and validates exception handling.

mailtrap/models/projects.py (5)

1-1: LGTM! Correct Python 3.6 compatible type annotations.

Good use of List[Dict[str, Any]] instead of list[dict[str, Any]] for Python 3.6 compatibility.


4-7: LGTM! Clean data model.

Simple and focused class for share links data.


10-21: Well-structured permissions model.

Good separation of permission flags with clear naming.


24-37: Solid project model structure.

Clean data model with appropriate type hints and clear attribute organization.


39-50: Excellent deserialization method.

The from_dict method properly handles nested object creation and provides sensible defaults.

mailtrap/api/projects.py (6)

1-6: LGTM! Clean imports and proper typing.

The imports are well-organized and include all necessary components. The use of cast from typing module is appropriate for the type casting needs in this implementation.


9-13: LGTM! Well-designed URL building method.

The _build_url helper method properly constructs API URLs with account scoping. The use of *parts parameter allows for flexible URL construction for different endpoints.


15-20: LGTM! Proper implementation of list retrieval.

The method correctly uses type casting and list comprehension to convert the API response into Project objects. The explicit casting helps with type safety.


22-27: LGTM! Clean single project retrieval.

The method follows the same pattern as get_list with proper type casting and model conversion.


29-35: LGTM! Proper project creation with nested JSON structure.

The method correctly structures the request payload with the nested {"project": {"name": name}} format as expected by the API.


37-43: LGTM! Consistent update implementation.

The update method follows the same pattern as create, using PATCH method appropriately for partial updates.

mailtrap/api/base.py (3)

1-9: LGTM! Proper imports and type definitions.

The imports include all necessary components and the type aliases provide clear documentation for expected response formats.


20-41: LGTM! Robust error extraction logic.

The _extract_errors function handles multiple error response formats gracefully:

  • List of errors
  • Dictionary of field-specific errors
  • Single error values
  • Fallback for unknown formats

This provides good flexibility for different API endpoint error structures.


48-54: LGTM! Well-designed request method with proper type handling.

The _request method provides a clean abstraction for HTTP requests with proper type casting and error handling delegation.

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: 0

♻️ Duplicate comments (1)
mailtrap/api/base.py (1)

60-63: Fix exception chaining for better error diagnostics.

The ValueError caught during JSON parsing should be chained to preserve the original error context for debugging.

Apply this fix:

         try:
             data = response.json()
-        except ValueError:
-            raise APIError(status_code, errors=["Unknown Error"])
+        except ValueError as e:
+            raise APIError(status_code, errors=["Unknown Error"]) from e
🧹 Nitpick comments (1)
mailtrap/api/base.py (1)

44-46: Consider the abstract base class design.

The class inherits from ABC but doesn't define any abstract methods. If this is intended to be a true abstract base class, consider adding abstract methods that subclasses must implement. Otherwise, regular inheritance without ABC might be more appropriate.

If you want to enforce an interface, consider adding abstract methods:

 class BaseHttpApiClient(ABC):
     def __init__(self, session: Session):
         self.session = session
+
+    @abstractmethod
+    def get_base_url(self) -> str:
+        """Return the base URL for this API client."""
+        pass

Or remove ABC inheritance if not needed:

-class BaseHttpApiClient(ABC):
+class BaseHttpApiClient:
     def __init__(self, session: Session):
         self.session = session
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 16ac0ab and 994cacf.

📒 Files selected for processing (2)
  • mailtrap/api/base.py (1 hunks)
  • mailtrap/api/projects.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mailtrap/api/projects.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
mailtrap/api/base.py (2)
mailtrap/exceptions.py (2)
  • APIError (10-15)
  • AuthorizationError (18-20)
mailtrap/client.py (1)
  • _handle_failed_response (87-94)
🪛 Ruff (0.12.2)
mailtrap/api/base.py

44-44: BaseHttpApiClient is an abstract base class, but it has no abstract methods or properties

(B024)


63-63: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (4)
mailtrap/api/base.py (4)

1-10: LGTM!

The imports are well-organized and the type aliases provide clear documentation for the expected response formats.


12-17: LGTM!

The HttpMethod enum is correctly implemented with all standard HTTP methods properly defined.


20-41: Excellent error handling flexibility.

This function effectively handles various error response formats that different API endpoints might return, with appropriate fallbacks for edge cases. The flattening of nested error dictionaries provides clear, actionable error messages.


48-54: Solid request method design.

The method provides a clean interface for making HTTP requests with proper error handling delegation. The use of cast() for type checking is reasonable given the shared method design, though future refinements to typing could be considered as mentioned in the PR description.

@yanchuk yanchuk added the enhancement New feature or request label Jul 28, 2025
@yanchuk yanchuk linked an issue Jul 28, 2025 that may be closed by this pull request
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: 10

♻️ Duplicate comments (2)
mailtrap/models/projects.py (1)

1-20: Type annotation consistency issue already identified

A previous review has already identified the need to unify type annotations for Python 3.6 compatibility. The use of list[Inbox] on line 19 should be changed to List[Inbox] with appropriate imports.

tests/unit/api/test_projects.py (1)

159-172: Verify the HTTP method enum typo is fixed

A previous review identified that the implementation uses HttpMethod.DELTE instead of HttpMethod.DELETE. This test expects the correct DELETE method to work.

🧹 Nitpick comments (5)
examples/testing/projects.py (2)

1-3: Avoid sys.path manipulation in examples

Modifying sys.path directly is not a recommended practice. Consider using relative imports or running the example as a module.

Instead of modifying sys.path, run the example using:

python -m examples.testing.projects

Or use a more robust path resolution:

from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))

20-30: Add error handling to the example

The example doesn't demonstrate error handling, which would be helpful for users to understand how to handle API failures.

Consider wrapping API calls in try-except blocks:

try:
    created_project = api.create(project_name=project_name)
    print(f"Created project: {created_project.name}")
    
    projects = api.get_list()
    print(f"Found {len(projects)} projects")
    
    # ... rest of the operations
except APIError as e:
    print(f"API error occurred: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
tests/unit/api/test_projects.py (1)

45-45: Fix method signature for consistency

The parameter type annotation is missing the import.

-def test_get_list_should_return_project_list(
-    self, client: ProjectsApi, sample_project_dict: dict
-) -> None:
+def test_get_list_should_return_project_list(
+    self, client: ProjectsApi, sample_project_dict: Dict[str, Any]
+) -> None:

Apply similar changes to other test methods that use the sample_project_dict parameter.

mailtrap/http.py (2)

50-52: Enhance error message with actual response type

The error message could be more helpful by including what type was actually received.

         if not isinstance(data, expected_type):
-            raise APIError(response.status_code, errors=[f"Expected response type {expected_type.__name__}"])
+            raise APIError(
+                response.status_code, 
+                errors=[f"Expected response type {expected_type.__name__}, got {type(data).__name__}"]
+            )

105-120: Consider extracting nested function for better testability

The flatten_errors function could be extracted as a module-level function to improve testability and reusability.

-def _extract_errors(data: dict[str, Any]) -> list[str]:    
-    def flatten_errors(errors: Any) -> list[str]:
-        if isinstance(errors, list):
-            return [str(error) for error in errors]
-        
-        if isinstance(errors, dict):
-            flat_errors = []
-            for key, value in errors.items():
-                if isinstance(value, list):
-                    flat_errors.extend([f"{key}: {v}" for v in value])
-                else:
-                    flat_errors.append(f"{key}: {value}")
-            return flat_errors
-
-        return [str(errors)]
+def _flatten_errors(errors: Any) -> list[str]:
+    if isinstance(errors, list):
+        return [str(error) for error in errors]
+    
+    if isinstance(errors, dict):
+        flat_errors = []
+        for key, value in errors.items():
+            if isinstance(value, list):
+                flat_errors.extend([f"{key}: {v}" for v in value])
+            else:
+                flat_errors.append(f"{key}: {value}")
+        return flat_errors
+
+    return [str(errors)]
+
+
+def _extract_errors(data: dict[str, Any]) -> list[str]:

Then update the calls within the function:

     if "errors" in data:
-        return flatten_errors(data["errors"])
+        return _flatten_errors(data["errors"])
     
     if "error" in data:
-        return flatten_errors(data["error"])
+        return _flatten_errors(data["error"])
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 994cacf and 3723d35.

📒 Files selected for processing (11)
  • examples/testing/projects.py (1 hunks)
  • mailtrap/api/resources/projects.py (1 hunks)
  • mailtrap/api/testing.py (1 hunks)
  • mailtrap/client.py (2 hunks)
  • mailtrap/config.py (1 hunks)
  • mailtrap/http.py (1 hunks)
  • mailtrap/models/base.py (1 hunks)
  • mailtrap/models/inboxes.py (1 hunks)
  • mailtrap/models/permissions.py (1 hunks)
  • mailtrap/models/projects.py (1 hunks)
  • tests/unit/api/test_projects.py (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • mailtrap/config.py
  • mailtrap/models/permissions.py
  • mailtrap/api/testing.py
  • mailtrap/models/inboxes.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • mailtrap/client.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
mailtrap/api/resources/projects.py (5)
mailtrap/http.py (5)
  • HttpClient (13-102)
  • get (60-66)
  • post (76-82)
  • patch (92-98)
  • delete (100-102)
mailtrap/models/base.py (1)
  • DeletedObject (41-42)
mailtrap/api/testing.py (1)
  • projects (12-13)
mailtrap/models/projects.py (1)
  • Project (15-20)
tests/unit/api/test_projects.py (1)
  • client (19-20)
mailtrap/models/projects.py (3)
mailtrap/models/base.py (1)
  • BaseModel (9-36)
mailtrap/models/inboxes.py (1)
  • Inbox (8-31)
mailtrap/models/permissions.py (1)
  • Permissions (7-11)
mailtrap/http.py (3)
mailtrap/exceptions.py (2)
  • APIError (10-15)
  • AuthorizationError (18-20)
mailtrap/client.py (2)
  • headers (74-81)
  • _host (84-91)
mailtrap/api/resources/projects.py (2)
  • update (26-31)
  • delete (33-37)
🪛 Ruff (0.12.2)
mailtrap/http.py

33-33: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

@dataclass
class BaseModel:
@classmethod
def from_dict(cls: Type[T], data: dict[str, Any]) -> T:
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type annotation incompatible with Python 3.6

The method signature uses dict[str, Any] which requires Python 3.9+. For Python 3.6 compatibility, use Dict[str, Any] from the typing module.

Apply this diff to fix the compatibility issue:

-from typing import Any, Type, TypeVar, get_args, get_origin
+from typing import Any, Dict, Type, TypeVar, get_args, get_origin
-    def from_dict(cls: Type[T], data: dict[str, Any]) -> T:
+    def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
-        values: dict[str, Any] = {}
+        values: Dict[str, Any] = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def from_dict(cls: Type[T], data: dict[str, Any]) -> T:
from typing import Any, Dict, Type, TypeVar, get_args, get_origin
@classmethod
def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
values: Dict[str, Any] = {}
# …rest of implementation…
🤖 Prompt for AI Agents
In mailtrap/models/base.py at line 11, the type annotation dict[str, Any] is
incompatible with Python 3.6. Replace dict[str, Any] with Dict[str, Any] and
import Dict from the typing module to ensure compatibility with Python 3.6.

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

♻️ Duplicate comments (2)
tests/unit/api/test_projects.py (1)

1-1: Type annotations incompatible with Python 3.6

The code uses dict[str, Any] and list[str] which require Python 3.9+. For Python 3.6 compatibility, import and use Dict and List from typing.

-from typing import Any
+from typing import Any, Dict, List
-def sample_project_dict() -> dict[str, Any]:
+def sample_project_dict() -> Dict[str, Any]:
-    def test_create_should_raise_validation_error_on_pydantic_validation(
-        self, client: ProjectsApi, project_name: str, expected_errors: list[str]
-    ) -> None:
+    def test_create_should_raise_validation_error_on_pydantic_validation(
+        self, client: ProjectsApi, project_name: str, expected_errors: List[str]
+    ) -> None:

Also applies to line 238 where list[str] is used again.

Also applies to: 25-25, 172-172

mailtrap/http.py (1)

1-4: Type annotations incompatible with Python 3.6

The code uses lowercase generic type annotations which require Python 3.9+. According to the PR objectives, the SDK should support Python 3.6.

Also applies to: 20-20

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

22-43: Consider thread safety and multi-tenant implications of class-level token.

The class-level _default_token is shared across all instances and threads. This design has implications:

  • Not thread-safe if tokens need to be changed at runtime
  • Prevents using different tokens for different client instances in the same process
  • Could cause issues in multi-tenant applications

Consider either:

  1. Adding thread-safe token management (e.g., using threading.local())
  2. Supporting both class-level default and instance-level override tokens
  3. Documenting the single-token limitation clearly
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3723d35 and efa0737.

📒 Files selected for processing (15)
  • .gitignore (1 hunks)
  • README.md (2 hunks)
  • examples/testing/projects.py (1 hunks)
  • mailtrap/api/resources/projects.py (1 hunks)
  • mailtrap/api/testing.py (1 hunks)
  • mailtrap/client.py (4 hunks)
  • mailtrap/http.py (1 hunks)
  • mailtrap/schemas/base.py (1 hunks)
  • mailtrap/schemas/inboxes.py (1 hunks)
  • mailtrap/schemas/permissions.py (1 hunks)
  • mailtrap/schemas/projects.py (1 hunks)
  • requirements.test.txt (1 hunks)
  • requirements.txt (1 hunks)
  • tests/unit/api/test_projects.py (1 hunks)
  • tests/unit/test_client.py (3 hunks)
✅ Files skipped from review due to trivial changes (6)
  • .gitignore
  • README.md
  • mailtrap/schemas/base.py
  • requirements.txt
  • requirements.test.txt
  • mailtrap/schemas/inboxes.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • mailtrap/api/testing.py
  • mailtrap/api/resources/projects.py
  • examples/testing/projects.py
🔇 Additional comments (6)
mailtrap/schemas/permissions.py (1)

1-8: LGTM!

Clean and straightforward Pydantic model for permissions. The boolean fields appropriately represent the permission flags.

tests/unit/test_client.py (2)

29-32: Test helper correctly updated for class-level token management.

The modification to use configure_access_token class method instead of passing token as instance parameter aligns with the refactored client design.


87-93: Header access correctly updated to use class method.

The tests now properly use get_default_headers() class method instead of accessing instance headers attribute, consistent with the new token management approach.

Also applies to: 107-108

mailtrap/schemas/projects.py (1)

1-23: Well-structured Pydantic models with appropriate validation.

The schema definitions are clean and follow best practices:

  • Proper use of nested models for complex data structures
  • Appropriate field validation constraints on ProjectInput.name
  • Clear separation between input validation (ProjectInput) and data representation (Project)
tests/unit/api/test_projects.py (1)

43-312: Comprehensive test coverage with excellent structure.

The test implementation demonstrates best practices:

  • Thorough coverage of all CRUD operations
  • Proper error handling tests for various HTTP status codes
  • Good use of parametrized tests to reduce code duplication
  • Validation error testing for edge cases
  • Clean fixture usage
mailtrap/client.py (1)

44-50: Clean implementation of testing API factory method.

The get_testing_api method provides a convenient way to create TestingApi instances with proper configuration and shared HTTP client.

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

🔭 Outside diff range comments (1)
mailtrap/__init__.py (1)

1-14: Expose public symbols via __all__ to silence unused-import warnings

Imports at module level are meant for re-export, but Ruff still flags them unless they’re listed in __all__. Add/extend the tuple so static analysis tools understand the intent.

-from .client import MailtrapApiClient
-from .client import MailtrapClient
+from .client import MailtrapApiClient, MailtrapClient
+
+# Public interface
+__all__ = (
+    "MailtrapApiClient",
+    "MailtrapClient",
+    "APIError",
+    "AuthorizationError",
+    "ClientConfigurationError",
+    "MailtrapError",
+    "Address",
+    "Attachment",
+    "BaseEntity",
+    "BaseMail",
+    "Disposition",
+    "Mail",
+    "MailFromTemplate",
+)
🧹 Nitpick comments (1)
mailtrap/models/projects.py (1)

13-18: Consider validating share-link URLs

ShareLinks.admin and .viewer contain URLs. Declaring them as HttpUrl (or AnyUrl) gives automatic validation and clearer intent.

-class ShareLinks(BaseModel):
-    admin: str
-    viewer: str
+from pydantic import HttpUrl
+
+
+class ShareLinks(BaseModel):
+    admin: HttpUrl
+    viewer: HttpUrl
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7bb3725 and 1f3ff3a.

📒 Files selected for processing (13)
  • examples/testing/projects.py (1 hunks)
  • mailtrap/__init__.py (1 hunks)
  • mailtrap/api/resources/projects.py (1 hunks)
  • mailtrap/client.py (2 hunks)
  • mailtrap/config.py (1 hunks)
  • mailtrap/http.py (1 hunks)
  • mailtrap/models/base.py (1 hunks)
  • mailtrap/models/inboxes.py (1 hunks)
  • mailtrap/models/permissions.py (1 hunks)
  • mailtrap/models/projects.py (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/unit/api/test_projects.py (1 hunks)
  • tests/unit/test_client.py (1 hunks)
✅ Files skipped from review due to trivial changes (5)
  • mailtrap/models/base.py
  • mailtrap/config.py
  • mailtrap/models/permissions.py
  • pyproject.toml
  • mailtrap/models/inboxes.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • mailtrap/client.py
  • examples/testing/projects.py
  • tests/unit/test_client.py
  • mailtrap/api/resources/projects.py
  • mailtrap/http.py
  • tests/unit/api/test_projects.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
mailtrap/models/projects.py (2)
mailtrap/models/inboxes.py (1)
  • Inbox (8-33)
mailtrap/models/permissions.py (1)
  • Permissions (4-8)
🪛 Ruff (0.12.2)
mailtrap/__init__.py

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

(F401)

mailtrap/models/projects.py

2-2: pydantic.Field imported but unused

Remove unused import: pydantic.Field

(F401)

Copy link
Contributor

@andrii-porokhnavets andrii-porokhnavets left a comment

Choose a reason for hiding this comment

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

Good job. I like the new design. I left several comments, let's fix and will be good to go

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

♻️ Duplicate comments (1)
tests/unit/api/test_projects.py (1)

1-1: Use typing.Dict for Python <3.9 compatibility (repeat of prior feedback)

dict[str, Any] requires Python 3.9+. Prior review already flagged this; switch to Dict[str, Any] and import Dict.

Apply this diff:

-from typing import Any
+from typing import Any, Dict
@@
-@pytest.fixture
-def sample_project_dict() -> dict[str, Any]:
+@pytest.fixture
+def sample_project_dict() -> Dict[str, Any]:

Also applies to: 25-25

🧹 Nitpick comments (7)
mailtrap/config.py (3)

1-3: Add typing.Final and explicit exports for stability and clarity

Marking these as Final prevents accidental reassignment and clarifies intent. Exporting via all makes the public surface explicit.

Apply this diff:

+from typing import Final
+
-GENERAL_HOST = "mailtrap.io"
+GENERAL_HOST: Final[str] = "mailtrap.io"
 
-DEFAULT_REQUEST_TIMEOUT = 30  # in seconds
+DEFAULT_REQUEST_TIMEOUT: Final[float] = 30.0  # seconds
+
+__all__ = ("GENERAL_HOST", "DEFAULT_REQUEST_TIMEOUT")

3-3: Optionally split timeout into connect/read components for finer control

Requests supports separate connect/read timeouts. You can introduce these now (without changing current behavior) and adopt them in HttpClient later.

Apply this diff to add granular timeouts:

 DEFAULT_REQUEST_TIMEOUT: Final[float] = 30.0  # seconds
+
+# Optional granular timeouts (unused for now; can be adopted in HttpClient later)
+DEFAULT_CONNECT_TIMEOUT: Final[float] = 5.0   # seconds
+DEFAULT_READ_TIMEOUT: Final[float] = 30.0     # seconds

1-1: Naming consistency nit: GENERAL_HOST vs DEFAULT_*

Consider aligning naming with DEFAULT_* pattern (e.g., DEFAULT_HOST or BASE_DOMAIN) for consistency with DEFAULT_REQUEST_TIMEOUT. Only if it doesn’t cause churn elsewhere.

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

31-33: Avoid hardcoding account ID in URLs; use the ACCOUNT_ID constant

Reduces drift if ACCOUNT_ID changes and keeps fixtures consistent.

Apply this diff:

-        "share_links": {
-            "admin": "https://mailtrap.io/projects/321/admin",
-            "viewer": "https://mailtrap.io/projects/321/viewer",
-        },
+        "share_links": {
+            "admin": f"https://mailtrap.io/projects/{ACCOUNT_ID}/admin",
+            "viewer": f"https://mailtrap.io/projects/{ACCOUNT_ID}/viewer",
+        },

80-83: Optional: tighten parameter types in tests to Dict[str, Any]

For consistency with the fixture and to retain Python <3.9 compatibility, annotate test parameters expecting the dict payload as Dict[str, Any].

Apply this diff:

-    def test_get_list_should_return_project_list(
-        self, client: ProjectsApi, sample_project_dict: dict
-    ) -> None:
+    def test_get_list_should_return_project_list(
+        self, client: ProjectsApi, sample_project_dict: Dict[str, Any]
+    ) -> None:
@@
-    def test_get_by_id_should_return_single_project(
-        self, client: ProjectsApi, sample_project_dict: dict
-    ) -> None:
+    def test_get_by_id_should_return_single_project(
+        self, client: ProjectsApi, sample_project_dict: Dict[str, Any]
+    ) -> None:
@@
-    def test_create_should_return_new_project(
-        self, client: ProjectsApi, sample_project_dict: dict
-    ) -> None:
+    def test_create_should_return_new_project(
+        self, client: ProjectsApi, sample_project_dict: Dict[str, Any]
+    ) -> None:
@@
-    def test_update_should_return_updated_project(
-        self, client: ProjectsApi, sample_project_dict: dict
-    ) -> None:
+    def test_update_should_return_updated_project(
+        self, client: ProjectsApi, sample_project_dict: Dict[str, Any]
+    ) -> None:

Also applies to: 136-139, 186-189, 242-246

mailtrap/models/inboxes.py (2)

1-1: Confirm supported Python versions; adjust typing for compatibility if needed

Using built-in generics list[int] requires Python 3.9+. If the library supports Python 3.8 or older, switch to List[int] from typing.

If Python >= 3.9 is required, ignore. Otherwise apply:

-from typing import Optional
+from typing import Optional, List
@@
-    smtp_ports: list[int]
-    pop3_ports: list[int]
+    smtp_ports: List[int]
+    pop3_ports: List[int]

Also applies to: 27-28


1-1: Model timestamps as datetime for better type-safety

last_message_sent_at is a timestamp; using datetime improves validation and consumer ergonomics. Pydantic will parse ISO8601 strings automatically.

Apply this diff:

-from typing import Optional
+from typing import Optional
+from datetime import datetime
@@
-    last_message_sent_at: Optional[str] = None
+    last_message_sent_at: Optional[datetime] = None

Also applies to: 34-34

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 92c788a and 57a5452.

📒 Files selected for processing (10)
  • examples/testing/projects.py (1 hunks)
  • mailtrap/client.py (3 hunks)
  • mailtrap/config.py (1 hunks)
  • mailtrap/models/common.py (1 hunks)
  • mailtrap/models/inboxes.py (1 hunks)
  • mailtrap/models/permissions.py (1 hunks)
  • mailtrap/models/projects.py (1 hunks)
  • pyproject.toml (2 hunks)
  • tests/unit/api/test_projects.py (1 hunks)
  • tests/unit/test_client.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • mailtrap/models/common.py
  • pyproject.toml
  • mailtrap/models/projects.py
  • mailtrap/client.py
  • examples/testing/projects.py
  • mailtrap/models/permissions.py
🧰 Additional context used
🧬 Code Graph Analysis (3)
tests/unit/api/test_projects.py (6)
mailtrap/api/testing.py (1)
  • projects (16-17)
mailtrap/api/resources/projects.py (6)
  • ProjectsApi (6-39)
  • get_list (11-13)
  • get_by_id (15-19)
  • create (21-26)
  • update (28-33)
  • delete (35-39)
mailtrap/exceptions.py (1)
  • APIError (10-15)
mailtrap/http.py (5)
  • HttpClient (13-92)
  • get (25-29)
  • post (31-33)
  • patch (39-41)
  • delete (43-45)
mailtrap/models/common.py (1)
  • DeletedObject (5-6)
mailtrap/models/projects.py (1)
  • Project (16-21)
tests/unit/test_client.py (3)
tests/unit/api/test_projects.py (1)
  • client (20-21)
mailtrap/exceptions.py (1)
  • ClientConfigurationError (5-7)
mailtrap/client.py (1)
  • testing_api (44-50)
mailtrap/models/inboxes.py (1)
mailtrap/models/permissions.py (1)
  • Permissions (5-9)
🪛 Ruff (0.12.2)
tests/unit/test_client.py

48-48: Found useless expression. Either assign it to a variable or remove it.

(B018)

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

1-3: LGTM: Centralized host and timeout constants

Simple, minimal, and useful abstraction that other modules can import. Good starting point for config centralization.

tests/unit/api/test_projects.py (1)

43-313: LGTM: Comprehensive CRUD and error-path coverage for Projects API

The tests thoroughly cover success and common error scenarios across list/get/create/update/delete, validating response mapping to models and error propagation from HttpClient.

@VladimirTaytor VladimirTaytor self-requested a review August 13, 2025 14:06
Copy link
Contributor

@VladimirTaytor VladimirTaytor left a comment

Choose a reason for hiding this comment

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

Looks good, thanks for researching this approach with pydantic.dataclasses 💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support of Emails Sandbox (Testing) API: Projects
4 participants