mailtea 0.1.2__py3-none-any.whl
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.
- mailtea/__init__.py +7 -0
- mailtea/_resource.py +54 -0
- mailtea/_transport.py +36 -0
- mailtea/api_keys.py +36 -0
- mailtea/client.py +102 -0
- mailtea/contact_properties.py +37 -0
- mailtea/contacts.py +55 -0
- mailtea/domains.py +123 -0
- mailtea/emails.py +63 -0
- mailtea/errors.py +30 -0
- mailtea/posts.py +52 -0
- mailtea/segments.py +47 -0
- mailtea/tags.py +48 -0
- mailtea/webhooks.py +51 -0
- mailtea-0.1.2.dist-info/METADATA +100 -0
- mailtea-0.1.2.dist-info/RECORD +18 -0
- mailtea-0.1.2.dist-info/WHEEL +4 -0
- mailtea-0.1.2.dist-info/licenses/LICENSE +21 -0
mailtea/__init__.py
ADDED
mailtea/_resource.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shared helpers for resource classes: payload merging, query strings, and
|
|
2
|
+
attribute-access response wrapping."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def body(params: Optional[Dict[str, Any]], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
11
|
+
"""Merge a positional wire-format dict with keyword arguments.
|
|
12
|
+
|
|
13
|
+
Every method accepts either style (or both — kwargs win on conflict)::
|
|
14
|
+
|
|
15
|
+
mailtea.emails.send({"from": "a@b.c", "subject": "Hi"})
|
|
16
|
+
mailtea.emails.send(from_="a@b.c", subject="Hi")
|
|
17
|
+
|
|
18
|
+
A single trailing underscore is stripped from kwarg names so Python
|
|
19
|
+
reserved words can be used (``from_`` becomes ``"from"`` on the wire).
|
|
20
|
+
"""
|
|
21
|
+
merged = dict(params or {})
|
|
22
|
+
for key, value in kwargs.items():
|
|
23
|
+
merged[key[:-1] if key.endswith("_") else key] = value
|
|
24
|
+
return merged
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def query(params: Optional[Dict[str, Any]]) -> str:
|
|
28
|
+
"""Render a query string from a dict, dropping ``None`` values."""
|
|
29
|
+
if not params:
|
|
30
|
+
return ""
|
|
31
|
+
clean = {key: value for key, value in params.items() if value is not None}
|
|
32
|
+
return ("?" + urlencode(clean)) if clean else ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Resource(dict):
|
|
36
|
+
"""An API response: a plain dict that also allows attribute access, so
|
|
37
|
+
both ``email["id"]`` and ``email.id`` work. Nested dicts/lists are wrapped
|
|
38
|
+
on access. Keys that collide with dict methods (``get``, ``items``, …)
|
|
39
|
+
are only reachable via ``[...]`` indexing.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __getattr__(self, name: str) -> Any:
|
|
43
|
+
try:
|
|
44
|
+
return wrap(self[name])
|
|
45
|
+
except KeyError:
|
|
46
|
+
raise AttributeError(name) from None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def wrap(value: Any) -> Any:
|
|
50
|
+
if isinstance(value, dict):
|
|
51
|
+
return Resource(value)
|
|
52
|
+
if isinstance(value, list):
|
|
53
|
+
return [wrap(item) for item in value]
|
|
54
|
+
return value
|
mailtea/_transport.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.error
|
|
4
|
+
import urllib.request
|
|
5
|
+
from typing import Callable, Dict, Iterable, NamedTuple, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HttpResponse(NamedTuple):
|
|
9
|
+
status: int
|
|
10
|
+
headers: Dict[str, str]
|
|
11
|
+
text: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# (method, url, headers, body) -> HttpResponse. Injectable so tests need no network.
|
|
15
|
+
Transport = Callable[[str, str, Dict[str, str], Optional[bytes]], HttpResponse]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _lower_headers(items: Iterable[Tuple[str, str]]) -> Dict[str, str]:
|
|
19
|
+
return {key.lower(): value for key, value in items}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def urllib_transport(
|
|
23
|
+
method: str, url: str, headers: Dict[str, str], body: Optional[bytes]
|
|
24
|
+
) -> HttpResponse:
|
|
25
|
+
"""Default transport built on the standard library — no third-party deps."""
|
|
26
|
+
request = urllib.request.Request(url=url, data=body, headers=headers, method=method)
|
|
27
|
+
try:
|
|
28
|
+
with urllib.request.urlopen(request) as response: # noqa: S310 (trusted base URL)
|
|
29
|
+
return HttpResponse(
|
|
30
|
+
response.status,
|
|
31
|
+
_lower_headers(response.headers.items()),
|
|
32
|
+
response.read().decode("utf-8"),
|
|
33
|
+
)
|
|
34
|
+
except urllib.error.HTTPError as error:
|
|
35
|
+
text = error.read().decode("utf-8") if error.fp is not None else ""
|
|
36
|
+
return HttpResponse(error.code, _lower_headers(error.headers.items()), text)
|
mailtea/api_keys.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiKeys:
|
|
12
|
+
"""The ``api_keys`` resource. Access via ``mailtea.api_keys``.
|
|
13
|
+
|
|
14
|
+
Requires a token with ``settings:write``. A key can never be granted scopes
|
|
15
|
+
the calling token does not already hold.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, request: RequestFn) -> None:
|
|
19
|
+
self._request = request
|
|
20
|
+
|
|
21
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
22
|
+
"""Create an API key. The ``token`` is returned ONCE — store it securely.
|
|
23
|
+
|
|
24
|
+
Takes ``name``, optional ``permission`` (``"full_access"`` or
|
|
25
|
+
``"sending_access"``), and optional ``domain_id``. Accepts a
|
|
26
|
+
wire-format dict, keyword arguments, or both.
|
|
27
|
+
"""
|
|
28
|
+
return self._request("POST", "/v1/api-keys", _body(params, kwargs))
|
|
29
|
+
|
|
30
|
+
def list(self) -> Dict[str, Any]:
|
|
31
|
+
"""List API keys (token values are never returned)."""
|
|
32
|
+
return self._request("GET", "/v1/api-keys")
|
|
33
|
+
|
|
34
|
+
def revoke(self, id: str) -> Any:
|
|
35
|
+
"""Revoke (delete) an API key by id."""
|
|
36
|
+
return self._request("DELETE", "/v1/api-keys/" + quote(str(id), safe=""))
|
mailtea/client.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from ._resource import wrap as _wrap
|
|
8
|
+
from ._transport import Transport, urllib_transport
|
|
9
|
+
from .api_keys import ApiKeys
|
|
10
|
+
from .contact_properties import ContactProperties
|
|
11
|
+
from .contacts import Contacts
|
|
12
|
+
from .domains import Domains
|
|
13
|
+
from .emails import Emails
|
|
14
|
+
from .errors import MailteaError
|
|
15
|
+
from .posts import Posts
|
|
16
|
+
from .segments import Segments
|
|
17
|
+
from .tags import Tags
|
|
18
|
+
from .webhooks import Webhooks
|
|
19
|
+
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.mailtea.app"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Mailtea:
|
|
24
|
+
"""The Mailtea client.
|
|
25
|
+
|
|
26
|
+
>>> from mailtea import Mailtea
|
|
27
|
+
>>> mailtea = Mailtea(os.environ["MAILTEA_API_KEY"])
|
|
28
|
+
>>> sent = mailtea.emails.send(
|
|
29
|
+
... from_="you@yourdomain.com",
|
|
30
|
+
... to="recipient@example.com",
|
|
31
|
+
... subject="Hello",
|
|
32
|
+
... html="<p>Sent with Mailtea.</p>",
|
|
33
|
+
... )
|
|
34
|
+
>>> sent.id # responses allow attribute and ["..."] access alike
|
|
35
|
+
|
|
36
|
+
Payloads may equally be passed as a single wire-format dict
|
|
37
|
+
(``send({"from": ..., "to": ...})``) — both styles hit the same endpoint.
|
|
38
|
+
|
|
39
|
+
The API key may be passed explicitly or read from ``MAILTEA_API_KEY``.
|
|
40
|
+
Self-hosting/local dev: pass ``base_url`` or set ``MAILTEA_API_BASE_URL``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
api_key: Optional[str] = None,
|
|
46
|
+
*,
|
|
47
|
+
base_url: Optional[str] = None,
|
|
48
|
+
transport: Optional[Transport] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
key = api_key or os.environ.get("MAILTEA_API_KEY")
|
|
51
|
+
if not key:
|
|
52
|
+
raise MailteaError(
|
|
53
|
+
"Missing Mailtea API key. Pass it to Mailtea(api_key) or set the "
|
|
54
|
+
"MAILTEA_API_KEY environment variable.",
|
|
55
|
+
code="missing_api_key",
|
|
56
|
+
)
|
|
57
|
+
self._api_key = key
|
|
58
|
+
self._base_url = (
|
|
59
|
+
base_url or os.environ.get("MAILTEA_API_BASE_URL") or DEFAULT_BASE_URL
|
|
60
|
+
).rstrip("/")
|
|
61
|
+
self._transport: Transport = transport or urllib_transport
|
|
62
|
+
|
|
63
|
+
self.emails = Emails(self._request)
|
|
64
|
+
self.contacts = Contacts(self._request)
|
|
65
|
+
self.posts = Posts(self._request)
|
|
66
|
+
self.segments = Segments(self._request)
|
|
67
|
+
self.tags = Tags(self._request)
|
|
68
|
+
self.domains = Domains(self._request)
|
|
69
|
+
self.webhooks = Webhooks(self._request)
|
|
70
|
+
self.contact_properties = ContactProperties(self._request)
|
|
71
|
+
self.api_keys = ApiKeys(self._request)
|
|
72
|
+
|
|
73
|
+
def _request(self, method: str, path: str, body: Any = None) -> Any:
|
|
74
|
+
url = self._base_url + path
|
|
75
|
+
headers = {"Authorization": "Bearer " + self._api_key}
|
|
76
|
+
data: Optional[bytes] = None
|
|
77
|
+
if body is not None:
|
|
78
|
+
headers["Content-Type"] = "application/json"
|
|
79
|
+
data = json.dumps(body).encode("utf-8")
|
|
80
|
+
|
|
81
|
+
response = self._transport(method, url, headers, data)
|
|
82
|
+
request_id = response.headers.get("x-request-id")
|
|
83
|
+
|
|
84
|
+
if response.status >= 400:
|
|
85
|
+
message = "HTTP {0}".format(response.status)
|
|
86
|
+
details = None
|
|
87
|
+
try:
|
|
88
|
+
parsed = json.loads(response.text)
|
|
89
|
+
if isinstance(parsed, dict):
|
|
90
|
+
message = parsed.get("error") or message
|
|
91
|
+
details = parsed.get("details")
|
|
92
|
+
except ValueError:
|
|
93
|
+
pass # non-JSON body — keep the status-line message
|
|
94
|
+
raise MailteaError(
|
|
95
|
+
message, status=response.status, details=details, request_id=request_id
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if response.status == 204 or not response.text:
|
|
99
|
+
return None
|
|
100
|
+
# Responses are dicts that also allow attribute access
|
|
101
|
+
# (email["id"] and email.id both work).
|
|
102
|
+
return _wrap(json.loads(response.text))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContactProperties:
|
|
12
|
+
"""The ``contact_properties`` resource (custom contact fields). Access via
|
|
13
|
+
``mailtea.contact_properties``.
|
|
14
|
+
|
|
15
|
+
Definitions are team-scoped — there is no ``publication_id``. ``create``
|
|
16
|
+
takes ``key`` and ``type`` (``"string"`` or ``"number"``). Every method
|
|
17
|
+
accepts the payload as a wire-format dict, as keyword arguments, or both.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, request: RequestFn) -> None:
|
|
21
|
+
self._request = request
|
|
22
|
+
|
|
23
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
24
|
+
return self._request("POST", "/v1/contact-properties", _body(params, kwargs))
|
|
25
|
+
|
|
26
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
27
|
+
return self._request("GET", "/v1/contact-properties" + _query(_body(params, kwargs)))
|
|
28
|
+
|
|
29
|
+
def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
30
|
+
return self._request(
|
|
31
|
+
"PATCH", "/v1/contact-properties/" + quote(str(id), safe=""), _body(params, kwargs)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def delete(self, id: str) -> Dict[str, Any]:
|
|
35
|
+
return self._request(
|
|
36
|
+
"DELETE", "/v1/contact-properties/" + quote(str(id), safe="")
|
|
37
|
+
)
|
mailtea/contacts.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Contacts:
|
|
12
|
+
"""The ``contacts`` resource. Access via ``mailtea.contacts``.
|
|
13
|
+
|
|
14
|
+
Audience resources are scoped to a publication — pass ``publication_id``.
|
|
15
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
16
|
+
arguments, or both.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, request: RequestFn) -> None:
|
|
20
|
+
self._request = request
|
|
21
|
+
|
|
22
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
23
|
+
"""Create a contact — or update it if the email already exists in the
|
|
24
|
+
publication (the endpoint upserts). :meth:`upsert` is the same call."""
|
|
25
|
+
return self._request("POST", "/v1/contacts", _body(params, kwargs))
|
|
26
|
+
|
|
27
|
+
def upsert(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
28
|
+
"""Create the contact or update it in place — alias of :meth:`create`,
|
|
29
|
+
named for what ``POST /v1/contacts`` actually does."""
|
|
30
|
+
return self.create(params, **kwargs)
|
|
31
|
+
|
|
32
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
33
|
+
return self._request("GET", "/v1/contacts" + _query(_body(params, kwargs)))
|
|
34
|
+
|
|
35
|
+
def get(self, id_or_email: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
36
|
+
return self._request(
|
|
37
|
+
"GET",
|
|
38
|
+
"/v1/contacts/" + quote(str(id_or_email), safe="") + _query(_body(params, kwargs)),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def update(self, id_or_email: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
42
|
+
merged = _body(params, kwargs)
|
|
43
|
+
return self._request(
|
|
44
|
+
"PATCH",
|
|
45
|
+
"/v1/contacts/"
|
|
46
|
+
+ quote(str(id_or_email), safe="")
|
|
47
|
+
+ _query({"publication_id": merged.get("publication_id")}),
|
|
48
|
+
merged,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def delete(self, id_or_email: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
52
|
+
return self._request(
|
|
53
|
+
"DELETE",
|
|
54
|
+
"/v1/contacts/" + quote(str(id_or_email), safe="") + _query(_body(params, kwargs)),
|
|
55
|
+
)
|
mailtea/domains.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Domains:
|
|
12
|
+
"""The ``domains`` resource (email/site sending domains). Access via
|
|
13
|
+
``mailtea.domains``.
|
|
14
|
+
|
|
15
|
+
Scoped to a publication — pass ``publication_id``. Register a domain, add the
|
|
16
|
+
returned DNS ``records``, then :meth:`verify` it before sending from it.
|
|
17
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
18
|
+
arguments, or both.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, request: RequestFn) -> None:
|
|
22
|
+
self._request = request
|
|
23
|
+
#: Tracking sub-domains (CNAME) under a domain.
|
|
24
|
+
self.tracking = TrackingDomains(request)
|
|
25
|
+
|
|
26
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
27
|
+
"""Register a domain. The response ``records`` lists the DNS records to add."""
|
|
28
|
+
return self._request("POST", "/v1/domains", _body(params, kwargs))
|
|
29
|
+
|
|
30
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
31
|
+
return self._request("GET", "/v1/domains" + _query(_body(params, kwargs)))
|
|
32
|
+
|
|
33
|
+
def get(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
34
|
+
return self._request(
|
|
35
|
+
"GET", "/v1/domains/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def verify(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
39
|
+
"""Verify a domain via its DNS records; ``status`` becomes ``"verified"``."""
|
|
40
|
+
return self._request(
|
|
41
|
+
"POST",
|
|
42
|
+
"/v1/domains/" + quote(str(id), safe="") + "/verify" + _query(_body(params, kwargs)),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
46
|
+
merged = _body(params, kwargs)
|
|
47
|
+
return self._request(
|
|
48
|
+
"PATCH",
|
|
49
|
+
"/v1/domains/"
|
|
50
|
+
+ quote(str(id), safe="")
|
|
51
|
+
+ _query({"publication_id": merged.get("publication_id")}),
|
|
52
|
+
merged,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def delete(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
56
|
+
return self._request(
|
|
57
|
+
"DELETE", "/v1/domains/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TrackingDomains:
|
|
62
|
+
"""Tracking sub-domains (CNAME) under a domain — used to serve open-pixel
|
|
63
|
+
and click-tracking links from your own domain. Access via
|
|
64
|
+
``mailtea.domains.tracking``.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, request: RequestFn) -> None:
|
|
68
|
+
self._request = request
|
|
69
|
+
|
|
70
|
+
def create(self, domain_id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
71
|
+
"""Add a tracking sub-domain. Takes ``publication_id`` and ``subdomain``.
|
|
72
|
+
The response ``records`` lists the CNAME to add."""
|
|
73
|
+
merged = _body(params, kwargs)
|
|
74
|
+
return self._request(
|
|
75
|
+
"POST",
|
|
76
|
+
"/v1/domains/"
|
|
77
|
+
+ quote(str(domain_id), safe="")
|
|
78
|
+
+ "/tracking-domains"
|
|
79
|
+
+ _query({"publication_id": merged.get("publication_id")}),
|
|
80
|
+
{"subdomain": merged.get("subdomain")},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def list(self, domain_id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
84
|
+
return self._request(
|
|
85
|
+
"GET",
|
|
86
|
+
"/v1/domains/"
|
|
87
|
+
+ quote(str(domain_id), safe="")
|
|
88
|
+
+ "/tracking-domains"
|
|
89
|
+
+ _query(_body(params, kwargs)),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def verify(
|
|
93
|
+
self,
|
|
94
|
+
domain_id: str,
|
|
95
|
+
tracking_domain_id: str,
|
|
96
|
+
params: Optional[Dict[str, Any]] = None,
|
|
97
|
+
**kwargs: Any,
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
return self._request(
|
|
100
|
+
"POST",
|
|
101
|
+
"/v1/domains/"
|
|
102
|
+
+ quote(str(domain_id), safe="")
|
|
103
|
+
+ "/tracking-domains/"
|
|
104
|
+
+ quote(str(tracking_domain_id), safe="")
|
|
105
|
+
+ "/verify"
|
|
106
|
+
+ _query(_body(params, kwargs)),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def delete(
|
|
110
|
+
self,
|
|
111
|
+
domain_id: str,
|
|
112
|
+
tracking_domain_id: str,
|
|
113
|
+
params: Optional[Dict[str, Any]] = None,
|
|
114
|
+
**kwargs: Any,
|
|
115
|
+
) -> Dict[str, Any]:
|
|
116
|
+
return self._request(
|
|
117
|
+
"DELETE",
|
|
118
|
+
"/v1/domains/"
|
|
119
|
+
+ quote(str(domain_id), safe="")
|
|
120
|
+
+ "/tracking-domains/"
|
|
121
|
+
+ quote(str(tracking_domain_id), safe="")
|
|
122
|
+
+ _query(_body(params, kwargs)),
|
|
123
|
+
)
|
mailtea/emails.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
Recipients = Union[str, List[str]]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Emails:
|
|
13
|
+
"""The ``emails`` resource. Access via ``mailtea.emails``.
|
|
14
|
+
|
|
15
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
16
|
+
arguments, or both (snake_case keys like ``reply_to``; use ``from_=`` as
|
|
17
|
+
the keyword form of ``"from"``).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, request: RequestFn) -> None:
|
|
21
|
+
self._request = request
|
|
22
|
+
|
|
23
|
+
def send(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
24
|
+
"""Send a transactional email. Provide ``html``/``text`` OR a ``template``.
|
|
25
|
+
|
|
26
|
+
>>> mailtea.emails.send(from_="a@b.co", to="c@d.co", subject="Hi", html="<p>Hi</p>")
|
|
27
|
+
|
|
28
|
+
Returns ``{"id": ...}``.
|
|
29
|
+
"""
|
|
30
|
+
return self._request("POST", "/v1/emails", _body(params, kwargs))
|
|
31
|
+
|
|
32
|
+
def batch(self, emails: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
33
|
+
"""Send up to 100 emails in one request. Returns ``{"data": [{"id": ...}]}``."""
|
|
34
|
+
return self._request("POST", "/v1/emails/batch", emails)
|
|
35
|
+
|
|
36
|
+
def get(self, id: str) -> Dict[str, Any]:
|
|
37
|
+
"""Retrieve an email with its delivery status and tracking counters.
|
|
38
|
+
|
|
39
|
+
Adds a friendly ``status`` alias of the raw ``last_event`` wire field.
|
|
40
|
+
"""
|
|
41
|
+
email = self._request("GET", "/v1/emails/" + quote(str(id), safe=""))
|
|
42
|
+
if isinstance(email, dict) and email.get("status") is None:
|
|
43
|
+
email["status"] = email.get("last_event")
|
|
44
|
+
return email
|
|
45
|
+
|
|
46
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
47
|
+
"""List emails (most recent first). Optional filters: ``status``, ``tag_name``,
|
|
48
|
+
``tag_value``, ``from_date``, ``to_date``, ``limit``, ``offset``."""
|
|
49
|
+
return self._request("GET", "/v1/emails" + _query(_body(params, kwargs)))
|
|
50
|
+
|
|
51
|
+
def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
52
|
+
"""Update a scheduled email (currently only ``scheduled_at``)."""
|
|
53
|
+
return self._request(
|
|
54
|
+
"PATCH", "/v1/emails/" + quote(str(id), safe=""), _body(params, kwargs)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def reschedule(self, id: str, scheduled_at: str) -> Dict[str, Any]:
|
|
58
|
+
"""Convenience wrapper over :meth:`update` for the reschedule case."""
|
|
59
|
+
return self.update(id, {"scheduled_at": scheduled_at})
|
|
60
|
+
|
|
61
|
+
def cancel(self, id: str) -> Dict[str, Any]:
|
|
62
|
+
"""Cancel a scheduled email before it sends."""
|
|
63
|
+
return self._request("POST", "/v1/emails/" + quote(str(id), safe="") + "/cancel")
|
mailtea/errors.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MailteaError(Exception):
|
|
7
|
+
"""Raised when the Mailtea API returns an error or the client is misconfigured.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
status: HTTP status code (0 for client-side errors).
|
|
11
|
+
code: machine-readable code for client-side errors (e.g. ``missing_api_key``).
|
|
12
|
+
details: validation issue list returned by the API, if any.
|
|
13
|
+
request_id: the API's ``x-request-id`` for support/debugging.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
*,
|
|
20
|
+
status: int = 0,
|
|
21
|
+
code: Optional[str] = None,
|
|
22
|
+
details: Any = None,
|
|
23
|
+
request_id: Optional[str] = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.message = message
|
|
27
|
+
self.status = status
|
|
28
|
+
self.code = code
|
|
29
|
+
self.details = details
|
|
30
|
+
self.request_id = request_id
|
mailtea/posts.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Posts:
|
|
12
|
+
"""The ``posts`` resource (newsletter posts/issues). Access via ``mailtea.posts``.
|
|
13
|
+
|
|
14
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
15
|
+
arguments, or both (snake_case keys like ``reply_to``; use ``from_=`` as
|
|
16
|
+
the keyword form of ``"from"``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, request: RequestFn) -> None:
|
|
20
|
+
self._request = request
|
|
21
|
+
|
|
22
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
23
|
+
"""Create a newsletter post (draft by default). Seed it from a published
|
|
24
|
+
server template with ``template_id`` + ``variables``, or pass inline
|
|
25
|
+
``html``. Set ``send=True`` to deliver right after creating (or with
|
|
26
|
+
``scheduled_at`` to schedule) — that requires the ``issues:send`` scope.
|
|
27
|
+
|
|
28
|
+
Returns ``{"id": ...}``.
|
|
29
|
+
"""
|
|
30
|
+
return self._request("POST", "/v1/posts", _body(params, kwargs))
|
|
31
|
+
|
|
32
|
+
def send(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
33
|
+
"""Send a draft post to the publication's audience — immediately, or at
|
|
34
|
+
``scheduled_at`` (ISO 8601) if given. Requires the ``issues:send`` scope.
|
|
35
|
+
"""
|
|
36
|
+
merged = _body(params, kwargs)
|
|
37
|
+
return self._request(
|
|
38
|
+
"POST", "/v1/posts/" + quote(str(id), safe="") + "/send", merged or None
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def send_test(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
42
|
+
"""Send a TEST copy of a post to specific recipients to check it before
|
|
43
|
+
subscribers see it. Renders the post exactly as a subscriber would receive
|
|
44
|
+
it and delivers a one-shot ``[TEST]`` email — it does NOT send to the
|
|
45
|
+
audience.
|
|
46
|
+
|
|
47
|
+
Takes ``recipients`` (up to 10), ``from`` (must use a verified domain),
|
|
48
|
+
and optional ``reply_to``. Returns ``{"sent_to": [...], "failed_to": [...]}``.
|
|
49
|
+
"""
|
|
50
|
+
return self._request(
|
|
51
|
+
"POST", "/v1/posts/" + quote(str(id), safe="") + "/test", _body(params, kwargs)
|
|
52
|
+
)
|
mailtea/segments.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Segments:
|
|
12
|
+
"""The ``segments`` resource. Access via ``mailtea.segments``.
|
|
13
|
+
|
|
14
|
+
Audience segments are scoped to a publication — pass ``publication_id``.
|
|
15
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
16
|
+
arguments, or both. To clear a nullable filter on update, pass ``None``
|
|
17
|
+
(e.g. ``status_filter=None``); omit the key to leave it unchanged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, request: RequestFn) -> None:
|
|
21
|
+
self._request = request
|
|
22
|
+
|
|
23
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
24
|
+
return self._request("POST", "/v1/segments", _body(params, kwargs))
|
|
25
|
+
|
|
26
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
27
|
+
return self._request("GET", "/v1/segments" + _query(_body(params, kwargs)))
|
|
28
|
+
|
|
29
|
+
def get(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
30
|
+
return self._request(
|
|
31
|
+
"GET", "/v1/segments/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
35
|
+
merged = _body(params, kwargs)
|
|
36
|
+
return self._request(
|
|
37
|
+
"PATCH",
|
|
38
|
+
"/v1/segments/"
|
|
39
|
+
+ quote(str(id), safe="")
|
|
40
|
+
+ _query({"publication_id": merged.get("publication_id")}),
|
|
41
|
+
merged,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def delete(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
45
|
+
return self._request(
|
|
46
|
+
"DELETE", "/v1/segments/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
47
|
+
)
|
mailtea/tags.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Tags:
|
|
12
|
+
"""The ``tags`` resource (tag definitions). Access via ``mailtea.tags``.
|
|
13
|
+
|
|
14
|
+
Tags are scoped to a publication — pass ``publication_id``. ``create``
|
|
15
|
+
requires ``default_subscription`` (``"opt_in"`` or ``"opt_out"``). This
|
|
16
|
+
manages tag definitions only; assigning tags to contacts is not yet exposed.
|
|
17
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
18
|
+
arguments, or both.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, request: RequestFn) -> None:
|
|
22
|
+
self._request = request
|
|
23
|
+
|
|
24
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
25
|
+
return self._request("POST", "/v1/tags", _body(params, kwargs))
|
|
26
|
+
|
|
27
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
28
|
+
return self._request("GET", "/v1/tags" + _query(_body(params, kwargs)))
|
|
29
|
+
|
|
30
|
+
def get(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
31
|
+
return self._request(
|
|
32
|
+
"GET", "/v1/tags/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
36
|
+
merged = _body(params, kwargs)
|
|
37
|
+
return self._request(
|
|
38
|
+
"PATCH",
|
|
39
|
+
"/v1/tags/"
|
|
40
|
+
+ quote(str(id), safe="")
|
|
41
|
+
+ _query({"publication_id": merged.get("publication_id")}),
|
|
42
|
+
merged,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def delete(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
46
|
+
return self._request(
|
|
47
|
+
"DELETE", "/v1/tags/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
48
|
+
)
|
mailtea/webhooks.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from ._resource import body as _body, query as _query
|
|
7
|
+
|
|
8
|
+
RequestFn = Callable[..., Any]
|
|
9
|
+
|
|
10
|
+
_BASE = "/v1/webhooks/endpoints"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Webhooks:
|
|
14
|
+
"""The ``webhooks`` resource (outbound event subscriptions). Access via
|
|
15
|
+
``mailtea.webhooks``.
|
|
16
|
+
|
|
17
|
+
Scoped to a publication — pass ``publication_id``. ``create`` returns the
|
|
18
|
+
``signing_secret`` once; store it to verify payload signatures.
|
|
19
|
+
Every method accepts the payload as a wire-format dict, as keyword
|
|
20
|
+
arguments, or both.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, request: RequestFn) -> None:
|
|
24
|
+
self._request = request
|
|
25
|
+
|
|
26
|
+
def create(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
27
|
+
return self._request("POST", _BASE, _body(params, kwargs))
|
|
28
|
+
|
|
29
|
+
def list(self, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
30
|
+
return self._request("GET", _BASE + _query(_body(params, kwargs)))
|
|
31
|
+
|
|
32
|
+
def get(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
33
|
+
return self._request(
|
|
34
|
+
"GET", _BASE + "/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def update(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
38
|
+
merged = _body(params, kwargs)
|
|
39
|
+
return self._request(
|
|
40
|
+
"PATCH",
|
|
41
|
+
_BASE
|
|
42
|
+
+ "/"
|
|
43
|
+
+ quote(str(id), safe="")
|
|
44
|
+
+ _query({"publication_id": merged.get("publication_id")}),
|
|
45
|
+
merged,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def delete(self, id: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
|
49
|
+
return self._request(
|
|
50
|
+
"DELETE", _BASE + "/" + quote(str(id), safe="") + _query(_body(params, kwargs))
|
|
51
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mailtea
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Mailtea Python SDK — send, schedule, and manage email from your app or AI agent.
|
|
5
|
+
Project-URL: Homepage, https://mailtea.app
|
|
6
|
+
Project-URL: Documentation, https://docs.mailtea.app
|
|
7
|
+
Project-URL: Repository, https://github.com/mailtea-app/mailtea-python
|
|
8
|
+
Author: Mailtea
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agent,email,mailtea,sdk,transactional-email
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Communications :: Email
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# mailtea (Python)
|
|
19
|
+
|
|
20
|
+
The official Python SDK for [Mailtea](https://mailtea.app) — a thin, zero-dependency
|
|
21
|
+
wrapper over the [REST API](https://api.mailtea.app). Python 3.9+.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install mailtea
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import os
|
|
33
|
+
from mailtea import Mailtea
|
|
34
|
+
|
|
35
|
+
mailtea = Mailtea(os.environ["MAILTEA_API_KEY"])
|
|
36
|
+
|
|
37
|
+
sent = mailtea.emails.send(
|
|
38
|
+
from_="you@yourdomain.com",
|
|
39
|
+
to="recipient@example.com",
|
|
40
|
+
subject="Hello from Mailtea",
|
|
41
|
+
html="<p>Your first email, sent with <strong>Mailtea</strong>.</p>",
|
|
42
|
+
)
|
|
43
|
+
print(sent.id)
|
|
44
|
+
|
|
45
|
+
email = mailtea.emails.get(sent.id)
|
|
46
|
+
print(email.status)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The API key can be passed to `Mailtea(api_key)` or read from `MAILTEA_API_KEY`.
|
|
50
|
+
Self-hosting or local dev? pass `base_url=` or set `MAILTEA_API_BASE_URL`.
|
|
51
|
+
|
|
52
|
+
Every method equally accepts a single wire-format dict — handy when you already
|
|
53
|
+
hold the JSON payload:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
sent = mailtea.emails.send({
|
|
57
|
+
"from": "you@yourdomain.com",
|
|
58
|
+
"to": "recipient@example.com",
|
|
59
|
+
"subject": "Hello from Mailtea",
|
|
60
|
+
"html": "<p>Your first email, sent with <strong>Mailtea</strong>.</p>",
|
|
61
|
+
})
|
|
62
|
+
print(sent["id"]) # responses support ["..."] and attribute access alike
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Keyword arguments use snake_case wire names; a trailing underscore escapes
|
|
66
|
+
Python reserved words (`from_=` → `"from"`).
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
| Method | Description |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `emails.send(params)` | Send a transactional email → `{"id": ...}` |
|
|
73
|
+
| `emails.batch(emails)` | Send up to 100 emails → `{"data": [{"id": ...}]}` |
|
|
74
|
+
| `emails.get(id)` | Retrieve an email and its delivery status |
|
|
75
|
+
| `emails.list(params=None)` | List emails → `{"data", "total", "limit", "offset", "has_more"}` |
|
|
76
|
+
| `emails.update(id, params)` | Reschedule a scheduled email |
|
|
77
|
+
| `emails.reschedule(id, scheduled_at)` | Convenience wrapper over `update` |
|
|
78
|
+
| `emails.cancel(id)` | Cancel a scheduled email |
|
|
79
|
+
| `contacts.create / upsert / list / get / update / delete` | Manage audience contacts (`upsert` = `create`; the endpoint upserts) |
|
|
80
|
+
| `posts.create(...)` | Create a newsletter post (draft, or `send=True`) → `{"id": ...}` |
|
|
81
|
+
| `posts.send(id, scheduled_at=None)` | Send a draft post to the audience, now or scheduled |
|
|
82
|
+
| `posts.send_test(id, params)` | Send a `[TEST]` copy of a post → `{"sent_to", "failed_to"}` |
|
|
83
|
+
| `segments.create / list / get / update / delete` | Manage audience segments |
|
|
84
|
+
| `tags.create / list / get / update / delete` | Manage tag definitions |
|
|
85
|
+
| `domains.create / list / get / verify / update / delete` | Manage sending domains (add, read DNS records, verify) |
|
|
86
|
+
| `domains.tracking.create / list / verify / delete` | Manage CNAME tracking sub-domains under a domain |
|
|
87
|
+
| `webhooks.create / list / get / update / delete` | Manage outbound event subscriptions |
|
|
88
|
+
| `contact_properties.create / list / update / delete` | Manage custom contact fields (team-scoped) |
|
|
89
|
+
| `api_keys.create / list / revoke` | Manage API keys (`settings:write`) |
|
|
90
|
+
|
|
91
|
+
Payloads follow the REST wire format (`reply_to`, `scheduled_at`, …), passed as
|
|
92
|
+
keyword arguments or a plain dict. Errors raise `MailteaError` with `status`,
|
|
93
|
+
`details`, and `request_id`.
|
|
94
|
+
|
|
95
|
+
## Develop
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd sdks/python
|
|
99
|
+
python3 -m unittest discover -s tests -t .
|
|
100
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mailtea/__init__.py,sha256=cS4H6DPaM6sBvoEy3zYedMgxMXBhc7gGfQq-Z5w8bc8,212
|
|
2
|
+
mailtea/_resource.py,sha256=_QVaREujr5w1HtTvFaMlQAyqJUreZ6sHRiLE26nC4d4,1850
|
|
3
|
+
mailtea/_transport.py,sha256=FHXx6bsdNcgbPs1LRIaBfWRfTmzoyWBFfQQqSdcFOiM,1321
|
|
4
|
+
mailtea/api_keys.py,sha256=VTXPKFB8r4iAgcOUuxzIJAlUMJ11jrBRqp66pQIwpCk,1275
|
|
5
|
+
mailtea/client.py,sha256=L_PuxEwSbqAQhCzJ6nXKoyXoU2eCqYKBgthra6XXq98,3694
|
|
6
|
+
mailtea/contact_properties.py,sha256=WX1xJ7ylZMkgc1d1DKQ-Vhkzk5dANNG_awWDWcMDwsc,1450
|
|
7
|
+
mailtea/contacts.py,sha256=JFLk8DQ85T7PSaBfn_AORrR6AWaH4y_NYdQQ2XpfBek,2277
|
|
8
|
+
mailtea/domains.py,sha256=wM4X7Ve3kVw31wR7GvJsXjKydHHOGWiD7xvA7dpBGlI,4527
|
|
9
|
+
mailtea/emails.py,sha256=6trMltSTmPiI_o7VvRV-Kwbxv-n5fBleskI07vD7AC4,2720
|
|
10
|
+
mailtea/errors.py,sha256=DRvwEFoSuuRdP_Cb28GZYBpBKTYFWwUqbVLUh6Tb0fA,897
|
|
11
|
+
mailtea/posts.py,sha256=MMI-jk_05lRgpUbJZpMRrHkghYbXuHBQ5BNCZAmhASg,2227
|
|
12
|
+
mailtea/segments.py,sha256=EyMp6hlOaCfT_WmugLy91YiZ2SmNbUa53RBIbClMdWA,1873
|
|
13
|
+
mailtea/tags.py,sha256=zhG7w78scWVPgFSyKnIXzMeOXjxpBWBhG7hsnJ9qwIU,1891
|
|
14
|
+
mailtea/webhooks.py,sha256=rO6IM0ywZuQ0spKOw0ccy_lMsMtrDH4GhbJNJLz6wl0,1870
|
|
15
|
+
mailtea-0.1.2.dist-info/METADATA,sha256=carXUrSEQ6a-jmkFG7jxkuSX5LdvLNSkhZlcZdadjq4,3827
|
|
16
|
+
mailtea-0.1.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
17
|
+
mailtea-0.1.2.dist-info/licenses/LICENSE,sha256=mPPPljFBnTeFwc4OnW9TdnHIIjrqgDTtBsSVxC4McE4,1064
|
|
18
|
+
mailtea-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mailtea
|
|
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.
|