Skip to content

MT-22401: Add Email Campaigns API - #74

Open
Rabsztok wants to merge 1 commit into
mainfrom
MT-22401-python-email-campaigns
Open

MT-22401: Add Email Campaigns API#74
Rabsztok wants to merge 1 commit into
mainfrom
MT-22401-python-email-campaigns

Conversation

@Rabsztok

@Rabsztok Rabsztok commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Motivation

https://railsware.atlassian.net/browse/MT-22401

Port the Email Campaigns public API (MT-21113) to the Python SDK.

Changes

  • Add EmailCampaignsApi (client.email_campaigns_api.email_campaigns) covering the full contract: list (page-token pagination + search), get, create, update, delete, the five lifecycle actions (start, schedule, cancel, terminate, reset), and get_stats with an optional date range
  • Models follow the published OpenAPI schema: flat request bodies, data-envelope unwrapping, UUID mailsend_domain_id, audience id lists, request-side TemplateAttributes (subject/body_html/body_text/merge_tags), state-metadata error objects; delete returns DeletedObject
  • Export the campaign models from the package root and add a full-lifecycle example in examples/email_campaigns/

How to test

  • Run examples/email_campaigns/email_campaigns.py with a real API token and a verified sending domain — create a draft, update design/audience, schedule + cancel, fetch stats, delete
  • Verify a lifecycle 422 (e.g. cancel on a draft) surfaces the API error message

Summary by CodeRabbit

  • New Features

    • Added Email Campaigns API support for listing, creating, viewing, updating, scheduling, starting, canceling, terminating, resetting, and deleting campaigns.
    • Added campaign statistics with optional date-range filters.
    • Added structured campaign, template, delivery, scheduling, pagination, and statistics models.
    • Added a complete usage example covering the campaign lifecycle.
  • Documentation

    • Updated the supported functionality list with Email Campaigns API examples.

Decisions:
- Send flat request bodies (no email_campaign wrapper) per the current OpenAPI contract
- Unwrap the data envelope on single-campaign and stats responses; list keeps {data, pagination}
- delete returns DeletedObject(email_campaign_id) since the API responds 204 No Content
- Add the five lifecycle methods (start/schedule/cancel/terminate/reset) with ScheduleEmailCampaignParams and stats start_date/end_date params
- Require name, mailsend_domain_id (UUID string), from_local_part, and template_attributes on create; split request-side TemplateAttributes from the response-side CampaignTemplate
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a typed Email Campaigns API with CRUD, scheduling, lifecycle actions, statistics, client exposure, comprehensive unit tests, and a runnable example documented in the README.

Changes

Email Campaigns API

Layer / File(s) Summary
Campaign models and request contracts
mailtrap/models/email_campaigns.py, mailtrap/__init__.py
Defines campaign, statistics, pagination, lifecycle metadata, and request parameter models, then re-exports the public types.
Campaign API operations and client wiring
mailtrap/api/resources/email_campaigns.py, mailtrap/api/email_campaigns.py, mailtrap/client.py
Adds typed CRUD, scheduling, lifecycle, and statistics methods and exposes them through MailtrapClient.email_campaigns_api.
API behavior validation
tests/unit/api/email_campaigns/test_email_campaigns.py, tests/unit/test_client.py
Tests request serialization, response parsing, pagination, errors, lifecycle actions, scheduling, statistics, deletion, and account-id validation.
Campaign workflow example and documentation
examples/email_campaigns/email_campaigns.py, README.md
Adds a sequential campaign workflow example and documents it under supported functionality.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MailtrapClient
  participant EmailCampaignsBaseApi
  participant EmailCampaignsApi
  participant HttpClient
  MailtrapClient->>EmailCampaignsBaseApi: validate account_id and create API
  EmailCampaignsBaseApi->>EmailCampaignsApi: provide account_id and HttpClient
  EmailCampaignsApi->>HttpClient: send campaign operation request
  HttpClient-->>EmailCampaignsApi: return response envelope
  EmailCampaignsApi-->>MailtrapClient: return typed model
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: andrii-porokhnavets, igordobryn, vladimirtaytor

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the main change: adding the Email Campaigns API.
Description check ✅ Passed The description covers motivation, changes, and testing; only the optional Images and GIFs section is missing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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

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

