Skip to content

Conversation

Ihor-Bilous
Copy link
Collaborator

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

Motivation

In this PR I added ContactListsApi.

Changes

  • Added ContactListsApi, models, test, examples
  • Updated MailtrapClient

How to test

  • see examples/contacts/contact_lists.py

Images and GIFs

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

Summary by CodeRabbit

  • New Features
    • Added contact list management (create, retrieve, update, delete) and new ContactList / ContactListParams models; ContactListParams now available at package level.
  • Documentation
    • Added an example demonstrating contact list CRUD with the Python SDK.
  • Tests
    • Added comprehensive unit tests covering contact list operations (success and error cases).
  • Bug Fixes
    • Fixed endpoint path handling so an ID of 0 is treated as a valid resource ID.

Copy link

coderabbitai bot commented Aug 25, 2025

Walkthrough

Adds Contact Lists: new models, a ContactListsApi resource with full CRUD, a ContactsBaseApi accessor, package re-export of ContactListParams, an examples script, and unit tests covering success and error cases for /api/accounts/{account_id}/contacts/lists endpoints.

Changes

Cohort / File(s) Summary
API: Contact Lists Resource
mailtrap/api/resources/contact_lists.py
New ContactListsApi class with CRUD methods (get_list, get_by_id, create, update, delete) and _api_path helper, using ContactList, ContactListParams, and DeletedObject.
API: Contacts Base Extension
mailtrap/api/contacts.py
Adds contact_lists property on ContactsBaseApi returning a ContactListsApi instance wired with the existing HttpClient and account_id.
Models: Contacts
mailtrap/models/contacts.py
Adds dataclasses ContactListParams(RequestParams) (name: str) and ContactList (id: int, name: str).
Package Exports
mailtrap/__init__.py
Re-exports ContactListParams from mailtrap.models.contacts.
Examples
examples/contacts/contact_lists.py
New example script with API_TOKEN/ACCOUNT_ID placeholders; initializes MailtrapClient and exposes helper functions for contact list CRUD; prints list_contact_lists() when run.
Tests: API Contact Lists
tests/unit/api/test_contact_lists.py
Unit tests for all ContactListsApi methods using mocked HTTP responses (success and various 4xx errors), validating models and APIError propagation.
Tests: Models Contacts
tests/unit/models/test_contacts.py
Adds test for ContactListParams.api_data equality to {"name": "..."}.
Minor API Path Fix
mailtrap/api/resources/contact_fields.py
Changed conditional to if field_id is not None: so 0 is treated as a valid ID when building paths.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant MC as MailtrapClient
  participant CBA as ContactsBaseApi
  participant CLA as ContactListsApi
  participant HTTP as HttpClient
  participant API as Mailtrap REST API

  Dev->>MC: instantiate(client_config)
  Dev->>CBA: mc.contacts(account_id)
  CBA-->>Dev: ContactsBaseApi
  Dev->>CLA: contacts.contact_lists (accessor)
  CLA-->>Dev: ContactListsApi

  rect rgba(200,230,255,0.25)
    Dev->>CLA: create/list/get/update/delete(...)
    CLA->>HTTP: HTTP request to /api/accounts/{account_id}/contacts/lists[/id]
    HTTP->>API: network call
    API-->>HTTP: JSON response / status
    HTTP-->>CLA: parsed response
    CLA-->>Dev: ContactList / list / DeletedObject
  end

  alt Error (4xx)
    API-->>HTTP: error payload
    HTTP-->>CLA: raises APIError
    CLA-->>Dev: APIError (propagated)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • VladimirTaytor
  • IgorDobryn
  • andrii-porokhnavets

Poem

A rabbit hops with code so bright,
I plant new lists beneath the night.
Post, patch, fetch, and then delete,
My floppy ears hear test reports tweet.
Hooray — the contacts hop in line! 🐇✨

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.


📜 Recent 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 9a7ca5f and eee5f6c.

