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 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)}")
@@ -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)
@@ -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)