@Rabsztok
Rabsztok marked this pull request as ready for review July 30, 2026 12:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
tests/unit/api/email_campaigns/test_email_campaigns.py (3)

332-372: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exact-byte request-body assertions are brittle across create/update/schedule tests. All three sites assert responses.calls[0].request.body == a hard-coded byte string, coupling the tests to the JSON serializer's exact key order and escaping rather than the semantic request contract being verified.

  • tests/unit/api/email_campaigns/test_email_campaigns.py#L332-L372: parse responses.calls[0].request.body with json.loads and compare against a dict literal instead of the raw byte string.
  • tests/unit/api/email_campaigns/test_email_campaigns.py#L374-L408: same — compare parsed JSON dict for the "only supplied fields" assertion.
  • tests/unit/api/email_campaigns/test_email_campaigns.py#L538-L564: same — compare parsed JSON dict for the schedule datetime body.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/api/email_campaigns/test_email_campaigns.py` around lines 332 -
372, Replace the exact-byte request-body assertions in
tests/unit/api/email_campaigns/test_email_campaigns.py at lines 332-372,
374-408, and 538-564 with json.loads parsing of responses.calls[0].request.body,
then compare each parsed payload to a dict literal preserving the existing
semantic expectations for create, supplied fields, and schedule datetime bodies.

562-564: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same byte-exact body brittleness (schedule request).