📒 Files selected for processing (8)
  • examples/contacts/contact_lists.py (1 hunks)
  • mailtrap/__init__.py (1 hunks)
  • mailtrap/api/contacts.py (2 hunks)
  • mailtrap/api/resources/contact_fields.py (1 hunks)
  • mailtrap/api/resources/contact_lists.py (1 hunks)
  • mailtrap/models/contacts.py (1 hunks)
  • tests/unit/api/test_contact_lists.py (1 hunks)
  • tests/unit/models/test_contacts.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/unit/models/test_contacts.py
  • mailtrap/models/contacts.py
  • mailtrap/api/contacts.py
  • mailtrap/api/resources/contact_lists.py
  • tests/unit/api/test_contact_lists.py
  • examples/contacts/contact_lists.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T13:17:32.889Z
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#36
File: mailtrap/api/contacts.py:15-17
Timestamp: 2025-08-25T13:17:32.889Z
Learning: In the mailtrap-python project, contact list IDs cannot be 0, so using `if list_id:` instead of `if list_id is not None:` is acceptable and preferred for readability in ContactListsApi._api_path and similar methods.

Applied to files:

  • mailtrap/api/resources/contact_fields.py
🧬 Code graph analysis (1)
mailtrap/__init__.py (1)
mailtrap/models/contacts.py (1)
  • ContactListParams (34-35)
🪛 Ruff (0.12.2)
mailtrap/__init__.py

8-8: .models.contacts.ContactListParams 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.9 on macos-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.13 on windows-latest
  • GitHub Check: Test python3.10 on windows-latest
🔇 Additional comments (1)
mailtrap/api/resources/contact_fields.py (1)

45-49: No changes required: ContactFieldsApi._api_path is already consistent with ContactListsApi

Both ContactFieldsApi and ContactListsApi use an explicit field_id is not None / list_id is not None guard, so passing 0 yields /…/fields/0 or /…/lists/0 in both cases. There is no discrepancy to align, and neither codepath has special‐cased truthy vs. null checks.

If the API truly never issues 0 as an ID, enforcing a positive‐integer check (e.g. raising ValueError on 0) could be considered as an independent enhancement, but it is out of scope for this change.

✨ 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-20

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.

@Ihor-Bilous Ihor-Bilous self-assigned this Aug 25, 2025
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: 4

🧹 Nitpick comments (10)
tests/unit/models/test_contacts.py (1)

56-63: Fix test name typo (ContactField → ContactList)

The test name references ContactField but validates ContactListParams. Rename for clarity.

 class TestContactListParams:
-    def test_create_contact_field_params_api_data_should_return_correct_dict(
+    def test_contact_list_params_api_data_should_return_correct_dict(
         self,
     ) -> None:
         params = ContactListParams(name="Test List")
         api_data = params.api_data
         assert api_data == {"name": "Test List"}
mailtrap/models/contacts.py (1)

33-36: Validate name is non-empty for ContactListParams

Guard against accidental empty/whitespace-only names at the model layer. With pydantic v2 dataclasses, Field(min_length=1) is lightweight and consistent with existing RequestParams.api_data behavior.

-from pydantic.dataclasses import dataclass
+from pydantic.dataclasses import dataclass
+from pydantic import Field
@@
 @dataclass
 class ContactListParams(RequestParams):
-    name: str
+    name: str = Field(min_length=1)

If you also want to trim input, you can later switch to:
from typing_extensions import Annotated and from pydantic import StringConstraints with name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)].

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

189-206: Assert outbound JSON payload for create()

Strengthen this test by asserting the exact body sent to the API.

-from typing import Any
+from typing import Any
+import json
@@
         contact_list = contact_lists_api.create(create_contact_list_params)
 
         assert isinstance(contact_list, ContactList)
         assert contact_list.id == LIST_ID
         assert contact_list.name == "My Contact List"
+        # Verify payload matches params
+        assert json.loads(responses.calls[-1].request.body) == create_contact_list_params.api_data

251-268: Assert outbound JSON payload for update()

Mirror the create() test by validating the request body for update as well.

