utobo-email 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.
- utobo_email-1.0.0/.gitignore +39 -0
- utobo_email-1.0.0/PKG-INFO +68 -0
- utobo_email-1.0.0/README.md +43 -0
- utobo_email-1.0.0/pyproject.toml +32 -0
- utobo_email-1.0.0/utobo_email/__init__.py +5 -0
- utobo_email-1.0.0/utobo_email/client.py +106 -0
- utobo_email-1.0.0/utobo_email/emails.py +61 -0
- utobo_email-1.0.0/utobo_email/exceptions.py +16 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
**/.DS_Store
|
|
21
|
+
*.suo
|
|
22
|
+
*.ntvs*
|
|
23
|
+
*.njsproj
|
|
24
|
+
*.sln
|
|
25
|
+
*.sw?
|
|
26
|
+
|
|
27
|
+
# Environment variables
|
|
28
|
+
.env
|
|
29
|
+
.env.local
|
|
30
|
+
.env.*.local
|
|
31
|
+
|
|
32
|
+
# Vite temp files
|
|
33
|
+
vite.config.ts.timestamp-*.mjs
|
|
34
|
+
dist/robots.txt
|
|
35
|
+
node_modules/
|
|
36
|
+
.gstack/
|
|
37
|
+
|
|
38
|
+
# Local Claude settings (do not commit)
|
|
39
|
+
.claude/settings.local.json
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: utobo-email
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the utobo email Email Platform
|
|
5
|
+
Project-URL: Homepage, https://utobo.com/email
|
|
6
|
+
Project-URL: Documentation, https://utobo.com/email/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/myutobo/utobo-email-python
|
|
8
|
+
Project-URL: Issues, https://github.com/myutobo/utobo-email-python/issues
|
|
9
|
+
Author-email: Utobo <hello@mail.utobo.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: api,email,sdk,transactional,utobo
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications :: Email
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Requires-Dist: httpx>=0.24.0
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# utobo-email
|
|
27
|
+
|
|
28
|
+
Official Python SDK for the [utobo email](https://utobo.com/email) email platform.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install utobo-email
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from utobo_email import UtoboEmail
|
|
40
|
+
|
|
41
|
+
client = UtoboEmail("av_live_...")
|
|
42
|
+
|
|
43
|
+
result = client.emails.send(
|
|
44
|
+
from_="Acme <noreply@acme.com>",
|
|
45
|
+
to="user@example.com",
|
|
46
|
+
subject="Welcome!",
|
|
47
|
+
html="<p>Welcome to Acme!</p>",
|
|
48
|
+
)
|
|
49
|
+
print(result["id"])
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Check delivery status
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
status = client.emails.get("3c9b0f4e-...")
|
|
56
|
+
print(status["status"]) # "sent" | "failed"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
| Option | Env var | Default |
|
|
62
|
+
|--------|---------|---------|
|
|
63
|
+
| `api_key` | `UTOBO_EMAIL_API_KEY` | — (required) |
|
|
64
|
+
| `base_url` | `UTOBO_EMAIL_BASE_URL` | `https://api.email.utobo.com/v1` |
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# utobo-email
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [utobo email](https://utobo.com/email) email platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install utobo-email
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from utobo_email import UtoboEmail
|
|
15
|
+
|
|
16
|
+
client = UtoboEmail("av_live_...")
|
|
17
|
+
|
|
18
|
+
result = client.emails.send(
|
|
19
|
+
from_="Acme <noreply@acme.com>",
|
|
20
|
+
to="user@example.com",
|
|
21
|
+
subject="Welcome!",
|
|
22
|
+
html="<p>Welcome to Acme!</p>",
|
|
23
|
+
)
|
|
24
|
+
print(result["id"])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Check delivery status
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
status = client.emails.get("3c9b0f4e-...")
|
|
31
|
+
print(status["status"]) # "sent" | "failed"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
| Option | Env var | Default |
|
|
37
|
+
|--------|---------|---------|
|
|
38
|
+
| `api_key` | `UTOBO_EMAIL_API_KEY` | — (required) |
|
|
39
|
+
| `base_url` | `UTOBO_EMAIL_BASE_URL` | `https://api.email.utobo.com/v1` |
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "utobo-email"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for the utobo email Email Platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [{ name = "Utobo", email = "hello@mail.utobo.com" }]
|
|
13
|
+
keywords = ["utobo", "email", "api", "sdk", "transactional"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Communications :: Email",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["httpx>=0.24.0"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://utobo.com/email"
|
|
30
|
+
Documentation = "https://utobo.com/email/docs"
|
|
31
|
+
Repository = "https://github.com/myutobo/utobo-email-python"
|
|
32
|
+
Issues = "https://github.com/myutobo/utobo-email-python/issues"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .emails import Emails
|
|
9
|
+
from .exceptions import ApiError, NetworkError
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.email.utobo.com/v1"
|
|
12
|
+
SDK_VERSION = "1.0.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UtoboEmail:
|
|
16
|
+
"""utobo email Python SDK client.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
from utobo_email import UtoboEmail
|
|
20
|
+
|
|
21
|
+
client = UtoboEmail("av_live_...")
|
|
22
|
+
result = client.emails.send(
|
|
23
|
+
from_="Acme <noreply@acme.com>",
|
|
24
|
+
to="user@example.com",
|
|
25
|
+
subject="Welcome!",
|
|
26
|
+
html="<p>Welcome to Acme!</p>",
|
|
27
|
+
)
|
|
28
|
+
print(result["id"])
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
api_key: Optional[str] = None,
|
|
34
|
+
*,
|
|
35
|
+
base_url: Optional[str] = None,
|
|
36
|
+
):
|
|
37
|
+
resolved_key = api_key or os.environ.get("UTOBO_EMAIL_API_KEY") or os.environ.get("UTOBO_MAIL_API_KEY")
|
|
38
|
+
if not resolved_key:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"Missing API key. "
|
|
41
|
+
'Pass it to the constructor `UtoboEmail("av_live_...")` '
|
|
42
|
+
"or set the UTOBO_EMAIL_API_KEY environment variable."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self._api_key = resolved_key
|
|
46
|
+
self._base_url = (
|
|
47
|
+
base_url
|
|
48
|
+
or os.environ.get("UTOBO_EMAIL_BASE_URL")
|
|
49
|
+
or os.environ.get("UTOBO_MAIL_BASE_URL")
|
|
50
|
+
or DEFAULT_BASE_URL
|
|
51
|
+
)
|
|
52
|
+
self._http = httpx.Client(
|
|
53
|
+
base_url=self._base_url,
|
|
54
|
+
headers={
|
|
55
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"User-Agent": f"utobo-email-python/{SDK_VERSION}",
|
|
58
|
+
},
|
|
59
|
+
timeout=30.0,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.emails = Emails(self)
|
|
63
|
+
|
|
64
|
+
def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
65
|
+
try:
|
|
66
|
+
resp = self._http.post(path, json=body)
|
|
67
|
+
except httpx.HTTPError as e:
|
|
68
|
+
raise NetworkError(str(e)) from e
|
|
69
|
+
|
|
70
|
+
if not resp.is_success:
|
|
71
|
+
self._raise_api_error(resp)
|
|
72
|
+
return resp.json()
|
|
73
|
+
|
|
74
|
+
def _get(self, path: str) -> Dict[str, Any]:
|
|
75
|
+
try:
|
|
76
|
+
resp = self._http.get(path)
|
|
77
|
+
except httpx.HTTPError as e:
|
|
78
|
+
raise NetworkError(str(e)) from e
|
|
79
|
+
|
|
80
|
+
if not resp.is_success:
|
|
81
|
+
self._raise_api_error(resp)
|
|
82
|
+
return resp.json()
|
|
83
|
+
|
|
84
|
+
def _raise_api_error(self, resp: httpx.Response) -> None:
|
|
85
|
+
try:
|
|
86
|
+
data = resp.json()
|
|
87
|
+
raise ApiError(
|
|
88
|
+
status_code=resp.status_code,
|
|
89
|
+
name=data.get("name", "api_error"),
|
|
90
|
+
message=data.get("message", resp.reason_phrase or "Request failed"),
|
|
91
|
+
)
|
|
92
|
+
except (ValueError, KeyError):
|
|
93
|
+
raise ApiError(
|
|
94
|
+
status_code=resp.status_code,
|
|
95
|
+
name="api_error",
|
|
96
|
+
message=resp.reason_phrase or "Request failed",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
self._http.close()
|
|
101
|
+
|
|
102
|
+
def __enter__(self) -> "UtoboEmail":
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *_: Any) -> None:
|
|
106
|
+
self.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from .exceptions import ApiError, NetworkError
|
|
6
|
+
|
|
7
|
+
if False: # TYPE_CHECKING
|
|
8
|
+
from .client import UtoboEmail
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Emails:
|
|
12
|
+
def __init__(self, client: "UtoboEmail"):
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def send(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
from_: str,
|
|
19
|
+
to: Union[str, List[str]],
|
|
20
|
+
subject: str,
|
|
21
|
+
html: Optional[str] = None,
|
|
22
|
+
text: Optional[str] = None,
|
|
23
|
+
reply_to: Optional[Union[str, List[str]]] = None,
|
|
24
|
+
cc: Optional[Union[str, List[str]]] = None,
|
|
25
|
+
bcc: Optional[Union[str, List[str]]] = None,
|
|
26
|
+
attachments: Optional[List[Dict[str, Any]]] = None,
|
|
27
|
+
headers: Optional[Dict[str, str]] = None,
|
|
28
|
+
) -> Dict[str, Any]:
|
|
29
|
+
"""Send a transactional email.
|
|
30
|
+
|
|
31
|
+
Returns dict with 'id' and 'message' on success.
|
|
32
|
+
Raises ApiError or NetworkError on failure.
|
|
33
|
+
|
|
34
|
+
Each attachment dict requires 'filename' and 'content' (base64 string),
|
|
35
|
+
and an optional 'contentType'.
|
|
36
|
+
"""
|
|
37
|
+
body: Dict[str, Any] = {
|
|
38
|
+
"from": from_,
|
|
39
|
+
"to": [to] if isinstance(to, str) else to,
|
|
40
|
+
"subject": subject,
|
|
41
|
+
}
|
|
42
|
+
if html is not None:
|
|
43
|
+
body["html"] = html
|
|
44
|
+
if text is not None:
|
|
45
|
+
body["text"] = text
|
|
46
|
+
if reply_to is not None:
|
|
47
|
+
body["reply_to"] = [reply_to] if isinstance(reply_to, str) else reply_to
|
|
48
|
+
if cc is not None:
|
|
49
|
+
body["cc"] = [cc] if isinstance(cc, str) else cc
|
|
50
|
+
if bcc is not None:
|
|
51
|
+
body["bcc"] = [bcc] if isinstance(bcc, str) else bcc
|
|
52
|
+
if attachments is not None:
|
|
53
|
+
body["attachments"] = attachments
|
|
54
|
+
if headers is not None:
|
|
55
|
+
body["headers"] = headers
|
|
56
|
+
|
|
57
|
+
return self._client._post("/send-email", body)
|
|
58
|
+
|
|
59
|
+
def get(self, email_id: str) -> Dict[str, Any]:
|
|
60
|
+
"""Retrieve the status of a previously sent email."""
|
|
61
|
+
return self._client._get(f"/send-email/{email_id}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class UtoboEmailError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ApiError(UtoboEmailError):
|
|
6
|
+
def __init__(self, status_code: int, name: str, message: str):
|
|
7
|
+
self.status_code = status_code
|
|
8
|
+
self.name = name
|
|
9
|
+
self.message = message
|
|
10
|
+
super().__init__(f"{name} ({status_code}): {message}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NetworkError(UtoboEmailError):
|
|
14
|
+
def __init__(self, message: str = "Unable to reach the utobo email API"):
|
|
15
|
+
self.message = message
|
|
16
|
+
super().__init__(message)
|