Skip to content

Commit ef0919e

Browse files
authored
feat: implement Team API API endpoints (#12) BREAKING CHANGE
* feat: implement Team API API endpoints * chore: reorder and clean up TypedDict definitions in types module * Fix styling
1 parent 0cb8d66 commit ef0919e

13 files changed

Lines changed: 2259 additions & 104 deletions

File tree

UPGRADE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Upgrade To v2
2+
3+
This guide covers upgrading from the latest released v1 Python SDK to v2.
4+
5+
## Highlights
6+
7+
- Sending email now lives behind `Lettermint.email(token)`.
8+
- The full Lettermint API is available through `Lettermint.api(token)`.
9+
- Sending tokens use `x-lettermint-token`; full API tokens use `Authorization: Bearer`.
10+
- `ping()` returns the raw trimmed `pong` response.
11+
- Request and response shapes are generated from the OpenAPI specs as `TypedDict`/`Literal` types.
12+
13+
## Replace Client Construction
14+
15+
```python
16+
from lettermint import Lettermint
17+
18+
email = Lettermint.email("sending-token")
19+
api = Lettermint.api("api-token")
20+
```
21+
22+
Existing `Lettermint(api_token="...").email` sending usage still works.
23+
24+
## Batch Sending
25+
26+
```python
27+
email.send_batch([
28+
{"from": "sender@example.com", "to": ["user@example.com"], "subject": "Hello", "text": "Hi"}
29+
])
30+
```
31+
32+
## Full API
33+
34+
```python
35+
domains = api.domains.list()
36+
message_html = api.messages.html("message-id")
37+
```
38+

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,6 @@ ignore = [
8282

8383
[tool.ruff.lint.isort]
8484
known-first-party = ["lettermint"]
85+
86+
[tool.ruff.lint.per-file-ignores]
87+
"src/lettermint/types.py" = ["UP013"]

src/lettermint/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@
5050
ValidationError,
5151
WebhookVerificationError,
5252
)
53-
from .lettermint import AsyncLettermint, Lettermint
54-
from .types import EmailAttachment, EmailPayload, EmailStatus, SendEmailResponse
53+
from .lettermint import ApiClient, AsyncApiClient, AsyncLettermint, Lettermint
54+
from .types import (
55+
EmailAttachment,
56+
EmailPayload,
57+
EmailStatus,
58+
SendBatchEmailResponse,
59+
SendEmailResponse,
60+
)
5561
from .webhook import Webhook
5662

5763
__version__ = "1.0.0"
@@ -60,6 +66,8 @@
6066
# Main clients
6167
"Lettermint",
6268
"AsyncLettermint",
69+
"ApiClient",
70+
"AsyncApiClient",
6371
# Webhook
6472
"Webhook",
6573
# Exceptions
@@ -77,4 +85,5 @@
7785
"EmailPayload",
7886
"EmailStatus",
7987
"SendEmailResponse",
88+
"SendBatchEmailResponse",
8089
]

src/lettermint/client.py

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,19 @@ def __init__(
3333
api_token: str,
3434
base_url: str | None = None,
3535
timeout: float = DEFAULT_TIMEOUT,
36+
auth_scheme: str = "sending",
3637
) -> None:
3738
self._api_token = api_token
3839
self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
3940
self._timeout = timeout
41+
self._auth_scheme = auth_scheme
4042
self._client = httpx.Client(
4143
base_url=self._base_url,
4244
timeout=self._timeout,
4345
headers={
4446
"Content-Type": "application/json",
4547
"Accept": "application/json",
4648
"User-Agent": f"Lettermint/{version('lettermint')} (Python; python {platform.python_version()})",
47-
"x-lettermint-token": self._api_token,
4849
},
4950
)
5051

@@ -94,6 +95,18 @@ def _handle_response(self, response: httpx.Response) -> Any:
9495
response_body,
9596
)
9697