-        contact_list = contact_lists_api.update(LIST_ID, update_contact_list_params)
+        contact_list = contact_lists_api.update(LIST_ID, update_contact_list_params)
 
         assert isinstance(contact_list, ContactList)
         assert contact_list.id == LIST_ID
         assert contact_list.name == "Updated Contact List"
+        # Verify payload matches params
+        import json
+        assert json.loads(responses.calls[-1].request.body) == update_contact_list_params.api_data
examples/contacts/contact_lists.py (2)

5-6: Polish placeholders for clarity

Use “YOUR_...” in placeholders to avoid confusion.

-API_TOKEN = "YOU_API_TOKEN"
-ACCOUNT_ID = "YOU_ACCOUNT_ID"
+API_TOKEN = "YOUR_API_TOKEN"
+ACCOUNT_ID = "YOUR_ACCOUNT_ID"

34-35: Optional: show safe configuration pattern via environment variables

Examples commonly read secrets from env vars to reduce accidental token leaks when users copy-paste.

-if __name__ == "__main__":
-    print(list_contact_lists())
+if __name__ == "__main__":
+    import os
+    token = os.getenv("MAILTRAP_API_TOKEN", API_TOKEN)
+    account = os.getenv("MAILTRAP_ACCOUNT_ID", ACCOUNT_ID)
+    if token != "YOUR_API_TOKEN" and account != "YOUR_ACCOUNT_ID":
+        print(list_contact_lists())
+    else:
+        print("Set MAILTRAP_API_TOKEN and MAILTRAP_ACCOUNT_ID to run this example.")
mailtrap/api/resources/contact_lists.py (4)

40-44: Guard against list_id=0 edge case in path builder

Truthiness check could skip 0. While IDs are typically positive, prefer explicit None check.

-        if list_id:
+        if list_id is not None:
             return f"{path}/{list_id}"

14-16: Handle empty/None responses defensively in get_list

HttpClient returns None for empty content. Be defensive and coerce to an empty list to keep the return type stable.

-        response = self._client.get(self._api_path())
-        return [ContactList(**field) for field in response]
+        response = self._client.get(self._api_path())
+        return [ContactList(**field) for field in (response or [])]

9-13: Optional: add a class docstring for discoverability

A brief docstring helps users of the SDK quickly understand scope and usage.

 class ContactListsApi:
+    """CRUD operations for contact lists within a specific Mailtrap account."""

36-38: Use keyword argument for DeletedObject initialization for clarity and consistency

While the DeletedObject dataclass currently only has a single id field—so positional initialization will work—the keyword form is clearer and more robust against future changes (e.g. adding new fields or reordering). To keep all delete handlers consistent, please update the other resources as well.

Locations to update:

  • mailtrap/api/resources/contact_lists.py
  • mailtrap/api/resources/templates.py
  • mailtrap/api/resources/contact_fields.py

Example diff for contact_lists.py:

-    def delete(self, list_id: int) -> DeletedObject:
-        self._client.delete(self._api_path(list_id))
-        return DeletedObject(list_id)
+    def delete(self, list_id: int) -> DeletedObject:
+        self._client.delete(self._api_path(list_id))
+        return DeletedObject(id=list_id)

Apply the same change to the delete methods in:

  • templates.py (line 41)
  • contact_fields.py (line 43)
📜 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 d3cac02 and 9a7ca5f.

