Skip to content

Commit 3158ddc

Browse files
authored
feat: add typed Python SDK (#213)
1 parent 92f56f1 commit 3158ddc

15 files changed

Lines changed: 2054 additions & 2 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Publish Python package
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- "packages/python-sdk/**" # Trigger only changes within sdk/python
9+
- ".github/workflows/release-python-package.yml"
10+
11+
concurrency: ${{ github.workflow }}-${{ github.ref }}
12+
13+
jobs:
14+
release:
15+
runs-on: ubuntu-latest
16+
defaults:
17+
run:
18+
working-directory: packages/python-sdk
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Set up Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.11"
26+
27+
- uses: snok/install-poetry@v1
28+
with:
29+
version: "1.8.2"
30+
virtualenvs-create: false
31+
32+
- name: Build package with Poetry
33+
run: poetry build --no-interaction
34+
35+
- name: List contents of dist
36+
run: ls -la dist
37+
38+
- name: Publish to PyPI
39+
uses: pypa/gh-action-pypi-publish@v1.4.2
40+
with:
41+
user: __token__
42+
password: ${{ secrets.PYPI_API_TOKEN }}
43+
packages_dir: packages/python-sdk/dist

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ yarn-error.log*
4040
*.pem
4141
prod_db.tar
4242

43-
bin
43+
bin
44+
__pycache__

apps/docs/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"pages": [
2828
"introduction",
2929
"get-started/nodejs",
30+
"get-started/python",
3031
"get-started/local",
3132
"get-started/self-hosting",
3233
"get-started/smtp"
@@ -138,4 +139,4 @@
138139
"github": "https://github.com/usesend"
139140
}
140141
}
141-
}
142+
}

apps/docs/get-started/python.mdx

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
title: Python SDK
3+
description: Official UseSend Python SDK for sending emails and managing contacts.
4+
icon: python
5+
---
6+
7+
This guide shows how to install and use the official `usesend` Python SDK.
8+
9+
## Installation
10+
11+
Install from PyPI:
12+
13+
```bash
14+
pip install usesend
15+
```
16+
17+
## Initialize
18+
19+
```python
20+
from usesend import UseSend, types
21+
22+
23+
# Option A: pass values directly (helpful in scripts/tests)
24+
client = UseSend("us_xxx")
25+
26+
# Option B: custom base URL (self-hosted)
27+
client = UseSend("us_xxx", url="https://your-domain.example")
28+
```
29+
30+
## Send an email
31+
32+
`EmailCreate` is a TypedDict for editor hints; at runtime you pass a regular dict. The client accepts `from` or `from_` (it normalizes `from_` to `from`).
33+
34+
```python
35+
from usesend import UseSend, types
36+
37+
client = UseSend("us_xxx")
38+
39+
payload: types.EmailCreate = {
40+
"to": "user@example.com",
41+
"from": "no-reply@yourdomain.com",
42+
"subject": "Welcome",
43+
"html": "<strong>Hello!</strong>",
44+
}
45+
46+
data, err = client.emails.send(payload)
47+
print(data or err)
48+
```
49+
50+
Attachments and scheduling:
51+
52+
```python
53+
from datetime import datetime, timedelta
54+
55+
payload: types.EmailCreate = {
56+
"to": ["user1@example.com", "user2@example.com"],
57+
"from": "no-reply@yourdomain.com",
58+
"subject": "Report",
59+
"text": "See attached.",
60+
"attachments": [
61+
{"filename": "report.txt", "content": "SGVsbG8gd29ybGQ="}, # base64
62+
],
63+
"scheduledAt": datetime.utcnow() + timedelta(minutes=10),
64+
}
65+
data, err = client.emails.create(payload)
66+
```
67+
68+
## Batch send
69+
70+
```python
71+
items: list[types.EmailBatchItem] = [
72+
{"to": "a@example.com", "from": "no-reply@yourdomain.com", "subject": "A", "html": "<p>A</p>"},
73+
{"to": "b@example.com", "from": "no-reply@yourdomain.com", "subject": "B", "html": "<p>B</p>"},
74+
]
75+
data, err = client.emails.batch(items)
76+
```
77+
78+
## Retrieve and manage emails
79+
80+
Get an email:
81+
82+
```python
83+
email, err = client.emails.get("email_123")
84+
```
85+
86+
Update schedule time:
87+
88+
```python
89+
from datetime import datetime, timedelta
90+
91+
update: types.EmailUpdate = {"scheduledAt": datetime.utcnow() + timedelta(hours=1)}
92+
data, err = client.emails.update("email_123", update)
93+
```
94+
95+
Cancel a scheduled email:
96+
97+
```python
98+
data, err = client.emails.cancel("email_123")
99+
```
100+
101+
## Contacts
102+
103+
All contact operations require a contact book ID (`book_id`).
104+
105+
Create a contact:
106+
107+
```python
108+
create: types.ContactCreate = {
109+
"email": "user@example.com",
110+
"firstName": "Jane",
111+
"properties": {"plan": "pro"},
112+
}
113+
data, err = client.contacts.create("book_123", create)
114+
```
115+
116+
Get a contact:
117+
118+
```python
119+
contact, err = client.contacts.get("book_123", "contact_456")
120+
```
121+
122+
Update a contact:
123+
124+
```python
125+
update: types.ContactUpdate = {"subscribed": False}
126+
data, err = client.contacts.update("book_123", "contact_456", update)
127+
```
128+
129+
Upsert a contact:
130+
131+
```python
132+
upsert: types.ContactUpsert = {
133+
"email": "user@example.com",
134+
"firstName": "Jane",
135+
}
136+
data, err = client.contacts.upsert("book_123", "contact_456", upsert)
137+
```
138+
139+
Delete a contact:
140+
141+
```python
142+
data, err = client.contacts.delete(book_id="book_123", contact_id="contact_456")
143+
```
144+
145+
## Error handling
146+
147+
By default the client raises `UseSendHTTPError` for non-2xx responses. To handle errors as return values, pass `raise_on_error=False`.
148+
149+
```python
150+
from usesend import UseSend, UseSendHTTPError
151+
152+
# Raises exceptions on errors (default)
153+
client = UseSend("us_xxx")
154+
try:
155+
data, _ = client.emails.get("email_123")
156+
except UseSendHTTPError as e:
157+
print("request failed:", e)
158+
159+
# Returns (None, error) instead of raising
160+
client = UseSend("us_xxx", raise_on_error=False)
161+
data, err = client.emails.get("email_123")
162+
if err:
163+
print("error:", err)
164+
```

