mailblastr 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mailblastr/__init__.py +74 -0
- mailblastr/api_keys.py +34 -0
- mailblastr/audiences.py +56 -0
- mailblastr/automations.py +98 -0
- mailblastr/batch.py +14 -0
- mailblastr/campaigns.py +110 -0
- mailblastr/contact_properties.py +50 -0
- mailblastr/contacts.py +191 -0
- mailblastr/domains.py +109 -0
- mailblastr/emails.py +180 -0
- mailblastr/events.py +52 -0
- mailblastr/exceptions.py +25 -0
- mailblastr/http_client.py +173 -0
- mailblastr/logs.py +41 -0
- mailblastr/polls.py +18 -0
- mailblastr/segments.py +76 -0
- mailblastr/templates.py +89 -0
- mailblastr/topics.py +71 -0
- mailblastr/webhooks.py +160 -0
- mailblastr-1.0.0.dist-info/METADATA +298 -0
- mailblastr-1.0.0.dist-info/RECORD +24 -0
- mailblastr-1.0.0.dist-info/WHEEL +5 -0
- mailblastr-1.0.0.dist-info/licenses/LICENSE +21 -0
- mailblastr-1.0.0.dist-info/top_level.txt +1 -0
mailblastr/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Official MailBlastr Python SDK.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import mailblastr
|
|
6
|
+
mailblastr.api_key = "mb_xxxxxxxxx"
|
|
7
|
+
|
|
8
|
+
email = mailblastr.Emails.send({
|
|
9
|
+
"from": "Acme <hello@yourdomain.com>",
|
|
10
|
+
"to": ["user@example.com"],
|
|
11
|
+
"subject": "Hello from MailBlastr",
|
|
12
|
+
"html": "<p>Your first email</p>",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
Every method returns the parsed JSON response and raises
|
|
16
|
+
:class:`mailblastr.MailblastrError` on any non-2xx status.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from .http_client import DEFAULT_BASE_URL, USER_AGENT, VERSION
|
|
22
|
+
from .exceptions import MailblastrError
|
|
23
|
+
from .emails import Emails
|
|
24
|
+
from .batch import Batch
|
|
25
|
+
from .domains import Domains
|
|
26
|
+
from .audiences import Audiences
|
|
27
|
+
from .contacts import Contacts
|
|
28
|
+
from .contact_properties import ContactProperties
|
|
29
|
+
from .campaigns import Campaigns
|
|
30
|
+
from .segments import Segments
|
|
31
|
+
from .topics import Topics
|
|
32
|
+
from .templates import Templates
|
|
33
|
+
from .automations import Automations
|
|
34
|
+
from .webhooks import Webhooks, verify_webhook_signature
|
|
35
|
+
from .events import Events
|
|
36
|
+
from .api_keys import ApiKeys
|
|
37
|
+
from .logs import Logs
|
|
38
|
+
from .polls import Polls
|
|
39
|
+
|
|
40
|
+
# ---- Module-level configuration (resend-python style) ----
|
|
41
|
+
|
|
42
|
+
# Your MailBlastr API key, e.g. mailblastr.api_key = "mb_xxxxxxxxx"
|
|
43
|
+
api_key: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
# Override to point at a different API host.
|
|
46
|
+
base_url: str = DEFAULT_BASE_URL
|
|
47
|
+
|
|
48
|
+
__version__ = VERSION
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"api_key",
|
|
52
|
+
"base_url",
|
|
53
|
+
"MailblastrError",
|
|
54
|
+
"verify_webhook_signature",
|
|
55
|
+
"Emails",
|
|
56
|
+
"Batch",
|
|
57
|
+
"Domains",
|
|
58
|
+
"Audiences",
|
|
59
|
+
"Contacts",
|
|
60
|
+
"ContactProperties",
|
|
61
|
+
"Campaigns",
|
|
62
|
+
"Segments",
|
|
63
|
+
"Topics",
|
|
64
|
+
"Templates",
|
|
65
|
+
"Automations",
|
|
66
|
+
"Webhooks",
|
|
67
|
+
"Events",
|
|
68
|
+
"ApiKeys",
|
|
69
|
+
"Logs",
|
|
70
|
+
"Polls",
|
|
71
|
+
"DEFAULT_BASE_URL",
|
|
72
|
+
"USER_AGENT",
|
|
73
|
+
"VERSION",
|
|
74
|
+
]
|
mailblastr/api_keys.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""API keys resource."""
|
|
2
|
+
|
|
3
|
+
from typing import TypedDict
|
|
4
|
+
|
|
5
|
+
from . import http_client
|
|
6
|
+
from .http_client import path_escape as _e
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CreateParams(TypedDict, total=False):
|
|
10
|
+
name: str # required
|
|
11
|
+
permission: str # 'full_access' | 'sending_access'
|
|
12
|
+
domain_id: str # scope a sending_access key to one domain
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApiKeys:
|
|
16
|
+
"""``mailblastr.ApiKeys`` — the /api-keys endpoints."""
|
|
17
|
+
|
|
18
|
+
CreateParams = CreateParams
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def create(cls, params):
|
|
22
|
+
"""Create an API key. The full token is returned ONLY here.
|
|
23
|
+
POST /api-keys"""
|
|
24
|
+
return http_client.request("POST", "/api-keys", params)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def list(cls):
|
|
28
|
+
"""List API keys (non-secret prefixes only). GET /api-keys"""
|
|
29
|
+
return http_client.request("GET", "/api-keys")
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def remove(cls, api_key_id):
|
|
33
|
+
"""Delete an API key. DELETE /api-keys/:id"""
|
|
34
|
+
return http_client.request("DELETE", f"/api-keys/{_e(api_key_id)}")
|
mailblastr/audiences.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Audiences resource (incl. Google Sheet contact import)."""
|
|
2
|
+
|
|
3
|
+
from typing import TypedDict
|
|
4
|
+
|
|
5
|
+
from . import http_client
|
|
6
|
+
from .http_client import paginate, path_escape as _e
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CreateParams(TypedDict):
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImportSheetParams(TypedDict, total=False):
|
|
14
|
+
url: str # required: link-shared Google Sheet URL
|
|
15
|
+
segment_name: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Audiences:
|
|
19
|
+
"""``mailblastr.Audiences`` — the /audiences endpoints."""
|
|
20
|
+
|
|
21
|
+
CreateParams = CreateParams
|
|
22
|
+
ImportSheetParams = ImportSheetParams
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def create(cls, params):
|
|
26
|
+
"""Create an audience. POST /audiences"""
|
|
27
|
+
return http_client.request("POST", "/audiences", params)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get(cls, audience_id):
|
|
31
|
+
"""Retrieve an audience. GET /audiences/:id"""
|
|
32
|
+
return http_client.request("GET", f"/audiences/{_e(audience_id)}")
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def list(cls, params=None):
|
|
36
|
+
"""List audiences. GET /audiences"""
|
|
37
|
+
return http_client.request("GET", f"/audiences{paginate(params)}")
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def update(cls, audience_id, params):
|
|
41
|
+
"""Rename an audience. PATCH /audiences/:id"""
|
|
42
|
+
return http_client.request("PATCH", f"/audiences/{_e(audience_id)}", params)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def import_sheet(cls, audience_id, params):
|
|
46
|
+
"""Import contacts from a link-shared Google Sheet. Header columns
|
|
47
|
+
become contact properties (usable as ``{{merge_tags}}``); rows land in
|
|
48
|
+
a fresh segment. POST /audiences/:id/contacts/import-sheet"""
|
|
49
|
+
return http_client.request(
|
|
50
|
+
"POST", f"/audiences/{_e(audience_id)}/contacts/import-sheet", params
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def remove(cls, audience_id):
|
|
55
|
+
"""Delete an audience. DELETE /audiences/:id"""
|
|
56
|
+
return http_client.request("DELETE", f"/audiences/{_e(audience_id)}")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Automations resource — DOMAIN-FIRST: every automation belongs to one of
|
|
2
|
+
your sending domains (``domain`` is REQUIRED on create). Only ``Events.send``
|
|
3
|
+
calls with the same ``domain`` trigger it."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, TypedDict
|
|
6
|
+
|
|
7
|
+
from . import http_client
|
|
8
|
+
from .http_client import paginate, path_escape as _e
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CreateParams(TypedDict, total=False):
|
|
12
|
+
name: str # required
|
|
13
|
+
domain: str # REQUIRED: the sending domain this automation belongs to
|
|
14
|
+
trigger: str # e.g. 'contact.created', 'email.opened', or a custom event name
|
|
15
|
+
status: str # 'enabled' | 'disabled' (default 'disabled')
|
|
16
|
+
steps: List[Dict[str, Any]] # optional inline step graph
|
|
17
|
+
connections: List[Dict[str, str]] # optional typed edges between step keys
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UpdateParams(TypedDict, total=False):
|
|
21
|
+
name: str
|
|
22
|
+
status: str # 'enabled' | 'disabled'
|
|
23
|
+
domain: str # re-point at another domain (disabled automations only)
|
|
24
|
+
connections: List[Dict[str, str]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AddStepParams(TypedDict, total=False):
|
|
28
|
+
type: str # required, e.g. 'send_email' | 'wait' | 'condition'
|
|
29
|
+
config: Dict[str, Any]
|
|
30
|
+
key: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Automations:
|
|
34
|
+
"""``mailblastr.Automations`` — the /automations endpoints."""
|
|
35
|
+
|
|
36
|
+
CreateParams = CreateParams
|
|
37
|
+
UpdateParams = UpdateParams
|
|
38
|
+
AddStepParams = AddStepParams
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def create(cls, params):
|
|
42
|
+
"""Create an automation (``domain`` is required). POST /automations"""
|
|
43
|
+
return http_client.request("POST", "/automations", params)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def get(cls, automation_id):
|
|
47
|
+
"""Retrieve an automation (with steps/connections/enrollments).
|
|
48
|
+
GET /automations/:id"""
|
|
49
|
+
return http_client.request("GET", f"/automations/{_e(automation_id)}")
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def list(cls, params=None):
|
|
53
|
+
"""List automations. GET /automations"""
|
|
54
|
+
return http_client.request("GET", f"/automations{paginate(params)}")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def update(cls, automation_id, params):
|
|
58
|
+
"""Update an automation. PATCH /automations/:id"""
|
|
59
|
+
return http_client.request("PATCH", f"/automations/{_e(automation_id)}", params)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def add_step(cls, automation_id, params):
|
|
63
|
+
"""Append a step to an automation. POST /automations/:id/steps"""
|
|
64
|
+
return http_client.request("POST", f"/automations/{_e(automation_id)}/steps", params)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def delete_step(cls, automation_id, step_id):
|
|
68
|
+
"""Delete a step from an automation.
|
|
69
|
+
DELETE /automations/:id/steps/:step_id"""
|
|
70
|
+
return http_client.request(
|
|
71
|
+
"DELETE", f"/automations/{_e(automation_id)}/steps/{_e(step_id)}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def runs(cls, automation_id, params=None):
|
|
76
|
+
"""List an automation's runs. GET /automations/:id/runs"""
|
|
77
|
+
return http_client.request(
|
|
78
|
+
"GET", f"/automations/{_e(automation_id)}/runs{paginate(params)}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def get_run(cls, automation_id, run_id):
|
|
83
|
+
"""Retrieve a single automation run (with its step trace).
|
|
84
|
+
GET /automations/:id/runs/:run_id"""
|
|
85
|
+
return http_client.request(
|
|
86
|
+
"GET", f"/automations/{_e(automation_id)}/runs/{_e(run_id)}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def stop(cls, automation_id):
|
|
91
|
+
"""Stop an automation — prevents new runs; in-progress runs finish.
|
|
92
|
+
POST /automations/:id/stop"""
|
|
93
|
+
return http_client.request("POST", f"/automations/{_e(automation_id)}/stop")
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def remove(cls, automation_id):
|
|
97
|
+
"""Delete an automation. DELETE /automations/:id"""
|
|
98
|
+
return http_client.request("DELETE", f"/automations/{_e(automation_id)}")
|
mailblastr/batch.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Batch send resource — ``mailblastr.Batch.send([...])``."""
|
|
2
|
+
|
|
3
|
+
from . import http_client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Batch:
|
|
7
|
+
"""``mailblastr.Batch`` — the /emails/batch endpoint."""
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def send(cls, params, options=None):
|
|
11
|
+
"""Send up to 100 emails in one request. POST /emails/batch
|
|
12
|
+
|
|
13
|
+
``params`` is a list of :class:`mailblastr.Emails.SendParams` dicts."""
|
|
14
|
+
return http_client.request("POST", "/emails/batch", params, options)
|
mailblastr/campaigns.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Campaigns resource — bulk sends to a domain's contact pool (DOMAIN-FIRST:
|
|
2
|
+
``domain`` is required on create and picks the contact pool the campaign
|
|
3
|
+
targets; ``from`` may be a different verified domain)."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, TypedDict, Union
|
|
6
|
+
|
|
7
|
+
from . import http_client
|
|
8
|
+
from .http_client import paginate, path_escape as _e
|
|
9
|
+
|
|
10
|
+
CreateParams = TypedDict(
|
|
11
|
+
"CreateParams",
|
|
12
|
+
{
|
|
13
|
+
"domain": str, # REQUIRED: the sending domain whose contact pool to target
|
|
14
|
+
"from": str, # required
|
|
15
|
+
"subject": str, # required
|
|
16
|
+
"html": str,
|
|
17
|
+
"text": str,
|
|
18
|
+
"reply_to": Union[str, List[str]],
|
|
19
|
+
"preview_text": str,
|
|
20
|
+
"name": str,
|
|
21
|
+
"segment_id": str, # target a segment instead of everyone
|
|
22
|
+
"topic_id": str, # gate recipients by a topic subscription
|
|
23
|
+
"recurrence": str, # 'daily' | 'weekly' | 'monthly'
|
|
24
|
+
"recurrence_every": int, # periods between recurring sends (1-365)
|
|
25
|
+
"ab_test": Dict[str, Any], # A/B-test configuration
|
|
26
|
+
"followups": List[Dict[str, Any]], # engagement follow-ups (max 5)
|
|
27
|
+
"list_to": bool,
|
|
28
|
+
"unsubscribe_policy": str, # 'account' | 'domain' | 'ignore'
|
|
29
|
+
"send": bool, # send immediately on create
|
|
30
|
+
"scheduled_at": str,
|
|
31
|
+
},
|
|
32
|
+
total=False,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
UpdateParams = TypedDict(
|
|
36
|
+
"UpdateParams",
|
|
37
|
+
{
|
|
38
|
+
"name": str,
|
|
39
|
+
"from": str,
|
|
40
|
+
"subject": str,
|
|
41
|
+
"html": str,
|
|
42
|
+
"text": str,
|
|
43
|
+
"reply_to": Union[str, List[str]],
|
|
44
|
+
"preview_text": str,
|
|
45
|
+
"domain": str, # re-point at another domain's pool (draft campaigns only)
|
|
46
|
+
"segment_id": Union[str, None],
|
|
47
|
+
"topic_id": Union[str, None],
|
|
48
|
+
"recurrence": str,
|
|
49
|
+
"recurrence_every": int,
|
|
50
|
+
"ab_test": Dict[str, Any],
|
|
51
|
+
},
|
|
52
|
+
total=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Campaigns:
|
|
57
|
+
"""``mailblastr.Campaigns`` — the /campaigns endpoints."""
|
|
58
|
+
|
|
59
|
+
CreateParams = CreateParams
|
|
60
|
+
UpdateParams = UpdateParams
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def create(cls, params):
|
|
64
|
+
"""Create a campaign (``domain`` is required). POST /campaigns"""
|
|
65
|
+
return http_client.request("POST", "/campaigns", params)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get(cls, campaign_id):
|
|
69
|
+
"""Retrieve a campaign. GET /campaigns/:id"""
|
|
70
|
+
return http_client.request("GET", f"/campaigns/{_e(campaign_id)}")
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def list(cls, params=None):
|
|
74
|
+
"""List campaigns. GET /campaigns"""
|
|
75
|
+
return http_client.request("GET", f"/campaigns{paginate(params)}")
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def update(cls, campaign_id, params):
|
|
79
|
+
"""Update a campaign. PATCH /campaigns/:id"""
|
|
80
|
+
return http_client.request("PATCH", f"/campaigns/{_e(campaign_id)}", params)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def send(cls, campaign_id, params=None):
|
|
84
|
+
"""Send now, or schedule with ``{"scheduled_at": ...}``.
|
|
85
|
+
POST /campaigns/:id/send"""
|
|
86
|
+
return http_client.request(
|
|
87
|
+
"POST", f"/campaigns/{_e(campaign_id)}/send", params if params is not None else {}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def cancel(cls, campaign_id):
|
|
92
|
+
"""Cancel a scheduled campaign (returns it to draft).
|
|
93
|
+
POST /campaigns/:id/cancel"""
|
|
94
|
+
return http_client.request("POST", f"/campaigns/{_e(campaign_id)}/cancel")
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def stats(cls, campaign_id):
|
|
98
|
+
"""Per-campaign analytics (counts, engagement rates, top links).
|
|
99
|
+
GET /campaigns/:id/stats"""
|
|
100
|
+
return http_client.request("GET", f"/campaigns/{_e(campaign_id)}/stats")
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def ab(cls, campaign_id):
|
|
104
|
+
"""A/B winner evaluation for an A/B campaign. GET /campaigns/:id/ab"""
|
|
105
|
+
return http_client.request("GET", f"/campaigns/{_e(campaign_id)}/ab")
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def remove(cls, campaign_id):
|
|
109
|
+
"""Delete a campaign. DELETE /campaigns/:id"""
|
|
110
|
+
return http_client.request("DELETE", f"/campaigns/{_e(campaign_id)}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Contact properties (custom fields) resource."""
|
|
2
|
+
|
|
3
|
+
from typing import TypedDict, Union
|
|
4
|
+
|
|
5
|
+
from . import http_client
|
|
6
|
+
from .http_client import paginate, path_escape as _e
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CreateParams(TypedDict, total=False):
|
|
10
|
+
key: str # canonical merge-tag key (`name` is accepted as an alias)
|
|
11
|
+
name: str # alias for `key`
|
|
12
|
+
type: str # required: 'string' | 'number'
|
|
13
|
+
fallback_value: Union[str, int, float]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UpdateParams(TypedDict, total=False):
|
|
17
|
+
fallback_value: Union[str, int, float, None] # only fallback_value is mutable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContactProperties:
|
|
21
|
+
"""``mailblastr.ContactProperties`` — the /contact-properties endpoints."""
|
|
22
|
+
|
|
23
|
+
CreateParams = CreateParams
|
|
24
|
+
UpdateParams = UpdateParams
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def create(cls, params):
|
|
28
|
+
"""Register a custom contact property. POST /contact-properties"""
|
|
29
|
+
return http_client.request("POST", "/contact-properties", params)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def get(cls, property_id):
|
|
33
|
+
"""Retrieve a contact property. GET /contact-properties/:id"""
|
|
34
|
+
return http_client.request("GET", f"/contact-properties/{_e(property_id)}")
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def list(cls, params=None):
|
|
38
|
+
"""List contact properties. GET /contact-properties"""
|
|
39
|
+
return http_client.request("GET", f"/contact-properties{paginate(params)}")
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def update(cls, property_id, params):
|
|
43
|
+
"""Update a contact property (key/type are immutable).
|
|
44
|
+
PATCH /contact-properties/:id"""
|
|
45
|
+
return http_client.request("PATCH", f"/contact-properties/{_e(property_id)}", params)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def remove(cls, property_id):
|
|
49
|
+
"""Delete a contact property. DELETE /contact-properties/:id"""
|
|
50
|
+
return http_client.request("DELETE", f"/contact-properties/{_e(property_id)}")
|
mailblastr/contacts.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Contacts resource — DOMAIN-FIRST flat /contacts API plus the nested
|
|
2
|
+
/audiences/:id/contacts variants, bulk imports, segment membership and topics.
|
|
3
|
+
|
|
4
|
+
Domain-first model: each sending domain has its own contact POOL — the same
|
|
5
|
+
address on two domains is two records with separate consent. On the flat
|
|
6
|
+
``/contacts`` API, ``domain`` is REQUIRED to create/list and disambiguates an
|
|
7
|
+
EMAIL id on get/update/remove. Pass ``audience_id`` instead to target a
|
|
8
|
+
specific audience via the nested API.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, List, TypedDict, Union
|
|
12
|
+
|
|
13
|
+
from . import http_client
|
|
14
|
+
from .http_client import build_query, path_escape as _e
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CreateParams(TypedDict, total=False):
|
|
18
|
+
domain: str # REQUIRED on the flat /contacts API (when audience_id is omitted)
|
|
19
|
+
audience_id: str # target a specific audience via the nested API instead
|
|
20
|
+
email: str # required
|
|
21
|
+
first_name: str
|
|
22
|
+
last_name: str
|
|
23
|
+
unsubscribed: bool
|
|
24
|
+
properties: Dict[str, Union[str, int, float]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UpdateParams(TypedDict, total=False):
|
|
28
|
+
id: str # required: contact id OR email
|
|
29
|
+
domain: str # disambiguates an EMAIL id across domains (flat API)
|
|
30
|
+
audience_id: str # omit for the flat /contacts/:id API
|
|
31
|
+
first_name: str
|
|
32
|
+
last_name: str
|
|
33
|
+
unsubscribed: bool
|
|
34
|
+
properties: Dict[str, Union[str, int, float]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ContactInput(TypedDict, total=False):
|
|
38
|
+
email: str # required
|
|
39
|
+
first_name: str
|
|
40
|
+
last_name: str
|
|
41
|
+
unsubscribed: bool
|
|
42
|
+
properties: Dict[str, Union[str, int, float]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UpdateTopicsParams(TypedDict):
|
|
46
|
+
topics: List[Dict[str, str]] # [{"id": ..., "subscription": "opt_in" | "opt_out"}]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Contacts:
|
|
50
|
+
"""``mailblastr.Contacts`` — the /contacts (+ nested audience) endpoints."""
|
|
51
|
+
|
|
52
|
+
CreateParams = CreateParams
|
|
53
|
+
UpdateParams = UpdateParams
|
|
54
|
+
ContactInput = ContactInput
|
|
55
|
+
UpdateTopicsParams = UpdateTopicsParams
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def create(cls, params):
|
|
59
|
+
"""Create a contact. Domain-first: pass ``domain`` to create in that
|
|
60
|
+
domain's contact pool via the flat ``/contacts`` API (required there),
|
|
61
|
+
or ``audience_id`` to target a specific audience via the nested API."""
|
|
62
|
+
body = dict(params)
|
|
63
|
+
audience_id = body.pop("audience_id", None)
|
|
64
|
+
domain = body.pop("domain", None)
|
|
65
|
+
if audience_id:
|
|
66
|
+
# The nested audience route derives its pool from the path.
|
|
67
|
+
return http_client.request("POST", f"/audiences/{_e(audience_id)}/contacts", body)
|
|
68
|
+
if domain is not None:
|
|
69
|
+
body["domain"] = domain
|
|
70
|
+
return http_client.request("POST", "/contacts", body)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get(cls, params):
|
|
74
|
+
"""Retrieve a contact by id or email — ``{"id", "domain"?, "audience_id"?}``.
|
|
75
|
+
An id is exact; an EMAIL can exist in several domains' pools, so pass
|
|
76
|
+
``domain`` to pick the pool (omitted → the oldest match anywhere)."""
|
|
77
|
+
cid = _e(params["id"])
|
|
78
|
+
audience_id = params.get("audience_id")
|
|
79
|
+
if audience_id:
|
|
80
|
+
return http_client.request("GET", f"/audiences/{_e(audience_id)}/contacts/{cid}")
|
|
81
|
+
qs = build_query({"domain": params.get("domain")})
|
|
82
|
+
return http_client.request("GET", f"/contacts/{cid}{qs}")
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def list(cls, params=None):
|
|
86
|
+
"""List contacts. Domain-first: ``domain`` is required on the flat
|
|
87
|
+
``/contacts`` list (names the pool); or pass ``audience_id`` for the
|
|
88
|
+
nested list. ``segment_id`` filters either variant."""
|
|
89
|
+
params = params or {}
|
|
90
|
+
audience_id = params.get("audience_id")
|
|
91
|
+
qs = build_query(
|
|
92
|
+
{
|
|
93
|
+
"domain": params.get("domain") if not audience_id else None,
|
|
94
|
+
"limit": params.get("limit"),
|
|
95
|
+
"after": params.get("after"),
|
|
96
|
+
"before": params.get("before"),
|
|
97
|
+
"segment_id": params.get("segment_id"),
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
base = f"/audiences/{_e(audience_id)}/contacts" if audience_id else "/contacts"
|
|
101
|
+
return http_client.request("GET", f"{base}{qs}")
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def batch(cls, params):
|
|
105
|
+
"""Bulk-import contacts from a list — ``{"audience_id", "contacts",
|
|
106
|
+
"on_conflict"?}``. Upserts by email; max 10,000 per call.
|
|
107
|
+
``on_conflict='skip'`` leaves existing contacts untouched.
|
|
108
|
+
POST /audiences/:id/contacts/batch"""
|
|
109
|
+
qs = build_query({"on_conflict": params.get("on_conflict")})
|
|
110
|
+
return http_client.request(
|
|
111
|
+
"POST",
|
|
112
|
+
f"/audiences/{_e(params['audience_id'])}/contacts/batch{qs}",
|
|
113
|
+
{"contacts": params["contacts"]},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def import_csv(cls, params):
|
|
118
|
+
"""Bulk-import contacts from CSV text — ``{"audience_id", "csv",
|
|
119
|
+
"on_conflict"?, "create_properties"?}``. Upserts by email. By default
|
|
120
|
+
every non-builtin CSV column is auto-registered as a custom property;
|
|
121
|
+
pass ``create_properties=False`` for strict mode.
|
|
122
|
+
POST /audiences/:id/contacts/import"""
|
|
123
|
+
qs = build_query(
|
|
124
|
+
{
|
|
125
|
+
"on_conflict": params.get("on_conflict"),
|
|
126
|
+
"create_properties": (
|
|
127
|
+
"false" if params.get("create_properties") is False else None
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
return http_client.request(
|
|
132
|
+
"POST",
|
|
133
|
+
f"/audiences/{_e(params['audience_id'])}/contacts/import{qs}",
|
|
134
|
+
{"csv": params["csv"]},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def update(cls, params):
|
|
139
|
+
"""Update a contact. On the flat API, pass ``domain`` when ``id`` is
|
|
140
|
+
an EMAIL (disambiguates across pools). PATCH /contacts/:id"""
|
|
141
|
+
body = dict(params)
|
|
142
|
+
cid = _e(body.pop("id"))
|
|
143
|
+
audience_id = body.pop("audience_id", None)
|
|
144
|
+
domain = body.pop("domain", None)
|
|
145
|
+
if audience_id:
|
|
146
|
+
return http_client.request(
|
|
147
|
+
"PATCH", f"/audiences/{_e(audience_id)}/contacts/{cid}", body
|
|
148
|
+
)
|
|
149
|
+
if domain is not None:
|
|
150
|
+
body["domain"] = domain
|
|
151
|
+
return http_client.request("PATCH", f"/contacts/{cid}", body)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def remove(cls, params):
|
|
155
|
+
"""Delete a contact — ``{"id", "domain"?, "audience_id"?}``. On the
|
|
156
|
+
flat API, pass ``domain`` when ``id`` is an EMAIL."""
|
|
157
|
+
cid = _e(params["id"])
|
|
158
|
+
audience_id = params.get("audience_id")
|
|
159
|
+
if audience_id:
|
|
160
|
+
return http_client.request("DELETE", f"/audiences/{_e(audience_id)}/contacts/{cid}")
|
|
161
|
+
qs = build_query({"domain": params.get("domain")})
|
|
162
|
+
return http_client.request("DELETE", f"/contacts/{cid}{qs}")
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def add_to_segment(cls, contact_id, segment_id):
|
|
166
|
+
"""Add a contact to a segment. POST /contacts/:id/segments/:segment_id"""
|
|
167
|
+
return http_client.request(
|
|
168
|
+
"POST", f"/contacts/{_e(contact_id)}/segments/{_e(segment_id)}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def remove_from_segment(cls, contact_id, segment_id):
|
|
173
|
+
"""Remove a contact from a segment. DELETE /contacts/:id/segments/:segment_id"""
|
|
174
|
+
return http_client.request(
|
|
175
|
+
"DELETE", f"/contacts/{_e(contact_id)}/segments/{_e(segment_id)}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def list_segments(cls, contact_id):
|
|
180
|
+
"""List the segments a contact belongs to. GET /contacts/:id/segments"""
|
|
181
|
+
return http_client.request("GET", f"/contacts/{_e(contact_id)}/segments")
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_topics(cls, contact_id):
|
|
185
|
+
"""Get a contact's topic subscriptions. GET /contacts/:id/topics"""
|
|
186
|
+
return http_client.request("GET", f"/contacts/{_e(contact_id)}/topics")
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def update_topics(cls, contact_id, params):
|
|
190
|
+
"""Update a contact's topic subscriptions. PATCH /contacts/:id/topics"""
|
|
191
|
+
return http_client.request("PATCH", f"/contacts/{_e(contact_id)}/topics", params)
|