unitpost 0.0.0__tar.gz → 0.2.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.
@@ -0,0 +1,7 @@
1
+ .venv
2
+ __pycache__
3
+ *.egg-info
4
+ dist
5
+ build
6
+ .pytest_cache
7
+ .mypy_cache
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: unitpost
3
+ Version: 0.2.0
4
+ Summary: Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage.
5
+ Project-URL: Homepage, https://www.unitpost.com
6
+ Project-URL: Documentation, https://www.unitpost.com/docs
7
+ Project-URL: Repository, https://github.com/unitpostcom/unitpost
8
+ Project-URL: Issues, https://github.com/unitpostcom/unitpost/issues
9
+ Author: Unitpost
10
+ License: MIT
11
+ Keywords: api,email,sdk,transactional-email,unitpost
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.8
16
+ Requires-Dist: httpx>=0.28.1
17
+ Requires-Dist: typing-extensions>=4.16.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: mypy>=2.3.0; extra == 'dev'
20
+ Requires-Dist: pytest>=9.1.1; extra == 'dev'
21
+ Requires-Dist: respx>=0.23.1; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # unitpost
25
+
26
+ Official [Unitpost](https://www.unitpost.com) SDK for Python. Send email and manage
27
+ contacts, segments, campaigns, templates, domains, suppressions, and webhooks.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install unitpost
33
+ ```
34
+
35
+ Requires Python 3.8+.
36
+
37
+ ## Quick start
38
+
39
+ ```python
40
+ from unitpost import Unitpost
41
+
42
+ unitpost = Unitpost() # reads UNITPOST_API_KEY (or pass Unitpost("up_..."))
43
+
44
+ result = unitpost.email.send({
45
+ "from": "Acme <you@yourdomain.com>", # bare address works too
46
+ "to": "customer@example.com",
47
+ "subject": "Hello",
48
+ "html": "<p>It works!</p>",
49
+ })
50
+
51
+ if result.error:
52
+ print(result.error.code, result.error.message)
53
+ else:
54
+ print("sent", result.data["id"])
55
+ ```
56
+
57
+ Every method returns a `Result` with `.data` and `.error`. An API-level failure
58
+ populates `.error` (a `UnitpostError`) instead of raising.
59
+
60
+ ## Authentication
61
+
62
+ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
63
+ [unitpost.com → Settings → API keys](https://www.unitpost.com).
64
+
65
+ ## Client surface
66
+
67
+ | Accessor | Covers |
68
+ | --- | --- |
69
+ | `emails` | Send, batch, list, get, update, stats, inbound (`received_*`) |
70
+ | `contacts` | CRUD, import jobs |
71
+ | `contact_fields` | Custom field definitions |
72
+ | `segments` | Segments + membership |
73
+ | `topics` | Topics + contact topic prefs |
74
+ | `campaigns` | Draft, schedule, send, pause/resume |
75
+ | `templates` | Template CRUD |
76
+ | `brand_kits` | Brand kit |
77
+ | `domains` | Domains + verify |
78
+ | `webhooks` | Endpoints + `verify()` |
79
+ | `api_keys` | API key management |
80
+ | `suppressions` | Suppression list |
81
+ | `usage` | Billing-period usage snapshot |
82
+
83
+ ## Pagination
84
+
85
+ ```python
86
+ for contact in unitpost.contacts.list_all():
87
+ print(contact["email"])
88
+ ```
89
+
90
+ ## Suppressions
91
+
92
+ The suppression list holds addresses excluded from every send — single, batch,
93
+ and campaign, including cc/bcc — case-insensitively. Hard bounces and spam
94
+ complaints are added automatically; you can also add your own:
95
+
96
+ ```python
97
+ # Suppress one address, or many in a single call (idempotent — re-adding is a no-op).
98
+ unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
99
+ unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})
100
+
101
+ # Look one up by id or email; list (optionally filter scope); un-suppress.
102
+ unitpost.suppressions.get("bounced@example.com")
103
+ for s in unitpost.suppressions.list_all():
104
+ print(s["email"], s["reason"])
105
+ unitpost.suppressions.delete("bounced@example.com")
106
+ ```
107
+
108
+ `reason` accepts `manual`, `import`, or `api` — the engine-written `bounce` and
109
+ `complaint` reasons are read-only. Unitpost-wide (`platform`) entries are visible
110
+ but can't be removed via the API.
111
+
112
+ ## Verifying webhooks
113
+
114
+ ```python
115
+ from unitpost import verify_webhook
116
+
117
+ event = verify_webhook(
118
+ payload=raw_body, # the raw request body (str or bytes)
119
+ secret=os.environ["UNITPOST_WEBHOOK_SECRET"],
120
+ headers=request.headers,
121
+ )
122
+ ```
123
+
124
+ Or from the client (Resend-style alias):
125
+
126
+ ```python
127
+ event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
128
+ ```
129
+
130
+ ## Idempotency
131
+
132
+ `emails.send` and `emails.batch` are safe to retry by default — if you don't pass
133
+ `idempotency_key`, the SDK generates one so an automatic retry can't double-send.
134
+ Pass your own to dedupe across separate calls:
135
+
136
+ ```python
137
+ unitpost.email.send({...}, idempotency_key="...")
138
+ unitpost.email.batch([...], idempotency_key="...")
139
+ ```
140
+
141
+ > The server dedupes on `(workspace, idempotency key)` for 24h and replays the
142
+ > original result instead of re-sending (the email id for a send, the full list
143
+ > envelope for a batch).
144
+
145
+ ## Retries
146
+
147
+ Transient failures are retried automatically: `429`, `5xx`, and transport errors
148
+ (timeouts / network) retry up to `max_retries` times (default `2`). A `429`/`503`
149
+ `Retry-After` is honored; otherwise exponential backoff with full jitter is used.
150
+ Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
151
+ safe to replay are retried: `GET`s and writes carrying an idempotency key (which
152
+ `emails.send` and `emails.batch` always do); any other keyless write is never
153
+ auto-retried.
154
+
155
+ ```python
156
+ # Defaults shown; set max_retries=0 to disable.
157
+ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
158
+ ```
159
+
160
+ ## Resources
161
+
162
+ | | |
163
+ | --- | --- |
164
+ | Docs + API reference | https://www.unitpost.com/docs |
165
+ | Product / SMTP / MCP guides | https://www.unitpost.com/guides |
166
+ | OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
167
+ | LLM index | https://www.unitpost.com/llms.txt |
168
+ | Dashboard / API keys | https://www.unitpost.com |
169
+ | Email components (`@unitpost/email`) | https://www.unitpost.com/components |
170
+ | Template gallery | https://www.unitpost.com/templates/gallery |
171
+ | Playground | https://www.unitpost.com/playground |
172
+ | Migration Assistant | https://www.unitpost.com/migration |
173
+ | MCP + Agent Skills | https://www.unitpost.com/guides#ai-mcp · https://www.unitpost.com/guides#ai-skills |
174
+ | Support | support@unitpost.com |
175
+
176
+ ## License
177
+
178
+ MIT
@@ -0,0 +1,155 @@
1
+ # unitpost
2
+
3
+ Official [Unitpost](https://www.unitpost.com) SDK for Python. Send email and manage
4
+ contacts, segments, campaigns, templates, domains, suppressions, and webhooks.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install unitpost
10
+ ```
11
+
12
+ Requires Python 3.8+.
13
+
14
+ ## Quick start
15
+
16
+ ```python
17
+ from unitpost import Unitpost
18
+
19
+ unitpost = Unitpost() # reads UNITPOST_API_KEY (or pass Unitpost("up_..."))
20
+
21
+ result = unitpost.email.send({
22
+ "from": "Acme <you@yourdomain.com>", # bare address works too
23
+ "to": "customer@example.com",
24
+ "subject": "Hello",
25
+ "html": "<p>It works!</p>",
26
+ })
27
+
28
+ if result.error:
29
+ print(result.error.code, result.error.message)
30
+ else:
31
+ print("sent", result.data["id"])
32
+ ```
33
+
34
+ Every method returns a `Result` with `.data` and `.error`. An API-level failure
35
+ populates `.error` (a `UnitpostError`) instead of raising.
36
+
37
+ ## Authentication
38
+
39
+ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
40
+ [unitpost.com → Settings → API keys](https://www.unitpost.com).
41
+
42
+ ## Client surface
43
+
44
+ | Accessor | Covers |
45
+ | --- | --- |
46
+ | `emails` | Send, batch, list, get, update, stats, inbound (`received_*`) |
47
+ | `contacts` | CRUD, import jobs |
48
+ | `contact_fields` | Custom field definitions |
49
+ | `segments` | Segments + membership |
50
+ | `topics` | Topics + contact topic prefs |
51
+ | `campaigns` | Draft, schedule, send, pause/resume |
52
+ | `templates` | Template CRUD |
53
+ | `brand_kits` | Brand kit |
54
+ | `domains` | Domains + verify |
55
+ | `webhooks` | Endpoints + `verify()` |
56
+ | `api_keys` | API key management |
57
+ | `suppressions` | Suppression list |
58
+ | `usage` | Billing-period usage snapshot |
59
+
60
+ ## Pagination
61
+
62
+ ```python
63
+ for contact in unitpost.contacts.list_all():
64
+ print(contact["email"])
65
+ ```
66
+
67
+ ## Suppressions
68
+
69
+ The suppression list holds addresses excluded from every send — single, batch,
70
+ and campaign, including cc/bcc — case-insensitively. Hard bounces and spam
71
+ complaints are added automatically; you can also add your own:
72
+
73
+ ```python
74
+ # Suppress one address, or many in a single call (idempotent — re-adding is a no-op).
75
+ unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
76
+ unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})
77
+
78
+ # Look one up by id or email; list (optionally filter scope); un-suppress.
79
+ unitpost.suppressions.get("bounced@example.com")
80
+ for s in unitpost.suppressions.list_all():
81
+ print(s["email"], s["reason"])
82
+ unitpost.suppressions.delete("bounced@example.com")
83
+ ```
84
+
85
+ `reason` accepts `manual`, `import`, or `api` — the engine-written `bounce` and
86
+ `complaint` reasons are read-only. Unitpost-wide (`platform`) entries are visible
87
+ but can't be removed via the API.
88
+
89
+ ## Verifying webhooks
90
+
91
+ ```python
92
+ from unitpost import verify_webhook
93
+
94
+ event = verify_webhook(
95
+ payload=raw_body, # the raw request body (str or bytes)
96
+ secret=os.environ["UNITPOST_WEBHOOK_SECRET"],
97
+ headers=request.headers,
98
+ )
99
+ ```
100
+
101
+ Or from the client (Resend-style alias):
102
+
103
+ ```python
104
+ event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
105
+ ```
106
+
107
+ ## Idempotency
108
+
109
+ `emails.send` and `emails.batch` are safe to retry by default — if you don't pass
110
+ `idempotency_key`, the SDK generates one so an automatic retry can't double-send.
111
+ Pass your own to dedupe across separate calls:
112
+
113
+ ```python
114
+ unitpost.email.send({...}, idempotency_key="...")
115
+ unitpost.email.batch([...], idempotency_key="...")
116
+ ```
117
+
118
+ > The server dedupes on `(workspace, idempotency key)` for 24h and replays the
119
+ > original result instead of re-sending (the email id for a send, the full list
120
+ > envelope for a batch).
121
+
122
+ ## Retries
123
+
124
+ Transient failures are retried automatically: `429`, `5xx`, and transport errors
125
+ (timeouts / network) retry up to `max_retries` times (default `2`). A `429`/`503`
126
+ `Retry-After` is honored; otherwise exponential backoff with full jitter is used.
127
+ Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
128
+ safe to replay are retried: `GET`s and writes carrying an idempotency key (which
129
+ `emails.send` and `emails.batch` always do); any other keyless write is never
130
+ auto-retried.
131
+
132
+ ```python
133
+ # Defaults shown; set max_retries=0 to disable.
134
+ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
135
+ ```
136
+
137
+ ## Resources
138
+
139
+ | | |
140
+ | --- | --- |
141
+ | Docs + API reference | https://www.unitpost.com/docs |
142
+ | Product / SMTP / MCP guides | https://www.unitpost.com/guides |
143
+ | OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
144
+ | LLM index | https://www.unitpost.com/llms.txt |
145
+ | Dashboard / API keys | https://www.unitpost.com |
146
+ | Email components (`@unitpost/email`) | https://www.unitpost.com/components |
147
+ | Template gallery | https://www.unitpost.com/templates/gallery |
148
+ | Playground | https://www.unitpost.com/playground |
149
+ | Migration Assistant | https://www.unitpost.com/migration |
150
+ | MCP + Agent Skills | https://www.unitpost.com/guides#ai-mcp · https://www.unitpost.com/guides#ai-skills |
151
+ | Support | support@unitpost.com |
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@unitpost/sdk-python",
3
+ "version": "0.1.0",
4
+ "description": "Workspace shim for the Python SDK (published to PyPI as `unitpost`). Lets Turborepo run its lint/test tasks; the real package lives under src/unitpost.",
5
+ "private": true,
6
+ "scripts": {
7
+ "test": "command -v python3 >/dev/null 2>&1 && python3 -c 'import httpx' >/dev/null 2>&1 && python3 -m pytest -q || echo 'skip: python/httpx not available (run in the dedicated sdks CI job)'",
8
+ "typecheck": "python3 -m mypy src/unitpost || true",
9
+ "build": "command -v python3 >/dev/null 2>&1 && python3 -c 'import build' >/dev/null 2>&1 && python3 -m build || echo 'skip: python build backend not available (the real sdist/wheel is built in publish-sdk-python.yml)'"
10
+ }
11
+ }
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "unitpost"
7
+ version = "0.2.0"
8
+ description = "Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Unitpost" }]
13
+ keywords = ["unitpost", "email", "transactional-email", "sdk", "api"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "httpx>=0.28.1",
21
+ "typing_extensions>=4.16.0",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://www.unitpost.com"
26
+ Documentation = "https://www.unitpost.com/docs"
27
+ Repository = "https://github.com/unitpostcom/unitpost"
28
+ Issues = "https://github.com/unitpostcom/unitpost/issues"
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest>=9.1.1", "respx>=0.23.1", "mypy>=2.3.0"]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/unitpost"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+
39
+ [tool.mypy]
40
+ python_version = "3.8"
41
+ strict = false
@@ -0,0 +1,16 @@
1
+ """unitpost — Official Unitpost SDK for Python."""
2
+
3
+ from ._client import Unitpost
4
+ from ._http import Result, UnitpostError, SDK_VERSION
5
+ from ._webhooks import verify_webhook, WebhookVerificationError
6
+
7
+ __all__ = [
8
+ "Unitpost",
9
+ "Result",
10
+ "UnitpostError",
11
+ "WebhookVerificationError",
12
+ "verify_webhook",
13
+ "SDK_VERSION",
14
+ ]
15
+
16
+ __version__ = SDK_VERSION
@@ -0,0 +1,83 @@
1
+ """The Unitpost client. Construct once, reuse everywhere.
2
+
3
+ from unitpost import Unitpost
4
+
5
+ unitpost = Unitpost() # reads UNITPOST_API_KEY
6
+ result = unitpost.email.send({
7
+ "from": "Acme <you@yourdomain.com>",
8
+ "to": "customer@example.com",
9
+ "subject": "Hello",
10
+ "html": "<p>It works!</p>",
11
+ })
12
+ if result.error:
13
+ print(result.error.code, result.error.message)
14
+ else:
15
+ print("sent", result.data["id"])
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Callable, Optional
21
+
22
+ import httpx
23
+
24
+ from ._http import HttpClient
25
+ from .resources import (
26
+ ApiKeys,
27
+ BrandKits,
28
+ ContactFields,
29
+ Contacts,
30
+ Email,
31
+ Segments,
32
+ Suppressions,
33
+ Usage,
34
+ Webhooks,
35
+ )
36
+
37
+
38
+ class Unitpost:
39
+ def __init__(
40
+ self,
41
+ api_key: Optional[str] = None,
42
+ *,
43
+ base_url: Optional[str] = None,
44
+ timeout: float = 30.0,
45
+ headers: Optional[dict] = None,
46
+ transport: Optional[httpx.BaseTransport] = None,
47
+ max_retries: int = 2,
48
+ retry_base_delay: float = 0.5,
49
+ max_retry_delay: float = 20.0,
50
+ sleep: Optional[Callable[[float], None]] = None,
51
+ ) -> None:
52
+ http = HttpClient(
53
+ api_key,
54
+ base_url=base_url,
55
+ timeout=timeout,
56
+ headers=headers,
57
+ transport=transport,
58
+ max_retries=max_retries,
59
+ retry_base_delay=retry_base_delay,
60
+ max_retry_delay=max_retry_delay,
61
+ sleep=sleep,
62
+ )
63
+ self._http = http
64
+ self.email = Email(http)
65
+ self.contacts = Contacts(http)
66
+ self.contact_fields = ContactFields(http)
67
+ self.segments = Segments(http)
68
+ self.brand_kits = BrandKits(http)
69
+ self.webhooks = Webhooks(http)
70
+ self.api_keys = ApiKeys(http)
71
+ self.suppressions = Suppressions(http)
72
+ # NOTE (pre-launch): `events` is intentionally absent while the
73
+ # custom-events surface is behind the launch gate.
74
+ self.usage = Usage(http)
75
+
76
+ def close(self) -> None:
77
+ self._http.close()
78
+
79
+ def __enter__(self) -> "Unitpost":
80
+ return self
81
+
82
+ def __exit__(self, *exc: object) -> None:
83
+ self.close()
@@ -0,0 +1,5 @@
1
+ # Code generated by @unitpost/sdk-codegen from the public OpenAPI spec.
2
+ # DO NOT EDIT BY HAND. Run `npm run sdk:generate` to regenerate.
3
+ # Source of truth: apps/web/docs/openapi.json (itself derived from the API's
4
+ # Zod schemas and CI-gated for drift).
5
+