Same concern as Lines 332-372 and 401-408; a single-key body is less risky here but still couples to exact serialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/api/email_campaigns/test_email_campaigns.py` around lines 562 -
564, Update the schedule request assertion around
responses.calls[0].request.body to parse the JSON body and assert the datetime
field value instead of comparing the serialized bytes exactly. Preserve
validation that the request contains the expected 2026-06-01T09:00:00.000Z
timestamp without coupling the test to JSON formatting or key ordering.

401-408: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same byte-exact body brittleness as the create test.

See Lines 332-372 for the shared concern and suggested fix (parse JSON and compare dicts instead of raw bytes).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/api/email_campaigns/test_email_campaigns.py` around lines 401 -
408, Update the request-body assertion in the relevant email campaign update
test to parse responses.calls[0].request.body as JSON and compare the resulting
dictionary structure, matching the approach used by the create test around lines
332-372 instead of asserting exact byte serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/email_campaigns/email_campaigns.py`:
- Around line 69-76: Update schedule_email_campaign to derive a future
scheduling datetime at runtime instead of using the stale hardcoded June 2026
value. Also update the example’s stats date range to align with the newly
scheduled campaign date so the campaign results remain visible.

In `@mailtrap/client.py`:
- Around line 121-127: Update the email_campaigns_api property to remove the
_validate_account_id requirement and stop passing account_id into
EmailCampaignsBaseApi, allowing token-scoped clients without an account ID.
Adjust the related account-ID validation test to assert the API remains usable
without account_id.

In `@mailtrap/models/email_campaigns.py`:
- Line 189: Update CreateEmailCampaignParams so template_attributes uses a
create-specific template model requiring subject: str, or validates that subject
is present before submission. Keep the existing partial TemplateAttributes model
for update payloads and ensure create() cannot send a template without a
subject.

---

Nitpick comments:
In `@tests/unit/api/email_campaigns/test_email_campaigns.py`:
- Around line 332-372: Replace the exact-byte request-body assertions in
tests/unit/api/email_campaigns/test_email_campaigns.py at lines 332-372,
374-408, and 538-564 with json.loads parsing of responses.calls[0].request.body,
then compare each parsed payload to a dict literal preserving the existing
semantic expectations for create, supplied fields, and schedule datetime bodies.
- Around line 562-564: Update the schedule request assertion around
responses.calls[0].request.body to parse the JSON body and assert the datetime
field value instead of comparing the serialized bytes exactly. Preserve
validation that the request contains the expected 2026-06-01T09:00:00.000Z
timestamp without coupling the test to JSON formatting or key ordering.
- Around line 401-408: Update the request-body assertion in the relevant email
campaign update test to parse responses.calls[0].request.body as JSON and
compare the resulting dictionary structure, matching the approach used by the
create test around lines 332-372 instead of asserting exact byte serialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 644203b8-ae5c-4930-a576-f37c3557d75a

📥 Commits

Reviewing files that changed from the base of the PR and between cd63644 and f447e0e.

📒 Files selected for processing (10)
  • README.md
  • examples/email_campaigns/email_campaigns.py
  • mailtrap/__init__.py
  • mailtrap/api/email_campaigns.py
  • mailtrap/api/resources/email_campaigns.py
  • mailtrap/client.py
  • mailtrap/models/email_campaigns.py
  • tests/unit/api/email_campaigns/__init__.py
  • tests/unit/api/email_campaigns/test_email_campaigns.py
  • tests/unit/test_client.py

Comment on lines +69 to +76
def schedule_email_campaign(email_campaign_id: int) -> EmailCampaign:
# The campaign must be a `draft`; the time comes back in
# `current_state_metadata.scheduled_at`.
return email_campaigns_api.schedule(
email_campaign_id=email_campaign_id,
schedule_params=mt.ScheduleEmailCampaignParams(
datetime="2026-06-01T09:00:00.000Z"
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching example email campaigns:"
fd -a 'email_campaigns.py$' . || true

echo
echo "Current date from sandbox:"
date -u +"%Y-%m-%dT%H:%M:%S.000Z"

echo
echo "Relevant file excerpt:"
if [ -f examples/email_campaigns/email_campaigns.py ]; then
  nl -ba examples/email_campaigns/email_campaigns.py | sed -n '1,130p'
fi

echo
echo "Search for date params in example:"
if [ -f examples/email_campaigns/email_campaigns.py ]; then
  rg -n 'datetime=|start_date=|end_date=|today|timezone|timedelta' examples/email_campaigns/email_campaigns.py
fi

echo
echo "Check API docs/usages for ScheduleEmailCampaignParams and get_stats date expectations if present:"
rg -n 'ScheduleEmailCampaignParams|schedule\(email_campaign_id|get_stats\(email_campaign_id|start_date.*end_date' . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

Repository: mailtrap/mailtrap-python

Length of output: 634


🌐 Web query:

Mailtrap Python SDK ScheduleEmailCampaignParams datetime scheduled campaign get_stats start_date end_date

💡 Result:

The Mailtrap Python SDK provides tools to interact with the Mailtrap API, including sending emails and accessing various reporting and account management features [1][2][3]. Regarding your specific queries: 1. ScheduleEmailCampaignParams: The Mailtrap API manages campaign scheduling via specific endpoints that expect a JSON request body containing a datetime field [4]. This field must be provided in ISO 8601 format and represents the future time when the campaign should be sent [4]. Note that this date must be no more than one month in the future [4]. While the official Mailtrap Python SDK facilitates general API requests, you may need to interface with these specific promotional campaign endpoints using the client's underlying request capabilities or by manually constructing the payload if it is not explicitly mapped as a high-level SDK class [1][3]. 2. Stats (start_date and end_date): The Mailtrap Sending Stats API allows you to retrieve performance data (e.g., delivery, bounce, open, click, and spam counts/rates) for your sent emails [5][6]. When querying these stats, the start_date and end_date parameters are required to define the reporting range [5][6]. These dates should be provided in YYYY-MM-DD format [7]. Although specific Python SDK method names can evolve, these parameters are standard across the Stats API, which includes endpoints such as: - /stats (Aggregated data) [5] - /stats/domains [5][6] - /stats/categories [5][6] - /stats/email_service_providers [5][6] - /stats/date [5][6] For implementation details, ensure you are using the latest version of the mailtrap package from PyPI, and consult the official API documentation for the most current endpoint mappings [8][3]. [4][1][5][7]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Read relevant source excerpts:"
sed -n '1,135p' examples/email_campaigns/email_campaigns.py
echo
sed -n '1,220p' mailtrap/api/email_campaigns.py
echo
sed -n '1,160p' mailtrap/models/email_campaigns.py

echo
echo "Find SDK type/model signatures:"
rg -n "class ScheduleEmailCampaignParams|ScheduleEmailCampaignParams|def schedule|def get_stats|start_date|end_date" mailtrap examples tests --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,220p'

echo
echo "Validate fixed ISO scheduling timestamp against now in Python:"
python3 - <<'PY'
from datetime import datetime, timedelta, timezone
now = datetime.fromisoformat("2026-07-30T12:08:26.000+00:00")
fixed = datetime.fromisoformat("2026-06-01T09:00:00.000+00:00")
future = "(datetime.now(timezone.utc) + timedelta(days=1))"
print(f"now={now.isoformat()}")
print(f"fixed={fixed.isoformat()}")
print(f"fixed_is_in_past={fixed < now}")
print(f"future_proposal_satisfies_one_month_max={future} would be {now+timedelta(days=1)}", 0.0 <= (now+timedelta(days=1) - now).total_seconds() / 86400.0 <= 31)
PY

Repository: mailtrap/mailtrap-python

Length of output: 14482


Use a non-stale scheduling date in the campaign example.

The hardcoded datetime="2026-06-01T09:00:00.000Z" is before July 2026, so the schedule call will fail for users running this example now. Use a runtime-derived future datetime instead; keep the stats date range aligned to the newly created campaign so it can show results.

Proposed fix
+from datetime import datetime, timedelta, timezone
+
 ...
-            datetime="2026-06-01T09:00:00.000Z"
+            datetime=(
+                (datetime.now(timezone.utc) + timedelta(days=1))
+                .isoformat(timespec="milliseconds")
+                .replace("+00:00", "Z")
+            )
...
 def get_email_campaign_stats(email_campaign_id: int) -> EmailCampaignStats:
+    today = datetime.now(timezone.utc).date()
     return email_campaigns_api.get_stats(
         email_campaign_id=email_campaign_id,
-        start_date="2026-05-01",
-        end_date="2026-05-31",
+        start_date=(today - timedelta(days=30)).isoformat(),
+        end_date=today.isoformat(),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/email_campaigns/email_campaigns.py` around lines 69 - 76, Update
schedule_email_campaign to derive a future scheduling datetime at runtime
instead of using the stale hardcoded June 2026 value. Also update the example’s
stats date range to align with the newly scheduled campaign date so the campaign
results remain visible.

