add campaign api (#274)

This commit is contained in:
KM Koushik
2025-10-18 10:31:43 +11:00
committed by GitHub
parent e631f16c85
commit a5ca3b2f87
31 changed files with 2093 additions and 187 deletions
+31 -1
View File
@@ -37,6 +37,29 @@ resp, _ = client.emails.send(payload={
"html": "<strong>Hi!</strong>",
})
# 3) Campaigns
campaign_payload: types.CampaignCreate = {
"name": "Welcome Series",
"subject": "Welcome to our service!",
"html": "<p>Thanks for joining us!</p>",
"from": "welcome@example.com",
"contactBookId": "cb_1234567890",
}
campaign_resp, _ = client.campaigns.create(payload=campaign_payload)
# Schedule a campaign
schedule_payload: types.CampaignSchedule = {
"scheduledAt": "2024-12-01T10:00:00Z",
}
schedule_resp, _ = client.campaigns.schedule(
campaign_id=campaign_resp["id"],
payload=schedule_payload
)
# Pause/resume campaigns
client.campaigns.pause(campaign_id="campaign_123")
client.campaigns.resume(campaign_id="campaign_123")
# Toggle behavior if desired:
# - raise_on_error=False: return (None, error_dict) instead of raising
# No model parsing occurs; methods return plain dicts following the typed shapes.
@@ -55,7 +78,14 @@ This package is managed with Poetry. Models are maintained in-repo under
It is published as `usesend` on PyPI.
## Available Resources
- **Emails**: `client.emails.send()`, `client.emails.get()`
- **Contacts**: `client.contacts.create()`, `client.contacts.get()`, `client.contacts.list()`
- **Domains**: `client.domains.create()`, `client.domains.get()`, `client.domains.verify()`
- **Campaigns**: `client.campaigns.create()`, `client.campaigns.get()`, `client.campaigns.schedule()`, `client.campaigns.pause()`, `client.campaigns.resume()`
Notes
- Human-friendly models are available under `usesend.types` (e.g., `EmailCreate`, `Contact`, `APIError`).
- Human-friendly models are available under `usesend.types` (e.g., `EmailCreate`, `CampaignCreate`, `Contact`, `APIError`).
- Endpoint methods accept TypedDict payloads or plain dicts via the `payload=` keyword.
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "usesend"
version = "0.2.6"
version = "0.2.7"
description = "Python SDK for the UseSend API"
authors = ["UseSend"]
license = "MIT"
+2 -1
View File
@@ -2,6 +2,7 @@
from .usesend import UseSend, UseSendHTTPError
from .domains import Domains # type: ignore
from .campaigns import Campaigns # type: ignore
from . import types
__all__ = ["UseSend", "UseSendHTTPError", "types", "Domains"]
__all__ = ["UseSend", "UseSendHTTPError", "types", "Domains", "Campaigns"]
+68
View File
@@ -0,0 +1,68 @@
"""Campaign resource client using TypedDict shapes (no Pydantic)."""
from __future__ import annotations
from typing import Any, Dict, Optional, Tuple
from .types import (
APIError,
Campaign,
CampaignCreate,
CampaignCreateResponse,
CampaignSchedule,
CampaignScheduleResponse,
CampaignActionResponse,
)
class Campaigns:
"""Client for `/campaigns` endpoints."""
def __init__(self, usesend: "UseSend") -> None:
self.usesend = usesend
def create(
self, payload: CampaignCreate
) -> Tuple[Optional[CampaignCreateResponse], Optional[APIError]]:
data, err = self.usesend.post(
"/campaigns",
payload,
)
return (data, err) # type: ignore[return-value]
def get(
self, campaign_id: str
) -> Tuple[Optional[Campaign], Optional[APIError]]:
data, err = self.usesend.get(
f"/campaigns/{campaign_id}"
)
return (data, err) # type: ignore[return-value]
def schedule(
self, campaign_id: str, payload: CampaignSchedule
) -> Tuple[Optional[CampaignScheduleResponse], Optional[APIError]]:
data, err = self.usesend.post(
f"/campaigns/{campaign_id}/schedule",
payload,
)
return (data, err) # type: ignore[return-value]
def pause(
self, campaign_id: str
) -> Tuple[Optional[CampaignActionResponse], Optional[APIError]]:
data, err = self.usesend.post(
f"/campaigns/{campaign_id}/pause",
{},
)
return (data, err) # type: ignore[return-value]
def resume(
self, campaign_id: str
) -> Tuple[Optional[CampaignActionResponse], Optional[APIError]]:
data, err = self.usesend.post(
f"/campaigns/{campaign_id}/resume",
{},
)
return (data, err) # type: ignore[return-value]
from .usesend import UseSend # noqa: E402 pylint: disable=wrong-import-position
+202 -96
View File
@@ -15,14 +15,14 @@ from typing_extensions import NotRequired, Required, Literal
# ---------------------------------------------------------------------------
DomainStatus = Literal[
'NOT_STARTED',
'PENDING',
'SUCCESS',
'FAILED',
'TEMPORARY_FAILURE',
"NOT_STARTED",
"PENDING",
"SUCCESS",
"FAILED",
"TEMPORARY_FAILURE",
]
DNSRecordType = Literal['MX', 'TXT']
DNSRecordType = Literal["MX", "TXT"]
class DNSRecord(TypedDict, total=False):
@@ -99,24 +99,25 @@ class DomainDeleteResponse(TypedDict):
success: bool
message: str
# ---------------------------------------------------------------------------
# Emails
# ---------------------------------------------------------------------------
EmailEventStatus = Literal[
'SCHEDULED',
'QUEUED',
'SENT',
'DELIVERY_DELAYED',
'BOUNCED',
'REJECTED',
'RENDERING_FAILURE',
'DELIVERED',
'OPENED',
'CLICKED',
'COMPLAINED',
'FAILED',
'CANCELLED',
"SCHEDULED",
"QUEUED",
"SENT",
"DELIVERY_DELAYED",
"BOUNCED",
"REJECTED",
"RENDERING_FAILURE",
"DELIVERED",
"OPENED",
"CLICKED",
"COMPLAINED",
"FAILED",
"CANCELLED",
]
@@ -128,22 +129,22 @@ class EmailEvent(TypedDict, total=False):
Email = TypedDict(
'Email',
"Email",
{
'id': str,
'teamId': float,
'to': Union[str, List[str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'from': str,
'subject': str,
'html': str,
'text': str,
'createdAt': str,
'updatedAt': str,
'emailEvents': List[EmailEvent],
}
"id": str,
"teamId": float,
"to": Union[str, List[str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"from": str,
"subject": str,
"html": str,
"text": str,
"createdAt": str,
"updatedAt": str,
"emailEvents": List[EmailEvent],
},
)
@@ -157,40 +158,40 @@ class EmailUpdateResponse(TypedDict, total=False):
EmailLatestStatus = Literal[
'SCHEDULED',
'QUEUED',
'SENT',
'DELIVERY_DELAYED',
'BOUNCED',
'REJECTED',
'RENDERING_FAILURE',
'DELIVERED',
'OPENED',
'CLICKED',
'COMPLAINED',
'FAILED',
'CANCELLED',
"SCHEDULED",
"QUEUED",
"SENT",
"DELIVERY_DELAYED",
"BOUNCED",
"REJECTED",
"RENDERING_FAILURE",
"DELIVERED",
"OPENED",
"CLICKED",
"COMPLAINED",
"FAILED",
"CANCELLED",
]
EmailListItem = TypedDict(
'EmailListItem',
"EmailListItem",
{
'id': str,
'to': Union[str, List[str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'from': str,
'subject': str,
'html': str,
'text': str,
'createdAt': str,
'updatedAt': str,
'latestStatus': EmailLatestStatus,
'scheduledAt': str,
'domainId': float,
}
"id": str,
"to": Union[str, List[str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"from": str,
"subject": str,
"html": str,
"text": str,
"createdAt": str,
"updatedAt": str,
"latestStatus": EmailLatestStatus,
"scheduledAt": str,
"domainId": float,
},
)
@@ -205,23 +206,23 @@ class Attachment(TypedDict):
EmailCreate = TypedDict(
'EmailCreate',
"EmailCreate",
{
'to': Required[Union[str, List[str]]],
'from': Required[str],
'subject': NotRequired[str],
'templateId': NotRequired[str],
'variables': NotRequired[Dict[str, str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'text': NotRequired[str],
'html': NotRequired[str],
'attachments': NotRequired[List[Attachment]],
'scheduledAt': NotRequired[Union[datetime, str]],
'inReplyToId': NotRequired[str],
'headers': NotRequired[Dict[str, str]],
}
"to": Required[Union[str, List[str]]],
"from": Required[str],
"subject": NotRequired[str],
"templateId": NotRequired[str],
"variables": NotRequired[Dict[str, str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"text": NotRequired[str],
"html": NotRequired[str],
"attachments": NotRequired[List[Attachment]],
"scheduledAt": NotRequired[Union[datetime, str]],
"inReplyToId": NotRequired[str],
"headers": NotRequired[Dict[str, str]],
},
)
@@ -230,23 +231,23 @@ class EmailCreateResponse(TypedDict, total=False):
EmailBatchItem = TypedDict(
'EmailBatchItem',
"EmailBatchItem",
{
'to': Required[Union[str, List[str]]],
'from': Required[str],
'subject': NotRequired[str],
'templateId': NotRequired[str],
'variables': NotRequired[Dict[str, str]],
'replyTo': NotRequired[Union[str, List[str]]],
'cc': NotRequired[Union[str, List[str]]],
'bcc': NotRequired[Union[str, List[str]]],
'text': NotRequired[str],
'html': NotRequired[str],
'attachments': NotRequired[List[Attachment]],
'scheduledAt': NotRequired[Union[datetime, str]],
'inReplyToId': NotRequired[str],
'headers': NotRequired[Dict[str, str]],
}
"to": Required[Union[str, List[str]]],
"from": Required[str],
"subject": NotRequired[str],
"templateId": NotRequired[str],
"variables": NotRequired[Dict[str, str]],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"text": NotRequired[str],
"html": NotRequired[str],
"attachments": NotRequired[List[Attachment]],
"scheduledAt": NotRequired[Union[datetime, str]],
"inReplyToId": NotRequired[str],
"headers": NotRequired[Dict[str, str]],
},
)
@@ -269,6 +270,7 @@ class EmailCancelResponse(TypedDict, total=False):
# Contacts
# ---------------------------------------------------------------------------
class ContactCreate(TypedDict, total=False):
email: str
firstName: Optional[str]
@@ -335,11 +337,115 @@ class ContactDeleteResponse(TypedDict):
success: bool
# ---------------------------------------------------------------------------
# Campaigns
# ---------------------------------------------------------------------------
Campaign = TypedDict(
"Campaign",
{
"id": str,
"name": str,
"from": str,
"subject": str,
"previewText": Optional[str],
"contactBookId": Optional[str],
"html": Optional[str],
"content": Optional[str],
"status": str,
"scheduledAt": Optional[str],
"batchSize": int,
"batchWindowMinutes": int,
"total": int,
"sent": int,
"delivered": int,
"opened": int,
"clicked": int,
"unsubscribed": int,
"bounced": int,
"hardBounced": int,
"complained": int,
"replyTo": List[str],
"cc": List[str],
"bcc": List[str],
"createdAt": str,
"updatedAt": str,
},
)
CampaignCreate = TypedDict(
"CampaignCreate",
{
"name": Required[str],
"from": Required[str],
"subject": Required[str],
"previewText": NotRequired[str],
"contactBookId": Required[str],
"content": NotRequired[str],
"html": NotRequired[str],
"replyTo": NotRequired[Union[str, List[str]]],
"cc": NotRequired[Union[str, List[str]]],
"bcc": NotRequired[Union[str, List[str]]],
"sendNow": NotRequired[bool],
"scheduledAt": NotRequired[str],
"batchSize": NotRequired[int],
},
)
CampaignCreateResponse = TypedDict(
"CampaignCreateResponse",
{
"id": str,
"name": str,
"from": str,
"subject": str,
"previewText": Optional[str],
"contactBookId": Optional[str],
"html": Optional[str],
"content": Optional[str],
"status": str,
"scheduledAt": Optional[str],
"batchSize": int,
"batchWindowMinutes": int,
"total": int,
"sent": int,
"delivered": int,
"opened": int,
"clicked": int,
"unsubscribed": int,
"bounced": int,
"hardBounced": int,
"complained": int,
"replyTo": List[str],
"cc": List[str],
"bcc": List[str],
"createdAt": str,
"updatedAt": str,
},
)
class CampaignSchedule(TypedDict, total=False):
scheduledAt: Optional[str]
batchSize: Optional[int]
sendNow: Optional[bool]
class CampaignScheduleResponse(TypedDict, total=False):
success: bool
class CampaignActionResponse(TypedDict, total=False):
success: bool
# ---------------------------------------------------------------------------
# Common
# ---------------------------------------------------------------------------
class APIError(TypedDict):
code: str
message: str
+2
View File
@@ -72,6 +72,7 @@ class UseSend:
self.emails = Emails(self)
self.contacts = Contacts(self)
self.domains = Domains(self)
self.campaigns = Campaigns(self)
# ------------------------------------------------------------------
# Internal request helper
@@ -125,3 +126,4 @@ class UseSend:
from .emails import Emails # noqa: E402 pylint: disable=wrong-import-position
from .contacts import Contacts # noqa: E402 pylint: disable=wrong-import-position
from .domains import Domains # type: ignore # noqa: E402
from .campaigns import Campaigns # type: ignore # noqa: E402