unitpost 0.2.1__tar.gz → 0.4.0__tar.gz
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.
- {unitpost-0.2.1 → unitpost-0.4.0}/PKG-INFO +50 -12
- {unitpost-0.2.1 → unitpost-0.4.0}/README.md +48 -10
- {unitpost-0.2.1 → unitpost-0.4.0}/pyproject.toml +1 -1
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/_client.py +6 -0
- unitpost-0.4.0/src/unitpost/_generated/models.py +1292 -0
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/_generated/operations.py +508 -18
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/_http.py +1 -1
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/resources.py +218 -2
- {unitpost-0.2.1 → unitpost-0.4.0}/tests/test_sdk.py +9 -0
- unitpost-0.2.1/src/unitpost/_generated/models.py +0 -2652
- {unitpost-0.2.1 → unitpost-0.4.0}/.gitignore +0 -0
- {unitpost-0.2.1 → unitpost-0.4.0}/package.json +0 -0
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/__init__.py +0 -0
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/_generated/__init__.py +0 -0
- {unitpost-0.2.1 → unitpost-0.4.0}/src/unitpost/_webhooks.py +0 -0
- {unitpost-0.2.1 → unitpost-0.4.0}/tests/test_retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: unitpost
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage.
|
|
5
5
|
Project-URL: Homepage, https://www.unitpost.com
|
|
6
6
|
Project-URL: Documentation, https://www.unitpost.com/docs
|
|
@@ -66,15 +66,16 @@ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
|
|
|
66
66
|
|
|
67
67
|
| Accessor | Covers |
|
|
68
68
|
| --- | --- |
|
|
69
|
-
| `
|
|
69
|
+
| `email` | Send, batch, list, get, update, stats, inbound (`received_*`) |
|
|
70
|
+
| `email.topics` | Topics + contact topic prefs |
|
|
71
|
+
| `email.campaigns` | Draft, schedule, send, pause/resume |
|
|
72
|
+
| `email.templates` | Template CRUD |
|
|
73
|
+
| `email.domains` | Domains + verify |
|
|
74
|
+
| `sms` | Send, get, list, `brands()`, `numbers()`, contact SMS consent. Available on paid plans: on the free plan `send` returns a `403` with code `limit_reached`, while the reads keep working. |
|
|
70
75
|
| `contacts` | CRUD, import jobs |
|
|
71
76
|
| `contact_fields` | Custom field definitions |
|
|
72
77
|
| `segments` | Segments + membership |
|
|
73
|
-
| `topics` | Topics + contact topic prefs |
|
|
74
|
-
| `campaigns` | Draft, schedule, send, pause/resume |
|
|
75
|
-
| `templates` | Template CRUD |
|
|
76
78
|
| `brand_kits` | Brand kit |
|
|
77
|
-
| `domains` | Domains + verify |
|
|
78
79
|
| `webhooks` | Endpoints + `verify()` |
|
|
79
80
|
| `api_keys` | API key management |
|
|
80
81
|
| `suppressions` | Suppression list |
|
|
@@ -98,7 +99,7 @@ complaints are added automatically; you can also add your own:
|
|
|
98
99
|
unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
|
|
99
100
|
unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})
|
|
100
101
|
|
|
101
|
-
# Look one up by id or email; list
|
|
102
|
+
# Look one up by id or email; list your own; un-suppress.
|
|
102
103
|
unitpost.suppressions.get("bounced@example.com")
|
|
103
104
|
for s in unitpost.suppressions.list_all():
|
|
104
105
|
print(s["email"], s["reason"])
|
|
@@ -127,9 +128,45 @@ Or from the client (Resend-style alias):
|
|
|
127
128
|
event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
|
|
128
129
|
```
|
|
129
130
|
|
|
131
|
+
## Receive a text and answer it
|
|
132
|
+
|
|
133
|
+
US toll-free, 10DLC and Canadian numbers take replies (`two_way: True` on
|
|
134
|
+
`sms.numbers()`). Subscribe a webhook to `sms.received`, skip STOP and texts
|
|
135
|
+
Unitpost already answered, read the conversation with `participant`, and answer
|
|
136
|
+
without `from` so the reply leaves from the number they texted.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import os
|
|
140
|
+
from unitpost import Unitpost, verify_webhook
|
|
141
|
+
|
|
142
|
+
unitpost = Unitpost() # reads UNITPOST_API_KEY
|
|
143
|
+
|
|
144
|
+
# Your webhook endpoint, subscribed to sms.received. Pass the RAW body.
|
|
145
|
+
def on_webhook(raw_body: bytes, headers: dict) -> None:
|
|
146
|
+
event = verify_webhook(
|
|
147
|
+
payload=raw_body,
|
|
148
|
+
secret=os.environ["UNITPOST_WEBHOOK_SECRET"], # whsec_…
|
|
149
|
+
headers=headers,
|
|
150
|
+
)
|
|
151
|
+
if event["type"] != "sms.received":
|
|
152
|
+
return
|
|
153
|
+
reply = event["data"]
|
|
154
|
+
if reply["keyword"] == "OPT_OUT" or reply["auto_replied"]:
|
|
155
|
+
return # STOP, or already answered
|
|
156
|
+
|
|
157
|
+
# The whole conversation with this person, both directions, newest first.
|
|
158
|
+
# Other filters: direction, status, from_ (sent as `from`), to, contact_id,
|
|
159
|
+
# campaign_id, created_after, created_before.
|
|
160
|
+
history = unitpost.sms.list(participant=reply["from"], limit=20)
|
|
161
|
+
answer = your_agent(history.data["data"], reply["body"] or "")
|
|
162
|
+
|
|
163
|
+
# No "from": the answer goes out from the number they texted.
|
|
164
|
+
unitpost.sms.send({"to": reply["from"], "body": answer})
|
|
165
|
+
```
|
|
166
|
+
|
|
130
167
|
## Idempotency
|
|
131
168
|
|
|
132
|
-
`
|
|
169
|
+
`email.send` and `email.batch` are safe to retry by default — if you don't pass
|
|
133
170
|
`idempotency_key`, the SDK generates one so an automatic retry can't double-send.
|
|
134
171
|
Pass your own to dedupe across separate calls:
|
|
135
172
|
|
|
@@ -149,7 +186,7 @@ Transient failures are retried automatically: `429`, `5xx`, and transport errors
|
|
|
149
186
|
`Retry-After` is honored; otherwise exponential backoff with full jitter is used.
|
|
150
187
|
Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
|
|
151
188
|
safe to replay are retried: `GET`s and writes carrying an idempotency key (which
|
|
152
|
-
`
|
|
189
|
+
`email.send` and `email.batch` always do); any other keyless write is never
|
|
153
190
|
auto-retried.
|
|
154
191
|
|
|
155
192
|
```python
|
|
@@ -162,7 +199,8 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
|
|
|
162
199
|
| | |
|
|
163
200
|
| --- | --- |
|
|
164
201
|
| Docs + API reference | https://www.unitpost.com/docs |
|
|
165
|
-
| Product
|
|
202
|
+
| Product guides | https://www.unitpost.com/guides |
|
|
203
|
+
| SMTP | https://www.unitpost.com/docs/smtp |
|
|
166
204
|
| OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
|
|
167
205
|
| LLM index | https://www.unitpost.com/llms.txt |
|
|
168
206
|
| Dashboard / API keys | https://www.unitpost.com |
|
|
@@ -170,7 +208,7 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
|
|
|
170
208
|
| Template gallery | https://www.unitpost.com/templates/gallery |
|
|
171
209
|
| Playground | https://www.unitpost.com/playground |
|
|
172
210
|
| Migration Assistant | https://www.unitpost.com/migration |
|
|
173
|
-
| MCP + Agent Skills | https://www.unitpost.com/
|
|
211
|
+
| MCP + Agent Skills | https://www.unitpost.com/docs/ai/mcp · https://www.unitpost.com/docs/ai/skills |
|
|
174
212
|
| Support | support@unitpost.com |
|
|
175
213
|
|
|
176
214
|
## License
|
|
@@ -43,15 +43,16 @@ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
|
|
|
43
43
|
|
|
44
44
|
| Accessor | Covers |
|
|
45
45
|
| --- | --- |
|
|
46
|
-
| `
|
|
46
|
+
| `email` | Send, batch, list, get, update, stats, inbound (`received_*`) |
|
|
47
|
+
| `email.topics` | Topics + contact topic prefs |
|
|
48
|
+
| `email.campaigns` | Draft, schedule, send, pause/resume |
|
|
49
|
+
| `email.templates` | Template CRUD |
|
|
50
|
+
| `email.domains` | Domains + verify |
|
|
51
|
+
| `sms` | Send, get, list, `brands()`, `numbers()`, contact SMS consent. Available on paid plans: on the free plan `send` returns a `403` with code `limit_reached`, while the reads keep working. |
|
|
47
52
|
| `contacts` | CRUD, import jobs |
|
|
48
53
|
| `contact_fields` | Custom field definitions |
|
|
49
54
|
| `segments` | Segments + membership |
|
|
50
|
-
| `topics` | Topics + contact topic prefs |
|
|
51
|
-
| `campaigns` | Draft, schedule, send, pause/resume |
|
|
52
|
-
| `templates` | Template CRUD |
|
|
53
55
|
| `brand_kits` | Brand kit |
|
|
54
|
-
| `domains` | Domains + verify |
|
|
55
56
|
| `webhooks` | Endpoints + `verify()` |
|
|
56
57
|
| `api_keys` | API key management |
|
|
57
58
|
| `suppressions` | Suppression list |
|
|
@@ -75,7 +76,7 @@ complaints are added automatically; you can also add your own:
|
|
|
75
76
|
unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
|
|
76
77
|
unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})
|
|
77
78
|
|
|
78
|
-
# Look one up by id or email; list
|
|
79
|
+
# Look one up by id or email; list your own; un-suppress.
|
|
79
80
|
unitpost.suppressions.get("bounced@example.com")
|
|
80
81
|
for s in unitpost.suppressions.list_all():
|
|
81
82
|
print(s["email"], s["reason"])
|
|
@@ -104,9 +105,45 @@ Or from the client (Resend-style alias):
|
|
|
104
105
|
event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
## Receive a text and answer it
|
|
109
|
+
|
|
110
|
+
US toll-free, 10DLC and Canadian numbers take replies (`two_way: True` on
|
|
111
|
+
`sms.numbers()`). Subscribe a webhook to `sms.received`, skip STOP and texts
|
|
112
|
+
Unitpost already answered, read the conversation with `participant`, and answer
|
|
113
|
+
without `from` so the reply leaves from the number they texted.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import os
|
|
117
|
+
from unitpost import Unitpost, verify_webhook
|
|
118
|
+
|
|
119
|
+
unitpost = Unitpost() # reads UNITPOST_API_KEY
|
|
120
|
+
|
|
121
|
+
# Your webhook endpoint, subscribed to sms.received. Pass the RAW body.
|
|
122
|
+
def on_webhook(raw_body: bytes, headers: dict) -> None:
|
|
123
|
+
event = verify_webhook(
|
|
124
|
+
payload=raw_body,
|
|
125
|
+
secret=os.environ["UNITPOST_WEBHOOK_SECRET"], # whsec_…
|
|
126
|
+
headers=headers,
|
|
127
|
+
)
|
|
128
|
+
if event["type"] != "sms.received":
|
|
129
|
+
return
|
|
130
|
+
reply = event["data"]
|
|
131
|
+
if reply["keyword"] == "OPT_OUT" or reply["auto_replied"]:
|
|
132
|
+
return # STOP, or already answered
|
|
133
|
+
|
|
134
|
+
# The whole conversation with this person, both directions, newest first.
|
|
135
|
+
# Other filters: direction, status, from_ (sent as `from`), to, contact_id,
|
|
136
|
+
# campaign_id, created_after, created_before.
|
|
137
|
+
history = unitpost.sms.list(participant=reply["from"], limit=20)
|
|
138
|
+
answer = your_agent(history.data["data"], reply["body"] or "")
|
|
139
|
+
|
|
140
|
+
# No "from": the answer goes out from the number they texted.
|
|
141
|
+
unitpost.sms.send({"to": reply["from"], "body": answer})
|
|
142
|
+
```
|
|
143
|
+
|
|
107
144
|
## Idempotency
|
|
108
145
|
|
|
109
|
-
`
|
|
146
|
+
`email.send` and `email.batch` are safe to retry by default — if you don't pass
|
|
110
147
|
`idempotency_key`, the SDK generates one so an automatic retry can't double-send.
|
|
111
148
|
Pass your own to dedupe across separate calls:
|
|
112
149
|
|
|
@@ -126,7 +163,7 @@ Transient failures are retried automatically: `429`, `5xx`, and transport errors
|
|
|
126
163
|
`Retry-After` is honored; otherwise exponential backoff with full jitter is used.
|
|
127
164
|
Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
|
|
128
165
|
safe to replay are retried: `GET`s and writes carrying an idempotency key (which
|
|
129
|
-
`
|
|
166
|
+
`email.send` and `email.batch` always do); any other keyless write is never
|
|
130
167
|
auto-retried.
|
|
131
168
|
|
|
132
169
|
```python
|
|
@@ -139,7 +176,8 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
|
|
|
139
176
|
| | |
|
|
140
177
|
| --- | --- |
|
|
141
178
|
| Docs + API reference | https://www.unitpost.com/docs |
|
|
142
|
-
| Product
|
|
179
|
+
| Product guides | https://www.unitpost.com/guides |
|
|
180
|
+
| SMTP | https://www.unitpost.com/docs/smtp |
|
|
143
181
|
| OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
|
|
144
182
|
| LLM index | https://www.unitpost.com/llms.txt |
|
|
145
183
|
| Dashboard / API keys | https://www.unitpost.com |
|
|
@@ -147,7 +185,7 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
|
|
|
147
185
|
| Template gallery | https://www.unitpost.com/templates/gallery |
|
|
148
186
|
| Playground | https://www.unitpost.com/playground |
|
|
149
187
|
| Migration Assistant | https://www.unitpost.com/migration |
|
|
150
|
-
| MCP + Agent Skills | https://www.unitpost.com/
|
|
188
|
+
| MCP + Agent Skills | https://www.unitpost.com/docs/ai/mcp · https://www.unitpost.com/docs/ai/skills |
|
|
151
189
|
| Support | support@unitpost.com |
|
|
152
190
|
|
|
153
191
|
## License
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitpost"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -29,7 +29,9 @@ from .resources import (
|
|
|
29
29
|
Contacts,
|
|
30
30
|
Email,
|
|
31
31
|
Segments,
|
|
32
|
+
Sms,
|
|
32
33
|
Suppressions,
|
|
34
|
+
Jobs,
|
|
33
35
|
Usage,
|
|
34
36
|
Webhooks,
|
|
35
37
|
)
|
|
@@ -62,6 +64,9 @@ class Unitpost:
|
|
|
62
64
|
)
|
|
63
65
|
self._http = http
|
|
64
66
|
self.email = Email(http)
|
|
67
|
+
# SMS channel. Available on paid plans: on the free plan `send` returns
|
|
68
|
+
# a 403 with code `limit_reached`; reads keep working.
|
|
69
|
+
self.sms = Sms(http)
|
|
65
70
|
self.contacts = Contacts(http)
|
|
66
71
|
self.contact_fields = ContactFields(http)
|
|
67
72
|
self.segments = Segments(http)
|
|
@@ -71,6 +76,7 @@ class Unitpost:
|
|
|
71
76
|
self.suppressions = Suppressions(http)
|
|
72
77
|
# NOTE (pre-launch): `events` is intentionally absent while the
|
|
73
78
|
# custom-events surface is behind the launch gate.
|
|
79
|
+
self.jobs = Jobs(http)
|
|
74
80
|
self.usage = Usage(http)
|
|
75
81
|
|
|
76
82
|
def close(self) -> None:
|