sendstack 0.1.1__tar.gz → 0.1.2__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.
- {sendstack-0.1.1/src/sendstack.egg-info → sendstack-0.1.2}/PKG-INFO +45 -1
- {sendstack-0.1.1 → sendstack-0.1.2}/README.md +44 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/pyproject.toml +1 -1
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack/__init__.py +11 -0
- sendstack-0.1.2/src/sendstack/files.py +83 -0
- {sendstack-0.1.1 → sendstack-0.1.2/src/sendstack.egg-info}/PKG-INFO +45 -1
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack.egg-info/SOURCES.txt +2 -0
- sendstack-0.1.2/tests/test_files.py +86 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/LICENSE +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/setup.cfg +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack/client.py +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack/errors.py +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack/py.typed +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack/types.py +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack/utils.py +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack.egg-info/dependency_links.txt +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack.egg-info/requires.txt +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/src/sendstack.egg-info/top_level.txt +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/tests/test_conformance.py +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/tests/test_distribution_identity.py +0 -0
- {sendstack-0.1.1 → sendstack-0.1.2}/tests/test_sendstack.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sendstack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Sync and async Python SDK for the SendStack email SaaS API.
|
|
5
5
|
Author: Noria Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -293,6 +293,45 @@ client.emails.send(
|
|
|
293
293
|
)
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
+
### Reading from files
|
|
297
|
+
|
|
298
|
+
The API accepts strings (`html`/`text`) and base64 (`attachments`). These
|
|
299
|
+
stdlib-only helpers do the read-and-encode step so you don't repeat it:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from sendstack import (
|
|
303
|
+
Sendstack,
|
|
304
|
+
html_from_file,
|
|
305
|
+
text_from_file,
|
|
306
|
+
attachment_from_file,
|
|
307
|
+
attachment_from_bytes,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
client.emails.send(
|
|
311
|
+
{
|
|
312
|
+
"from": "billing@example.com",
|
|
313
|
+
"to": "customer@example.com",
|
|
314
|
+
"subject": "Your invoice",
|
|
315
|
+
"html": html_from_file("templates/invoice.html"),
|
|
316
|
+
"text": text_from_file("templates/invoice.txt"),
|
|
317
|
+
"attachments": [
|
|
318
|
+
# From a path: filename defaults to the basename, content is base64-encoded.
|
|
319
|
+
attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
|
|
320
|
+
# From in-memory bytes (e.g. a generated PDF): filename is required.
|
|
321
|
+
attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
|
|
322
|
+
],
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
|
|
328
|
+
file into a string.
|
|
329
|
+
- `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
|
|
330
|
+
— read a file into a base64 attachment `dict`. `filename` defaults to the basename.
|
|
331
|
+
`path` accepts a `str` or any `os.PathLike`.
|
|
332
|
+
- `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
|
|
333
|
+
— encode in-memory `bytes`; `filename` is required.
|
|
334
|
+
|
|
296
335
|
## Domains
|
|
297
336
|
|
|
298
337
|
```python
|
|
@@ -510,6 +549,11 @@ Clients and errors:
|
|
|
510
549
|
- `SendstackError`
|
|
511
550
|
- `DEFAULT_BASE_URL`
|
|
512
551
|
|
|
552
|
+
Filesystem helpers:
|
|
553
|
+
|
|
554
|
+
- `html_from_file`, `text_from_file`
|
|
555
|
+
- `attachment_from_file`, `attachment_from_bytes`
|
|
556
|
+
|
|
513
557
|
Auth, options, and machinery:
|
|
514
558
|
|
|
515
559
|
- `BearerAuthStrategy`, `HeadersAuthStrategy`, `SendstackAuthStrategy`
|
|
@@ -266,6 +266,45 @@ client.emails.send(
|
|
|
266
266
|
)
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
+
### Reading from files
|
|
270
|
+
|
|
271
|
+
The API accepts strings (`html`/`text`) and base64 (`attachments`). These
|
|
272
|
+
stdlib-only helpers do the read-and-encode step so you don't repeat it:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from sendstack import (
|
|
276
|
+
Sendstack,
|
|
277
|
+
html_from_file,
|
|
278
|
+
text_from_file,
|
|
279
|
+
attachment_from_file,
|
|
280
|
+
attachment_from_bytes,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
client.emails.send(
|
|
284
|
+
{
|
|
285
|
+
"from": "billing@example.com",
|
|
286
|
+
"to": "customer@example.com",
|
|
287
|
+
"subject": "Your invoice",
|
|
288
|
+
"html": html_from_file("templates/invoice.html"),
|
|
289
|
+
"text": text_from_file("templates/invoice.txt"),
|
|
290
|
+
"attachments": [
|
|
291
|
+
# From a path: filename defaults to the basename, content is base64-encoded.
|
|
292
|
+
attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
|
|
293
|
+
# From in-memory bytes (e.g. a generated PDF): filename is required.
|
|
294
|
+
attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
|
|
295
|
+
],
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
|
|
301
|
+
file into a string.
|
|
302
|
+
- `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
|
|
303
|
+
— read a file into a base64 attachment `dict`. `filename` defaults to the basename.
|
|
304
|
+
`path` accepts a `str` or any `os.PathLike`.
|
|
305
|
+
- `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
|
|
306
|
+
— encode in-memory `bytes`; `filename` is required.
|
|
307
|
+
|
|
269
308
|
## Domains
|
|
270
309
|
|
|
271
310
|
```python
|
|
@@ -483,6 +522,11 @@ Clients and errors:
|
|
|
483
522
|
- `SendstackError`
|
|
484
523
|
- `DEFAULT_BASE_URL`
|
|
485
524
|
|
|
525
|
+
Filesystem helpers:
|
|
526
|
+
|
|
527
|
+
- `html_from_file`, `text_from_file`
|
|
528
|
+
- `attachment_from_file`, `attachment_from_bytes`
|
|
529
|
+
|
|
486
530
|
Auth, options, and machinery:
|
|
487
531
|
|
|
488
532
|
- `BearerAuthStrategy`, `HeadersAuthStrategy`, `SendstackAuthStrategy`
|
|
@@ -20,6 +20,12 @@ from .errors import (
|
|
|
20
20
|
is_success_envelope,
|
|
21
21
|
to_sendstack_error,
|
|
22
22
|
)
|
|
23
|
+
from .files import (
|
|
24
|
+
attachment_from_bytes,
|
|
25
|
+
attachment_from_file,
|
|
26
|
+
html_from_file,
|
|
27
|
+
text_from_file,
|
|
28
|
+
)
|
|
23
29
|
from .types import (
|
|
24
30
|
DEFAULT_BASE_URL,
|
|
25
31
|
BearerAuthStrategy,
|
|
@@ -77,6 +83,11 @@ __all__ = [
|
|
|
77
83
|
"is_error_envelope",
|
|
78
84
|
"is_success_envelope",
|
|
79
85
|
"to_sendstack_error",
|
|
86
|
+
# Filesystem helpers
|
|
87
|
+
"html_from_file",
|
|
88
|
+
"text_from_file",
|
|
89
|
+
"attachment_from_file",
|
|
90
|
+
"attachment_from_bytes",
|
|
80
91
|
# Auth
|
|
81
92
|
"BearerAuthStrategy",
|
|
82
93
|
"HeadersAuthStrategy",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Filesystem conveniences for building email payloads from local files.
|
|
2
|
+
|
|
3
|
+
The SendStack API accepts strings (``html``/``text``) and base64 (``attachments``).
|
|
4
|
+
These helpers do the read-and-encode step so callers don't repeat it. They use
|
|
5
|
+
only the standard library, so they add no dependencies.
|
|
6
|
+
|
|
7
|
+
The returned attachment ``dict`` uses the snake_case wire field names, so it can
|
|
8
|
+
be dropped straight into ``emails.send({"attachments": [...]})``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
PathLike = str | os.PathLike[str]
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"html_from_file",
|
|
22
|
+
"text_from_file",
|
|
23
|
+
"attachment_from_file",
|
|
24
|
+
"attachment_from_bytes",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def html_from_file(path: PathLike, *, encoding: str = "utf-8") -> str:
|
|
29
|
+
"""Read a text file (e.g. an HTML template) into a string for ``html``."""
|
|
30
|
+
return Path(path).read_text(encoding=encoding)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def text_from_file(path: PathLike, *, encoding: str = "utf-8") -> str:
|
|
34
|
+
"""Read a text file (e.g. a ``.txt`` body) into a string for ``text``."""
|
|
35
|
+
return Path(path).read_text(encoding=encoding)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def attachment_from_file(
|
|
39
|
+
path: PathLike,
|
|
40
|
+
*,
|
|
41
|
+
filename: str | None = None,
|
|
42
|
+
content_type: str | None = None,
|
|
43
|
+
inline: bool | None = None,
|
|
44
|
+
content_id: str | None = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Read a file from disk into a base64 attachment dict.
|
|
47
|
+
|
|
48
|
+
``filename`` defaults to the file's basename. The result is ready to drop
|
|
49
|
+
into ``emails.send({"attachments": [...]})``.
|
|
50
|
+
"""
|
|
51
|
+
data = Path(path).read_bytes()
|
|
52
|
+
return attachment_from_bytes(
|
|
53
|
+
data,
|
|
54
|
+
filename=filename if filename is not None else Path(os.fspath(path)).name,
|
|
55
|
+
content_type=content_type,
|
|
56
|
+
inline=inline,
|
|
57
|
+
content_id=content_id,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def attachment_from_bytes(
|
|
62
|
+
data: bytes,
|
|
63
|
+
*,
|
|
64
|
+
filename: str,
|
|
65
|
+
content_type: str | None = None,
|
|
66
|
+
inline: bool | None = None,
|
|
67
|
+
content_id: str | None = None,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
"""Encode in-memory bytes (e.g. a generated PDF) into a base64 attachment dict.
|
|
70
|
+
|
|
71
|
+
``filename`` is required since there is no path to derive one from.
|
|
72
|
+
"""
|
|
73
|
+
attachment: dict[str, Any] = {
|
|
74
|
+
"filename": filename,
|
|
75
|
+
"content_base64": base64.b64encode(data).decode("ascii"),
|
|
76
|
+
}
|
|
77
|
+
if content_type is not None:
|
|
78
|
+
attachment["content_type"] = content_type
|
|
79
|
+
if inline is not None:
|
|
80
|
+
attachment["inline"] = inline
|
|
81
|
+
if content_id is not None:
|
|
82
|
+
attachment["content_id"] = content_id
|
|
83
|
+
return attachment
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sendstack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Sync and async Python SDK for the SendStack email SaaS API.
|
|
5
5
|
Author: Noria Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -293,6 +293,45 @@ client.emails.send(
|
|
|
293
293
|
)
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
+
### Reading from files
|
|
297
|
+
|
|
298
|
+
The API accepts strings (`html`/`text`) and base64 (`attachments`). These
|
|
299
|
+
stdlib-only helpers do the read-and-encode step so you don't repeat it:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from sendstack import (
|
|
303
|
+
Sendstack,
|
|
304
|
+
html_from_file,
|
|
305
|
+
text_from_file,
|
|
306
|
+
attachment_from_file,
|
|
307
|
+
attachment_from_bytes,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
client.emails.send(
|
|
311
|
+
{
|
|
312
|
+
"from": "billing@example.com",
|
|
313
|
+
"to": "customer@example.com",
|
|
314
|
+
"subject": "Your invoice",
|
|
315
|
+
"html": html_from_file("templates/invoice.html"),
|
|
316
|
+
"text": text_from_file("templates/invoice.txt"),
|
|
317
|
+
"attachments": [
|
|
318
|
+
# From a path: filename defaults to the basename, content is base64-encoded.
|
|
319
|
+
attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
|
|
320
|
+
# From in-memory bytes (e.g. a generated PDF): filename is required.
|
|
321
|
+
attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
|
|
322
|
+
],
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
|
|
328
|
+
file into a string.
|
|
329
|
+
- `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
|
|
330
|
+
— read a file into a base64 attachment `dict`. `filename` defaults to the basename.
|
|
331
|
+
`path` accepts a `str` or any `os.PathLike`.
|
|
332
|
+
- `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
|
|
333
|
+
— encode in-memory `bytes`; `filename` is required.
|
|
334
|
+
|
|
296
335
|
## Domains
|
|
297
336
|
|
|
298
337
|
```python
|
|
@@ -510,6 +549,11 @@ Clients and errors:
|
|
|
510
549
|
- `SendstackError`
|
|
511
550
|
- `DEFAULT_BASE_URL`
|
|
512
551
|
|
|
552
|
+
Filesystem helpers:
|
|
553
|
+
|
|
554
|
+
- `html_from_file`, `text_from_file`
|
|
555
|
+
- `attachment_from_file`, `attachment_from_bytes`
|
|
556
|
+
|
|
513
557
|
Auth, options, and machinery:
|
|
514
558
|
|
|
515
559
|
- `BearerAuthStrategy`, `HeadersAuthStrategy`, `SendstackAuthStrategy`
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
src/sendstack/__init__.py
|
|
5
5
|
src/sendstack/client.py
|
|
6
6
|
src/sendstack/errors.py
|
|
7
|
+
src/sendstack/files.py
|
|
7
8
|
src/sendstack/py.typed
|
|
8
9
|
src/sendstack/types.py
|
|
9
10
|
src/sendstack/utils.py
|
|
@@ -14,4 +15,5 @@ src/sendstack.egg-info/requires.txt
|
|
|
14
15
|
src/sendstack.egg-info/top_level.txt
|
|
15
16
|
tests/test_conformance.py
|
|
16
17
|
tests/test_distribution_identity.py
|
|
18
|
+
tests/test_files.py
|
|
17
19
|
tests/test_sendstack.py
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tests for the filesystem convenience helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sendstack import (
|
|
9
|
+
attachment_from_bytes,
|
|
10
|
+
attachment_from_file,
|
|
11
|
+
html_from_file,
|
|
12
|
+
text_from_file,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_html_from_file_reads_utf8(tmp_path: Path) -> None:
|
|
17
|
+
path = tmp_path / "welcome.html"
|
|
18
|
+
path.write_text("<p>Hëllo</p>", encoding="utf-8")
|
|
19
|
+
|
|
20
|
+
assert html_from_file(path) == "<p>Hëllo</p>"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_text_from_file_reads_utf8_and_honors_encoding(tmp_path: Path) -> None:
|
|
24
|
+
path = tmp_path / "welcome.txt"
|
|
25
|
+
path.write_text("Héllo", encoding="latin-1")
|
|
26
|
+
|
|
27
|
+
assert text_from_file(path, encoding="latin-1") == "Héllo"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_attachment_from_file_defaults_filename_to_basename(tmp_path: Path) -> None:
|
|
31
|
+
path = tmp_path / "invoice.pdf"
|
|
32
|
+
payload = b"%PDF-1.4 fake"
|
|
33
|
+
path.write_bytes(payload)
|
|
34
|
+
|
|
35
|
+
assert attachment_from_file(path) == {
|
|
36
|
+
"filename": "invoice.pdf",
|
|
37
|
+
"content_base64": base64.b64encode(payload).decode("ascii"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_attachment_from_file_accepts_str_path_and_overrides(tmp_path: Path) -> None:
|
|
42
|
+
path = tmp_path / "logo.png"
|
|
43
|
+
payload = bytes([0x89, 0x50, 0x4E, 0x47])
|
|
44
|
+
path.write_bytes(payload)
|
|
45
|
+
|
|
46
|
+
# Pass the path as a str to exercise the os.fspath branch.
|
|
47
|
+
assert attachment_from_file(
|
|
48
|
+
str(path),
|
|
49
|
+
filename="brand-logo.png",
|
|
50
|
+
content_type="image/png",
|
|
51
|
+
inline=True,
|
|
52
|
+
content_id="logo",
|
|
53
|
+
) == {
|
|
54
|
+
"filename": "brand-logo.png",
|
|
55
|
+
"content_base64": base64.b64encode(payload).decode("ascii"),
|
|
56
|
+
"content_type": "image/png",
|
|
57
|
+
"inline": True,
|
|
58
|
+
"content_id": "logo",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_attachment_from_bytes_minimal() -> None:
|
|
63
|
+
data = b"generated bytes"
|
|
64
|
+
|
|
65
|
+
assert attachment_from_bytes(data, filename="report.bin") == {
|
|
66
|
+
"filename": "report.bin",
|
|
67
|
+
"content_base64": base64.b64encode(data).decode("ascii"),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_attachment_from_bytes_with_all_metadata() -> None:
|
|
72
|
+
data = bytes([1, 2, 3, 4])
|
|
73
|
+
|
|
74
|
+
assert attachment_from_bytes(
|
|
75
|
+
data,
|
|
76
|
+
filename="raw.dat",
|
|
77
|
+
content_type="application/octet-stream",
|
|
78
|
+
inline=False,
|
|
79
|
+
content_id="raw",
|
|
80
|
+
) == {
|
|
81
|
+
"filename": "raw.dat",
|
|
82
|
+
"content_base64": base64.b64encode(data).decode("ascii"),
|
|
83
|
+
"content_type": "application/octet-stream",
|
|
84
|
+
"inline": False,
|
|
85
|
+
"content_id": "raw",
|
|
86
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|