apps/docs/introduction.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ Quicklinks to set up your account and get started
3737
href="/get-started/nodejs"
3838
>
3939
Learn how to use our SDK using NodeJS to send emails programmatically.
40+
</Card>
41+
<Card
42+
title="Python"
43+
icon="python"
44+
href="/get-started/python"
45+
>
46+
Learn how to use our SDK using Python to send emails programmatically.
4047
</Card>
4148
<Card
4249
title="SMTP support"

packages/python-sdk/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 UseSend
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/python-sdk/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# UseSend Python SDK
2+
3+
A minimal Python SDK for the [UseSend](https://usesend.com) API, mirroring the structure of the JavaScript SDK.
4+
5+
## Installation
6+
7+
Install via pip or Poetry:
8+
9+
```
10+
pip install usesend
11+
# or
12+
poetry add usesend
13+
```
14+
15+
## Usage
16+
17+
```python
18+
from usesend import UseSend, types
19+
20+
# By default: raises UseSendHTTPError on non-2xx.
21+
client = UseSend("us_123")
22+
23+
# 1) TypedDict payload (autocomplete in IDEs). Use dict to pass 'from'.
24+
payload: types.EmailCreate = {
25+
"to": "test@example.com",
26+
"from": "no-reply@example.com",
27+
"subject": "Hello",
28+
"html": "<strong>Hi!</strong>",
29+
}
30+
resp, _ = client.emails.send(payload=payload)
31+
32+
# 2) Or pass a plain dict (supports 'from')
33+
resp, _ = client.emails.send(payload={
34+
"to": "test@example.com",
35+
"from": "no-reply@example.com",
36+
"subject": "Hello",
37+
"html": "<strong>Hi!</strong>",
38+
})
39+
40+
# Toggle behavior if desired:
41+
# - raise_on_error=False: return (None, error_dict) instead of raising
42+
# No model parsing occurs; methods return plain dicts following the typed shapes.
43+
client = UseSend("us_123", raise_on_error=False)
44+
raw, err = client.emails.get(email_id="email_123")
45+
if err:
46+
print("error:", err)
47+
else:
48+
print("ok:", raw)
49+
```
50+
51+
## Development
52+
53+
This package is managed with Poetry. Models are maintained in-repo under
54+
`usesend/types.py` (readable names). Update this file as the API evolves.
55+
56+
It is published as `usesend` on PyPI.
57+
58+
Notes
59+
60+
- Human-friendly models are available under `usesend.types` (e.g., `EmailCreate`, `Contact`, `APIError`).
61+
- Endpoint methods accept TypedDict payloads or plain dicts via the `payload=` keyword.

0 commit comments

Comments
 (0)