📒 Files selected for processing (7)
  • examples/contacts/contact_lists.py (1 hunks)
  • mailtrap/__init__.py (1 hunks)
  • mailtrap/api/contacts.py (2 hunks)
  • mailtrap/api/resources/contact_lists.py (1 hunks)
  • mailtrap/models/contacts.py (1 hunks)
  • tests/unit/api/test_contact_lists.py (1 hunks)
  • tests/unit/models/test_contacts.py (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-22T13:51:31.437Z
Learnt from: Ihor-Bilous
PR: railsware/mailtrap-python#35
File: examples/contacts/contact_fields.py:11-11
Timestamp: 2025-08-22T13:51:31.437Z
Learning: In mailtrap/api/contacts.py, ContactsBaseApi.contact_fields is defined as a property, not a method, so it can be accessed without parentheses like client.contacts_api.contact_fields.

Applied to files:

  • mailtrap/api/contacts.py
🧬 Code graph analysis (7)
mailtrap/models/contacts.py (1)
mailtrap/models/common.py (1)
  • RequestParams (12-18)
tests/unit/api/test_contact_lists.py (6)
mailtrap/api/contacts.py (1)
  • contact_lists (16-17)
mailtrap/api/resources/contact_lists.py (6)
  • ContactListsApi (9-44)
  • get_list (14-16)
  • get_by_id (18-20)
  • create (22-27)
  • update (29-34)
  • delete (36-38)
mailtrap/exceptions.py (1)
  • APIError (10-15)
mailtrap/http.py (4)
  • HttpClient (13-96)
  • get (25-29)
  • post (31-33)
  • patch (39-41)
mailtrap/models/common.py (1)
  • DeletedObject (22-23)
mailtrap/models/contacts.py (2)
  • ContactList (39-41)
  • ContactListParams (34-35)
mailtrap/api/contacts.py (1)
mailtrap/api/resources/contact_lists.py (1)
  • ContactListsApi (9-44)
tests/unit/models/test_contacts.py (2)
mailtrap/models/contacts.py (1)
  • ContactListParams (34-35)
mailtrap/models/common.py (1)
  • api_data (14-18)
mailtrap/api/resources/contact_lists.py (3)
mailtrap/http.py (4)
  • HttpClient (13-96)
  • get (25-29)
  • post (31-33)
  • patch (39-41)
mailtrap/models/common.py (2)
  • DeletedObject (22-23)
  • api_data (14-18)
mailtrap/models/contacts.py (2)
  • ContactList (39-41)
  • ContactListParams (34-35)
mailtrap/__init__.py (1)
mailtrap/models/contacts.py (1)
  • ContactListParams (34-35)
examples/contacts/contact_lists.py (6)
mailtrap/models/common.py (1)
  • DeletedObject (22-23)
mailtrap/models/contacts.py (2)
  • ContactField (26-30)
  • ContactListParams (34-35)
mailtrap/client.py (2)
  • MailtrapClient (24-142)
  • contacts_api (68-73)
tests/unit/api/test_contact_lists.py (1)
  • contact_lists_api (23-24)
mailtrap/api/contacts.py (1)
  • contact_lists (16-17)
mailtrap/api/resources/contact_lists.py (5)
  • create (22-27)
  • update (29-34)
  • get_list (14-16)
  • get_by_id (18-20)
  • delete (36-38)
🪛 Ruff (0.12.2)
mailtrap/__init__.py

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

(F401)

🔇 Additional comments (4)
mailtrap/api/contacts.py (1)

15-17: Property exposure of ContactListsApi — LGTM and consistent with design

Returning a new ContactListsApi via a @property mirrors contact_fields (per retrieved learnings) and keeps the surface uniform. No issues.

mailtrap/models/contacts.py (1)

38-41: ContactList dataclass — LGTM

Simple response model with id and name maps cleanly from API responses.

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

45-47: Comprehensive CRUD and error-path coverage — nice work

Good use of responses to exercise success and 401/403/404 branches across list/get/create/update/delete. Types and value assertions read well.

examples/contacts/contact_lists.py (1)

9-9: No changes needed: contact_lists is a property

Verified that in mailtrap/api/contacts.py the contact_lists accessor is decorated with @property (lines 15–17), so using it without parentheses is correct.

@yanchuk yanchuk linked an issue Aug 26, 2025 that may be closed by this pull request
@Ihor-Bilous Ihor-Bilous merged commit a964db6 into main Aug 26, 2025
19 checks passed
@Ihor-Bilous Ihor-Bilous deleted the ISSUE-20 branch August 26, 2025 18:40
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.

Support Mailtrap Contact Lists CRUD Functionality

3 participants