sendkit 1.0.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.
- sendkit-1.0.0/.github/workflows/tests.yml +62 -0
- sendkit-1.0.0/.gitignore +14 -0
- sendkit-1.0.0/CLAUDE.md +38 -0
- sendkit-1.0.0/PKG-INFO +90 -0
- sendkit-1.0.0/README.md +69 -0
- sendkit-1.0.0/pyproject.toml +31 -0
- sendkit-1.0.0/src/sendkit/__init__.py +4 -0
- sendkit-1.0.0/src/sendkit/client.py +63 -0
- sendkit-1.0.0/src/sendkit/emails.py +77 -0
- sendkit-1.0.0/src/sendkit/errors.py +14 -0
- sendkit-1.0.0/tests/__init__.py +0 -0
- sendkit-1.0.0/tests/test_sendkit.py +174 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ['[0-9]*']
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
tests:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python: ['3.10', '3.11', '3.12', '3.13']
|
|
17
|
+
|
|
18
|
+
name: Python ${{ matrix.python }}
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Setup Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python }}
|
|
27
|
+
|
|
28
|
+
- name: Install package and test dependencies
|
|
29
|
+
run: pip install -e . && pip install pytest
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: python -m pytest -v
|
|
33
|
+
|
|
34
|
+
release:
|
|
35
|
+
needs: tests
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
38
|
+
permissions:
|
|
39
|
+
contents: write
|
|
40
|
+
id-token: write
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- name: Create GitHub Release
|
|
46
|
+
run: gh release create ${{ github.ref_name }} --generate-notes
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
|
|
50
|
+
- name: Setup Python
|
|
51
|
+
uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: '3.13'
|
|
54
|
+
|
|
55
|
+
- name: Install build tools
|
|
56
|
+
run: pip install build
|
|
57
|
+
|
|
58
|
+
- name: Build package
|
|
59
|
+
run: python -m build
|
|
60
|
+
|
|
61
|
+
- name: Publish to PyPI
|
|
62
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
sendkit-1.0.0/.gitignore
ADDED
sendkit-1.0.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SendKit Python SDK
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Python SDK for the SendKit email API. Uses `urllib` (stdlib), zero external dependencies.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/sendkit/
|
|
11
|
+
├── __init__.py # Public exports (SendKit, SendKitError)
|
|
12
|
+
├── client.py # SendKit client: holds API key, _post() method
|
|
13
|
+
├── emails.py # Emails service (send, send_mime)
|
|
14
|
+
└── errors.py # SendKitError exception class
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `SendKit` class is the entry point, accepts api_key + optional base_url
|
|
18
|
+
- `client.emails` exposes email operations
|
|
19
|
+
- Uses keyword-only arguments for send() params
|
|
20
|
+
- `from_` parameter (trailing underscore) avoids Python reserved word conflict
|
|
21
|
+
- API errors raise `SendKitError` with name, message, status_code
|
|
22
|
+
- `POST /v1/emails` for structured emails, `POST /v1/emails/mime` for raw MIME
|
|
23
|
+
|
|
24
|
+
## Testing
|
|
25
|
+
|
|
26
|
+
- Tests use `unittest` + `http.server` for mock HTTP servers
|
|
27
|
+
- Run tests: `python -m pytest`
|
|
28
|
+
- No external test dependencies beyond pytest
|
|
29
|
+
|
|
30
|
+
## Releasing
|
|
31
|
+
|
|
32
|
+
- Tags use numeric format: `1.0.0` (no `v` prefix)
|
|
33
|
+
- CI runs tests on Python 3.10, 3.11, 3.12, 3.13
|
|
34
|
+
- Pushing a tag creates GitHub Release + publishes to PyPI via trusted publishing
|
|
35
|
+
|
|
36
|
+
## Git
|
|
37
|
+
|
|
38
|
+
- NEVER add `Co-Authored-By` lines to commit messages
|
sendkit-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sendkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the SendKit email API
|
|
5
|
+
Project-URL: Homepage, https://sendkit.com
|
|
6
|
+
Project-URL: Repository, https://github.com/sendkitdev/sendkit-python
|
|
7
|
+
Project-URL: Issues, https://github.com/sendkitdev/sendkit-python/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: api,email,sdk,sendkit
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# SendKit Python SDK
|
|
23
|
+
|
|
24
|
+
Official Python SDK for the [SendKit](https://sendkit.com) email API.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install sendkit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Create a Client
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from sendkit import SendKit
|
|
38
|
+
|
|
39
|
+
client = SendKit("sk_your_api_key")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Send an Email
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
result = client.emails.send(
|
|
46
|
+
from_="you@example.com",
|
|
47
|
+
to="recipient@example.com",
|
|
48
|
+
subject="Hello from SendKit",
|
|
49
|
+
html="<h1>Welcome!</h1>",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
print(result["id"])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Send a MIME Email
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
result = client.emails.send_mime(
|
|
59
|
+
envelope_from="you@example.com",
|
|
60
|
+
envelope_to="recipient@example.com",
|
|
61
|
+
raw_message=mime_string,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Error Handling
|
|
66
|
+
|
|
67
|
+
API errors raise `SendKitError`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from sendkit import SendKit, SendKitError
|
|
71
|
+
|
|
72
|
+
client = SendKit("sk_your_api_key")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
client.emails.send(...)
|
|
76
|
+
except SendKitError as e:
|
|
77
|
+
print(e.name) # e.g. "validation_error"
|
|
78
|
+
print(e.message) # e.g. "The to field is required."
|
|
79
|
+
print(e.status_code) # e.g. 422
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Configuration
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# Read API key from SENDKIT_API_KEY environment variable
|
|
86
|
+
client = SendKit()
|
|
87
|
+
|
|
88
|
+
# Custom base URL
|
|
89
|
+
client = SendKit("sk_...", base_url="https://custom.api.com")
|
|
90
|
+
```
|
sendkit-1.0.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# SendKit Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [SendKit](https://sendkit.com) email API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sendkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Create a Client
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from sendkit import SendKit
|
|
17
|
+
|
|
18
|
+
client = SendKit("sk_your_api_key")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Send an Email
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
result = client.emails.send(
|
|
25
|
+
from_="you@example.com",
|
|
26
|
+
to="recipient@example.com",
|
|
27
|
+
subject="Hello from SendKit",
|
|
28
|
+
html="<h1>Welcome!</h1>",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
print(result["id"])
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Send a MIME Email
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
result = client.emails.send_mime(
|
|
38
|
+
envelope_from="you@example.com",
|
|
39
|
+
envelope_to="recipient@example.com",
|
|
40
|
+
raw_message=mime_string,
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Error Handling
|
|
45
|
+
|
|
46
|
+
API errors raise `SendKitError`:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from sendkit import SendKit, SendKitError
|
|
50
|
+
|
|
51
|
+
client = SendKit("sk_your_api_key")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
client.emails.send(...)
|
|
55
|
+
except SendKitError as e:
|
|
56
|
+
print(e.name) # e.g. "validation_error"
|
|
57
|
+
print(e.message) # e.g. "The to field is required."
|
|
58
|
+
print(e.status_code) # e.g. 422
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Configuration
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Read API key from SENDKIT_API_KEY environment variable
|
|
65
|
+
client = SendKit()
|
|
66
|
+
|
|
67
|
+
# Custom base URL
|
|
68
|
+
client = SendKit("sk_...", base_url="https://custom.api.com")
|
|
69
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sendkit"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for the SendKit email API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["sendkit", "email", "api", "sdk"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://sendkit.com"
|
|
27
|
+
Repository = "https://github.com/sendkitdev/sendkit-python"
|
|
28
|
+
Issues = "https://github.com/sendkitdev/sendkit-python/issues"
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
from .emails import Emails
|
|
10
|
+
from .errors import SendKitError
|
|
11
|
+
|
|
12
|
+
_DEFAULT_BASE_URL = "https://api.sendkit.com"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SendKit:
|
|
16
|
+
"""SendKit API client.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
api_key: Your SendKit API key. If not provided, reads from
|
|
20
|
+
the ``SENDKIT_API_KEY`` environment variable.
|
|
21
|
+
base_url: Override the API base URL.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, api_key: str | None = None, *, base_url: str = _DEFAULT_BASE_URL) -> None:
|
|
25
|
+
self.api_key = api_key or os.environ.get("SENDKIT_API_KEY", "")
|
|
26
|
+
|
|
27
|
+
if not self.api_key:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
'Missing API key. Pass it to the constructor `SendKit("sk_...")` '
|
|
30
|
+
"or set the SENDKIT_API_KEY environment variable."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
self.base_url = base_url
|
|
34
|
+
self.emails = Emails(self)
|
|
35
|
+
|
|
36
|
+
def _post(self, path: str, body: dict[str, Any]) -> Any:
|
|
37
|
+
data = json.dumps(body).encode()
|
|
38
|
+
req = Request(
|
|
39
|
+
f"{self.base_url}{path}",
|
|
40
|
+
data=data,
|
|
41
|
+
method="POST",
|
|
42
|
+
headers={
|
|
43
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with urlopen(req) as resp:
|
|
50
|
+
return json.loads(resp.read())
|
|
51
|
+
except HTTPError as exc:
|
|
52
|
+
try:
|
|
53
|
+
error_body = json.loads(exc.read())
|
|
54
|
+
raise SendKitError(
|
|
55
|
+
message=error_body.get("message", exc.reason),
|
|
56
|
+
status_code=exc.code,
|
|
57
|
+
name=error_body.get("name", "application_error"),
|
|
58
|
+
) from None
|
|
59
|
+
except (json.JSONDecodeError, AttributeError):
|
|
60
|
+
raise SendKitError(
|
|
61
|
+
message=str(exc.reason),
|
|
62
|
+
status_code=exc.code,
|
|
63
|
+
) from None
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .client import SendKit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Emails:
|
|
10
|
+
"""Handles communication with the email related methods of the SendKit API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client: SendKit) -> None:
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def send(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
from_: str,
|
|
19
|
+
to: str | list[str],
|
|
20
|
+
subject: str,
|
|
21
|
+
html: str | None = None,
|
|
22
|
+
text: str | None = None,
|
|
23
|
+
cc: str | list[str] | None = None,
|
|
24
|
+
bcc: str | list[str] | None = None,
|
|
25
|
+
reply_to: str | None = None,
|
|
26
|
+
headers: dict[str, str] | None = None,
|
|
27
|
+
tags: list[str] | None = None,
|
|
28
|
+
scheduled_at: str | None = None,
|
|
29
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
30
|
+
) -> dict[str, str]:
|
|
31
|
+
"""Send a structured email.
|
|
32
|
+
|
|
33
|
+
Returns a dict with the email ``id``.
|
|
34
|
+
"""
|
|
35
|
+
payload: dict[str, Any] = {
|
|
36
|
+
"from": from_,
|
|
37
|
+
"to": to,
|
|
38
|
+
"subject": subject,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if html is not None:
|
|
42
|
+
payload["html"] = html
|
|
43
|
+
if text is not None:
|
|
44
|
+
payload["text"] = text
|
|
45
|
+
if cc is not None:
|
|
46
|
+
payload["cc"] = cc
|
|
47
|
+
if bcc is not None:
|
|
48
|
+
payload["bcc"] = bcc
|
|
49
|
+
if reply_to is not None:
|
|
50
|
+
payload["reply_to"] = reply_to
|
|
51
|
+
if headers is not None:
|
|
52
|
+
payload["headers"] = headers
|
|
53
|
+
if tags is not None:
|
|
54
|
+
payload["tags"] = tags
|
|
55
|
+
if scheduled_at is not None:
|
|
56
|
+
payload["scheduled_at"] = scheduled_at
|
|
57
|
+
if attachments is not None:
|
|
58
|
+
payload["attachments"] = attachments
|
|
59
|
+
|
|
60
|
+
return self._client._post("/v1/emails", payload)
|
|
61
|
+
|
|
62
|
+
def send_mime(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
envelope_from: str,
|
|
66
|
+
envelope_to: str,
|
|
67
|
+
raw_message: str,
|
|
68
|
+
) -> dict[str, str]:
|
|
69
|
+
"""Send a raw MIME email.
|
|
70
|
+
|
|
71
|
+
Returns a dict with the email ``id``.
|
|
72
|
+
"""
|
|
73
|
+
return self._client._post("/v1/emails/mime", {
|
|
74
|
+
"envelope_from": envelope_from,
|
|
75
|
+
"envelope_to": envelope_to,
|
|
76
|
+
"raw_message": raw_message,
|
|
77
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SendKitError(Exception):
|
|
5
|
+
"""Represents an error response from the SendKit API."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None, name: str = "application_error") -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
self.name = name
|
|
12
|
+
|
|
13
|
+
def __repr__(self) -> str:
|
|
14
|
+
return f"SendKitError(name={self.name!r}, status_code={self.status_code}, message={self.message!r})"
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from typing import Any
|
|
8
|
+
from unittest import TestCase
|
|
9
|
+
|
|
10
|
+
from sendkit import SendKit, SendKitError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_server(handler_class: type[BaseHTTPRequestHandler]) -> tuple[HTTPServer, str]:
|
|
14
|
+
server = HTTPServer(("127.0.0.1", 0), handler_class)
|
|
15
|
+
port = server.server_address[1]
|
|
16
|
+
thread = Thread(target=server.serve_forever, daemon=True)
|
|
17
|
+
thread.start()
|
|
18
|
+
return server, f"http://127.0.0.1:{port}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestNewClient(TestCase):
|
|
22
|
+
def test_with_api_key(self) -> None:
|
|
23
|
+
client = SendKit("sk_test_123")
|
|
24
|
+
self.assertEqual(client.api_key, "sk_test_123")
|
|
25
|
+
|
|
26
|
+
def test_missing_api_key(self) -> None:
|
|
27
|
+
os.environ.pop("SENDKIT_API_KEY", None)
|
|
28
|
+
with self.assertRaises(ValueError):
|
|
29
|
+
SendKit()
|
|
30
|
+
|
|
31
|
+
def test_from_env_variable(self) -> None:
|
|
32
|
+
os.environ["SENDKIT_API_KEY"] = "sk_from_env"
|
|
33
|
+
try:
|
|
34
|
+
client = SendKit()
|
|
35
|
+
self.assertEqual(client.api_key, "sk_from_env")
|
|
36
|
+
finally:
|
|
37
|
+
del os.environ["SENDKIT_API_KEY"]
|
|
38
|
+
|
|
39
|
+
def test_custom_base_url(self) -> None:
|
|
40
|
+
client = SendKit("sk_test_123", base_url="https://custom.api.com")
|
|
41
|
+
self.assertEqual(client.base_url, "https://custom.api.com")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestEmailsSend(TestCase):
|
|
45
|
+
def test_send_email(self) -> None:
|
|
46
|
+
captured: dict[str, Any] = {}
|
|
47
|
+
|
|
48
|
+
class Handler(BaseHTTPRequestHandler):
|
|
49
|
+
def do_POST(self) -> None:
|
|
50
|
+
captured["path"] = self.path
|
|
51
|
+
captured["method"] = self.command
|
|
52
|
+
captured["auth"] = self.headers.get("Authorization")
|
|
53
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
54
|
+
captured["body"] = json.loads(self.rfile.read(length))
|
|
55
|
+
self.send_response(200)
|
|
56
|
+
self.end_headers()
|
|
57
|
+
self.wfile.write(json.dumps({"id": "email-uuid-123"}).encode())
|
|
58
|
+
|
|
59
|
+
def log_message(self, *args: Any) -> None:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
server, url = _make_server(Handler)
|
|
63
|
+
try:
|
|
64
|
+
client = SendKit("sk_test_123", base_url=url)
|
|
65
|
+
result = client.emails.send(
|
|
66
|
+
from_="sender@example.com",
|
|
67
|
+
to="recipient@example.com",
|
|
68
|
+
subject="Test Email",
|
|
69
|
+
html="<p>Hello</p>",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.assertEqual(result["id"], "email-uuid-123")
|
|
73
|
+
self.assertEqual(captured["path"], "/v1/emails")
|
|
74
|
+
self.assertEqual(captured["method"], "POST")
|
|
75
|
+
self.assertEqual(captured["auth"], "Bearer sk_test_123")
|
|
76
|
+
self.assertEqual(captured["body"]["from"], "sender@example.com")
|
|
77
|
+
self.assertEqual(captured["body"]["to"], "recipient@example.com")
|
|
78
|
+
self.assertEqual(captured["body"]["subject"], "Test Email")
|
|
79
|
+
finally:
|
|
80
|
+
server.shutdown()
|
|
81
|
+
|
|
82
|
+
def test_send_with_optional_fields(self) -> None:
|
|
83
|
+
captured: dict[str, Any] = {}
|
|
84
|
+
|
|
85
|
+
class Handler(BaseHTTPRequestHandler):
|
|
86
|
+
def do_POST(self) -> None:
|
|
87
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
88
|
+
captured["body"] = json.loads(self.rfile.read(length))
|
|
89
|
+
self.send_response(200)
|
|
90
|
+
self.end_headers()
|
|
91
|
+
self.wfile.write(json.dumps({"id": "email-uuid-456"}).encode())
|
|
92
|
+
|
|
93
|
+
def log_message(self, *args: Any) -> None:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
server, url = _make_server(Handler)
|
|
97
|
+
try:
|
|
98
|
+
client = SendKit("sk_test_123", base_url=url)
|
|
99
|
+
client.emails.send(
|
|
100
|
+
from_="sender@example.com",
|
|
101
|
+
to="recipient@example.com",
|
|
102
|
+
subject="Test",
|
|
103
|
+
html="<p>Hi</p>",
|
|
104
|
+
reply_to="reply@example.com",
|
|
105
|
+
scheduled_at="2026-03-01T10:00:00Z",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.assertEqual(captured["body"]["reply_to"], "reply@example.com")
|
|
109
|
+
self.assertEqual(captured["body"]["scheduled_at"], "2026-03-01T10:00:00Z")
|
|
110
|
+
finally:
|
|
111
|
+
server.shutdown()
|
|
112
|
+
|
|
113
|
+
def test_send_mime_email(self) -> None:
|
|
114
|
+
captured: dict[str, Any] = {}
|
|
115
|
+
|
|
116
|
+
class Handler(BaseHTTPRequestHandler):
|
|
117
|
+
def do_POST(self) -> None:
|
|
118
|
+
captured["path"] = self.path
|
|
119
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
120
|
+
captured["body"] = json.loads(self.rfile.read(length))
|
|
121
|
+
self.send_response(200)
|
|
122
|
+
self.end_headers()
|
|
123
|
+
self.wfile.write(json.dumps({"id": "mime-uuid-789"}).encode())
|
|
124
|
+
|
|
125
|
+
def log_message(self, *args: Any) -> None:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
server, url = _make_server(Handler)
|
|
129
|
+
try:
|
|
130
|
+
client = SendKit("sk_test_123", base_url=url)
|
|
131
|
+
result = client.emails.send_mime(
|
|
132
|
+
envelope_from="sender@example.com",
|
|
133
|
+
envelope_to="recipient@example.com",
|
|
134
|
+
raw_message="From: sender@example.com\r\nTo: recipient@example.com\r\n\r\nHello",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
self.assertEqual(result["id"], "mime-uuid-789")
|
|
138
|
+
self.assertEqual(captured["path"], "/v1/emails/mime")
|
|
139
|
+
self.assertEqual(captured["body"]["envelope_from"], "sender@example.com")
|
|
140
|
+
self.assertEqual(captured["body"]["envelope_to"], "recipient@example.com")
|
|
141
|
+
finally:
|
|
142
|
+
server.shutdown()
|
|
143
|
+
|
|
144
|
+
def test_api_error(self) -> None:
|
|
145
|
+
class Handler(BaseHTTPRequestHandler):
|
|
146
|
+
def do_POST(self) -> None:
|
|
147
|
+
self.send_response(422)
|
|
148
|
+
self.end_headers()
|
|
149
|
+
self.wfile.write(json.dumps({
|
|
150
|
+
"name": "validation_error",
|
|
151
|
+
"message": "The to field is required.",
|
|
152
|
+
"statusCode": 422,
|
|
153
|
+
}).encode())
|
|
154
|
+
|
|
155
|
+
def log_message(self, *args: Any) -> None:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
server, url = _make_server(Handler)
|
|
159
|
+
try:
|
|
160
|
+
client = SendKit("sk_test_123", base_url=url)
|
|
161
|
+
with self.assertRaises(SendKitError) as ctx:
|
|
162
|
+
client.emails.send(
|
|
163
|
+
from_="sender@example.com",
|
|
164
|
+
to="",
|
|
165
|
+
subject="Test",
|
|
166
|
+
html="<p>Hi</p>",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
err = ctx.exception
|
|
170
|
+
self.assertEqual(err.name, "validation_error")
|
|
171
|
+
self.assertEqual(err.status_code, 422)
|
|
172
|
+
self.assertEqual(err.message, "The to field is required.")
|
|
173
|
+
finally:
|
|
174
|
+
server.shutdown()
|