98+
def _request_headers(self, headers: dict[str, str] | None = None) -> dict[str, str]:
99+
safe_headers = {
100+
key: value
101+
for key, value in (headers or {}).items()
102+
if key.lower() not in {"authorization", "x-lettermint-token"}
103+
}
104+
105+
if self._auth_scheme == "bearer":
106+
return {**safe_headers, "Authorization": f"Bearer {self._api_token}"}
107+
108+
return {**safe_headers, "x-lettermint-token": self._api_token}
109+
97110
def get(
98111
self,
99112
path: str,
@@ -115,11 +128,27 @@ def get(
115128
TimeoutError: On request timeout.
116129
"""
117130
try:
118-
response = self._client.get(path, params=params, headers=headers)
131+
response = self._client.get(path, params=params, headers=self._request_headers(headers))
119132
return self._handle_response(response)
120133
except httpx.TimeoutException as e:
121134
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
122135

136+
def get_raw(
137+
self,
138+
path: str,
139+
params: dict[str, str] | None = None,
140+
headers: dict[str, str] | None = None,
141+
) -> str:
142+
"""Make a GET request and return the raw response body."""
143+
try:
144+
response = self._client.get(path, params=params, headers=self._request_headers(headers))
145+
if response.is_success:
146+
return response.text
147+
self._handle_response(response)
148+
raise AssertionError("unreachable")
149+
except httpx.TimeoutException as e:
150+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
151+
123152
def post(
124153
self,
125154
path: str,
@@ -141,7 +170,7 @@ def post(
141170
TimeoutError: On request timeout.
142171
"""
143172
try:
144-
response = self._client.post(path, json=data, headers=headers)
173+
response = self._client.post(path, json=data, headers=self._request_headers(headers))
145174
return self._handle_response(response)
146175
except httpx.TimeoutException as e:
147176
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
@@ -167,14 +196,15 @@ def put(
167196
TimeoutError: On request timeout.
168197
"""
169198
try:
170-
response = self._client.put(path, json=data, headers=headers)
199+
response = self._client.put(path, json=data, headers=self._request_headers(headers))
171200
return self._handle_response(response)
172201
except httpx.TimeoutException as e:
173202
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
174203

175204
def delete(
176205
self,
177206
path: str,
207+
params: dict[str, str] | None = None,
178208
headers: dict[str, str] | None = None,
179209
) -> Any:
180210
"""Make a DELETE request to the API.
@@ -191,7 +221,11 @@ def delete(
191221
TimeoutError: On request timeout.
192222
"""
193223
try:
194-
response = self._client.delete(path, headers=headers)
224+
response = self._client.delete(
225+
path,
226+
params=params,
227+
headers=self._request_headers(headers),
228+
)
195229
return self._handle_response(response)
196230
except httpx.TimeoutException as e:
197231
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
@@ -211,18 +245,19 @@ def __init__(
211245
api_token: str,
212246
base_url: str | None = None,
213247
timeout: float = DEFAULT_TIMEOUT,
248+
auth_scheme: str = "sending",
214249
) -> None:
215250
self._api_token = api_token
216251
self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
217252
self._timeout = timeout
253+
self._auth_scheme = auth_scheme
218254
self._client = httpx.AsyncClient(
219255
base_url=self._base_url,
220256
timeout=self._timeout,
221257
headers={
222258
"Content-Type": "application/json",
223259
"Accept": "application/json",
224260
"User-Agent": f"Lettermint/{version('lettermint')} (Python; python {platform.python_version()})",
225-
"x-lettermint-token": self._api_token,
226261
},
227262
)
228263

@@ -272,6 +307,18 @@ def _handle_response(self, response: httpx.Response) -> Any:
272307
response_body,
273308
)
274309

310+
def _request_headers(self, headers: dict[str, str] | None = None) -> dict[str, str]:
311+
safe_headers = {
312+
key: value
313+
for key, value in (headers or {}).items()
314+
if key.lower() not in {"authorization", "x-lettermint-token"}
315+
}
316+
317+
if self._auth_scheme == "bearer":
318+
return {**safe_headers, "Authorization": f"Bearer {self._api_token}"}
319+
320+
return {**safe_headers, "x-lettermint-token": self._api_token}
321+
275322
async def get(
276323
self,
277324
path: str,
@@ -293,11 +340,35 @@ async def get(
293340
TimeoutError: On request timeout.
294341
"""
295342
try:
296-
response = await self._client.get(path, params=params, headers=headers)
343+
response = await self._client.get(
344+
path,
345+
params=params,
346+
headers=self._request_headers(headers),
347+
)
297348
return self._handle_response(response)
298349
except httpx.TimeoutException as e:
299350
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
300351

352+
async def get_raw(
353+
self,
354+
path: str,
355+
params: dict[str, str] | None = None,
356+
headers: dict[str, str] | None = None,
357+
) -> str:
358+
"""Make a GET request and return the raw response body."""
359+
try:
360+
response = await self._client.get(
361+
path,
362+
params=params,
363+
headers=self._request_headers(headers),
364+
)
365+
if response.is_success:
366+
return response.text
367+
self._handle_response(response)
368+
raise AssertionError("unreachable")
369+
except httpx.TimeoutException as e:
370+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
371+
301372
async def post(
302373
self,
303374
path: str,
@@ -319,7 +390,11 @@ async def post(
319390
TimeoutError: On request timeout.
320391
"""
321392
try:
322-
response = await self._client.post(path, json=data, headers=headers)
393+
response = await self._client.post(
394+
path,
395+
json=data,
396+
headers=self._request_headers(headers),
397+
)
323398
return self._handle_response(response)
324399
except httpx.TimeoutException as e:
325400
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
@@ -345,14 +420,19 @@ async def put(
345420
TimeoutError: On request timeout.
346421
"""
347422
try:
348-
response = await self._client.put(path, json=data, headers=headers)
423+
response = await self._client.put(
424+
path,
425+
json=data,
426+
headers=self._request_headers(headers),
427+
)
349428
return self._handle_response(response)
350429
except httpx.TimeoutException as e:
351430
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
352431

353432
async def delete(
354433
self,
355434
path: str,
435+
params: dict[str, str] | None = None,
356436
headers: dict[str, str] | None = None,
357437
) -> Any:
358438
"""Make a DELETE request to the API.
@@ -369,7 +449,11 @@ async def delete(
369449
TimeoutError: On request timeout.
370450
"""
371451
try:
372-
response = await self._client.delete(path, headers=headers)
452+
response = await self._client.delete(
453+
path,
454+
params=params,
455+
headers=self._request_headers(headers),
456+
)
373457
return self._handle_response(response)
374458
except httpx.TimeoutException as e:
375459
raise TimeoutError(f"Request timeout after {self._timeout}s") from e

0 commit comments

Comments
 (0)