Comment thread mailtrap/client.py
Comment on lines +121 to +127
@property
def email_campaigns_api(self) -> EmailCampaignsBaseApi:
self._validate_account_id("Email Campaigns API")
return EmailCampaignsBaseApi(
account_id=cast(str, self.account_id),
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not require account_id for this token-scoped API.

The resource uses /api/email_campaigns and explicitly resolves the account server-side from the token, but Line 123 rejects clients created without account_id. This makes a valid token-only campaign client unusable. Remove this validation and the unused account-id plumbing; update the corresponding account-id validation test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mailtrap/client.py` around lines 121 - 127, Update the email_campaigns_api
property to remove the _validate_account_id requirement and stop passing
account_id into EmailCampaignsBaseApi, allowing token-scoped clients without an
account ID. Adjust the related account-ID validation test to assert the API
remains usable without account_id.

name: str
mailsend_domain_id: str
from_local_part: str
template_attributes: TemplateAttributes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a subject for create payloads.

Line 189 accepts TemplateAttributes, whose subject is optional, so create() can send an invalid empty template and defer a required-field error to the API. Use a create-specific template model with subject: str (keep the partial model for updates), or validate template_attributes.subject in CreateEmailCampaignParams.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mailtrap/models/email_campaigns.py` at line 189, Update
CreateEmailCampaignParams so template_attributes uses a create-specific template
model requiring subject: str, or validates that subject is present before
submission. Keep the existing partial TemplateAttributes model for update
payloads and ensure create() cannot send a template without a subject.

@Rabsztok
Rabsztok requested review from Ihor-Bilous and mklocek July 30, 2026 12:12
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.

1 participant