sendstack 0.1.1__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sendstack
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Sync and async Python SDK for the SendStack email SaaS API.
5
5
  Author: Noria Labs
6
6
  License-Expression: MIT
@@ -123,11 +123,13 @@ decoded payload; on `AsyncSendstack` the same call returns a coroutine you
123
123
 
124
124
  ### Base URL
125
125
 
126
- The SDK defaults to `https://mailer.norialabs.com`, matching the current live
127
- API. Override it when you move to the SendStack domain:
126
+ The SDK defaults to `https://sendstack.norialabs.com/api/v1` (the versioned API
127
+ base). Override `base_url` to point at another environment include the
128
+ `/api/v1` version segment, since resource paths (e.g. `/emails`) are sent
129
+ relative to whatever base you provide:
128
130
 
129
131
  ```python
130
- client = Sendstack(token, base_url="https://sendstack.norialabs.com")
132
+ client = Sendstack(token, base_url="https://staging.norialabs.com/api/v1")
131
133
  ```
132
134
 
133
135
  ## Docs Split
@@ -139,7 +141,7 @@ The SaaS docs remain the canonical source for product/API behavior: account
139
141
  setup, API tokens, domain verification, DNS records, webhook event catalogs,
140
142
  deliverability concepts, provider behavior, dashboard workflows, and the raw
141
143
  HTTP API reference. Current live SaaS docs are at
142
- `https://mailer.norialabs.com/api/docs`.
144
+ `https://sendstack.norialabs.com/api/docs`.
143
145
 
144
146
  ## Auth
145
147
 
@@ -162,7 +164,7 @@ request (sync or async callables both work):
162
164
  from sendstack import BearerAuthStrategy, Sendstack
163
165
 
164
166
  client = Sendstack(
165
- base_url="https://mailer.norialabs.com",
167
+ base_url="https://sendstack.norialabs.com/api/v1",
166
168
  auth=BearerAuthStrategy(token=lambda context: get_fresh_token()),
167
169
  )
168
170
  ```
@@ -293,6 +295,45 @@ client.emails.send(
293
295
  )
294
296
  ```
295
297
 
298
+ ### Reading from files
299
+
300
+ The API accepts strings (`html`/`text`) and base64 (`attachments`). These
301
+ stdlib-only helpers do the read-and-encode step so you don't repeat it:
302
+
303
+ ```python
304
+ from sendstack import (
305
+ Sendstack,
306
+ html_from_file,
307
+ text_from_file,
308
+ attachment_from_file,
309
+ attachment_from_bytes,
310
+ )
311
+
312
+ client.emails.send(
313
+ {
314
+ "from": "billing@example.com",
315
+ "to": "customer@example.com",
316
+ "subject": "Your invoice",
317
+ "html": html_from_file("templates/invoice.html"),
318
+ "text": text_from_file("templates/invoice.txt"),
319
+ "attachments": [
320
+ # From a path: filename defaults to the basename, content is base64-encoded.
321
+ attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
322
+ # From in-memory bytes (e.g. a generated PDF): filename is required.
323
+ attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
324
+ ],
325
+ }
326
+ )
327
+ ```
328
+
329
+ - `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
330
+ file into a string.
331
+ - `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
332
+ — read a file into a base64 attachment `dict`. `filename` defaults to the basename.
333
+ `path` accepts a `str` or any `os.PathLike`.
334
+ - `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
335
+ — encode in-memory `bytes`; `filename` is required.
336
+
296
337
  ## Domains
297
338
 
298
339
  ```python
@@ -510,6 +551,11 @@ Clients and errors:
510
551
  - `SendstackError`
511
552
  - `DEFAULT_BASE_URL`
512
553
 
554
+ Filesystem helpers:
555
+
556
+ - `html_from_file`, `text_from_file`
557
+ - `attachment_from_file`, `attachment_from_bytes`
558
+
513
559
  Auth, options, and machinery:
514
560
 
515
561
  - `BearerAuthStrategy`, `HeadersAuthStrategy`, `SendstackAuthStrategy`
@@ -96,11 +96,13 @@ decoded payload; on `AsyncSendstack` the same call returns a coroutine you
96
96
 
97
97
  ### Base URL
98
98
 
99
- The SDK defaults to `https://mailer.norialabs.com`, matching the current live
100
- API. Override it when you move to the SendStack domain:
99
+ The SDK defaults to `https://sendstack.norialabs.com/api/v1` (the versioned API
100
+ base). Override `base_url` to point at another environment include the
101
+ `/api/v1` version segment, since resource paths (e.g. `/emails`) are sent
102
+ relative to whatever base you provide:
101
103
 
102
104
  ```python
103
- client = Sendstack(token, base_url="https://sendstack.norialabs.com")
105
+ client = Sendstack(token, base_url="https://staging.norialabs.com/api/v1")
104
106
  ```
105
107
 
106
108
  ## Docs Split
@@ -112,7 +114,7 @@ The SaaS docs remain the canonical source for product/API behavior: account
112
114
  setup, API tokens, domain verification, DNS records, webhook event catalogs,
113
115
  deliverability concepts, provider behavior, dashboard workflows, and the raw
114
116
  HTTP API reference. Current live SaaS docs are at
115
- `https://mailer.norialabs.com/api/docs`.
117
+ `https://sendstack.norialabs.com/api/docs`.
116
118
 
117
119
  ## Auth
118
120
 
@@ -135,7 +137,7 @@ request (sync or async callables both work):
135
137
  from sendstack import BearerAuthStrategy, Sendstack
136
138
 
137
139
  client = Sendstack(
138
- base_url="https://mailer.norialabs.com",
140
+ base_url="https://sendstack.norialabs.com/api/v1",
139
141
  auth=BearerAuthStrategy(token=lambda context: get_fresh_token()),
140
142
  )
141
143
  ```
@@ -266,6 +268,45 @@ client.emails.send(
266
268
  )
267
269
  ```
268
270
 
271
+ ### Reading from files
272
+
273
+ The API accepts strings (`html`/`text`) and base64 (`attachments`). These
274
+ stdlib-only helpers do the read-and-encode step so you don't repeat it:
275
+
276
+ ```python
277
+ from sendstack import (
278
+ Sendstack,
279
+ html_from_file,
280
+ text_from_file,
281
+ attachment_from_file,
282
+ attachment_from_bytes,
283
+ )
284
+
285
+ client.emails.send(
286
+ {
287
+ "from": "billing@example.com",
288
+ "to": "customer@example.com",
289
+ "subject": "Your invoice",
290
+ "html": html_from_file("templates/invoice.html"),
291
+ "text": text_from_file("templates/invoice.txt"),
292
+ "attachments": [
293
+ # From a path: filename defaults to the basename, content is base64-encoded.
294
+ attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
295
+ # From in-memory bytes (e.g. a generated PDF): filename is required.
296
+ attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
297
+ ],
298
+ }
299
+ )
300
+ ```
301
+
302
+ - `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
303
+ file into a string.
304
+ - `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
305
+ — read a file into a base64 attachment `dict`. `filename` defaults to the basename.
306
+ `path` accepts a `str` or any `os.PathLike`.
307
+ - `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
308
+ — encode in-memory `bytes`; `filename` is required.
309
+
269
310
  ## Domains
270
311
 
271
312
  ```python
@@ -483,6 +524,11 @@ Clients and errors:
483
524
  - `SendstackError`
484
525
  - `DEFAULT_BASE_URL`
485
526
 
527
+ Filesystem helpers:
528
+
529
+ - `html_from_file`, `text_from_file`
530
+ - `attachment_from_file`, `attachment_from_bytes`
531
+
486
532
  Auth, options, and machinery:
487
533
 
488
534
  - `BearerAuthStrategy`, `HeadersAuthStrategy`, `SendstackAuthStrategy`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sendstack"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Sync and async Python SDK for the SendStack email SaaS API."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
@@ -20,8 +20,10 @@ from typing import Any, Literal, NotRequired, TypeAlias, TypedDict
20
20
 
21
21
  import httpx
22
22
 
23
- # The live API host. Override via ``base_url`` when SendStack moves domains.
24
- DEFAULT_BASE_URL = "https://mailer.norialabs.com"
23
+ # The versioned API base. Override via ``base_url`` for other environments;
24
+ # include the /api/v1 segment, since resource paths (e.g. /emails) are sent
25
+ # relative to whatever base is configured.
26
+ DEFAULT_BASE_URL = "https://sendstack.norialabs.com/api/v1"
25
27
 
26
28
  # Sentinel distinguishing "no body" from an explicit ``None`` body.
27
29
  UNSET: Any = object()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sendstack
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Sync and async Python SDK for the SendStack email SaaS API.
5
5
  Author: Noria Labs
6
6
  License-Expression: MIT
@@ -123,11 +123,13 @@ decoded payload; on `AsyncSendstack` the same call returns a coroutine you
123
123
 
124
124
  ### Base URL
125
125
 
126
- The SDK defaults to `https://mailer.norialabs.com`, matching the current live
127
- API. Override it when you move to the SendStack domain:
126
+ The SDK defaults to `https://sendstack.norialabs.com/api/v1` (the versioned API
127
+ base). Override `base_url` to point at another environment include the
128
+ `/api/v1` version segment, since resource paths (e.g. `/emails`) are sent
129
+ relative to whatever base you provide:
128
130
 
129
131
  ```python
130
- client = Sendstack(token, base_url="https://sendstack.norialabs.com")
132
+ client = Sendstack(token, base_url="https://staging.norialabs.com/api/v1")
131
133
  ```
132
134
 
133
135
  ## Docs Split
@@ -139,7 +141,7 @@ The SaaS docs remain the canonical source for product/API behavior: account
139
141
  setup, API tokens, domain verification, DNS records, webhook event catalogs,
140
142
  deliverability concepts, provider behavior, dashboard workflows, and the raw
141
143
  HTTP API reference. Current live SaaS docs are at
142
- `https://mailer.norialabs.com/api/docs`.
144
+ `https://sendstack.norialabs.com/api/docs`.
143
145
 
144
146
  ## Auth
145
147
 
@@ -162,7 +164,7 @@ request (sync or async callables both work):
162
164
  from sendstack import BearerAuthStrategy, Sendstack
163
165
 
164
166
  client = Sendstack(
165
- base_url="https://mailer.norialabs.com",
167
+ base_url="https://sendstack.norialabs.com/api/v1",
166
168
  auth=BearerAuthStrategy(token=lambda context: get_fresh_token()),
167
169
  )
168
170
  ```
@@ -293,6 +295,45 @@ client.emails.send(
293
295
  )
294
296
  ```
295
297
 
298
+ ### Reading from files
299
+
300
+ The API accepts strings (`html`/`text`) and base64 (`attachments`). These
301
+ stdlib-only helpers do the read-and-encode step so you don't repeat it:
302
+
303
+ ```python
304
+ from sendstack import (
305
+ Sendstack,
306
+ html_from_file,
307
+ text_from_file,
308
+ attachment_from_file,
309
+ attachment_from_bytes,
310
+ )
311
+
312
+ client.emails.send(
313
+ {
314
+ "from": "billing@example.com",
315
+ "to": "customer@example.com",
316
+ "subject": "Your invoice",
317
+ "html": html_from_file("templates/invoice.html"),
318
+ "text": text_from_file("templates/invoice.txt"),
319
+ "attachments": [
320
+ # From a path: filename defaults to the basename, content is base64-encoded.
321
+ attachment_from_file("invoices/2026-06.pdf", content_type="application/pdf"),
322
+ # From in-memory bytes (e.g. a generated PDF): filename is required.
323
+ attachment_from_bytes(generated_pdf, filename="summary.pdf", content_type="application/pdf"),
324
+ ],
325
+ }
326
+ )
327
+ ```
328
+
329
+ - `html_from_file(path, *, encoding="utf-8")` / `text_from_file(...)` — read a text
330
+ file into a string.
331
+ - `attachment_from_file(path, *, filename=None, content_type=None, inline=None, content_id=None)`
332
+ — read a file into a base64 attachment `dict`. `filename` defaults to the basename.
333
+ `path` accepts a `str` or any `os.PathLike`.
334
+ - `attachment_from_bytes(data, *, filename, content_type=None, inline=None, content_id=None)`
335
+ — encode in-memory `bytes`; `filename` is required.
336
+
296
337
  ## Domains
297
338
 
298
339
  ```python
@@ -510,6 +551,11 @@ Clients and errors:
510
551
  - `SendstackError`
511
552
  - `DEFAULT_BASE_URL`
512
553
 
554
+ Filesystem helpers:
555
+
556
+ - `html_from_file`, `text_from_file`
557
+ - `attachment_from_file`, `attachment_from_bytes`
558
+
513
559
  Auth, options, and machinery:
514
560
 
515
561
  - `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
+ }
@@ -131,7 +131,7 @@ def test_send_email_aliases_and_unwraps():
131
131
 
132
132
  assert result == {"id": "m_1", "status": "queued"}
133
133
  request = calls[0]
134
- assert str(request.url) == "https://mailer.norialabs.com/emails"
134
+ assert str(request.url) == "https://sendstack.norialabs.com/api/v1/emails"
135
135
  assert request.method == "POST"
136
136
  assert request.headers["authorization"] == "Bearer tok"
137
137
  assert request.headers["accept"] == "application/json"
@@ -192,10 +192,10 @@ def test_emails_get_events_cancel_requeue():
192
192
  assert client.emails.requeue("m1")["status"] == "queued"
193
193
  finally:
194
194
  http.close()
195
- assert calls[0].url.path == "/emails/m 1" # decoded back by httpx (sent percent-encoded)
196
- assert calls[1].url.path == "/emails/m1/events"
197
- assert calls[2].method == "POST" and calls[2].url.path == "/emails/m1/cancel"
198
- assert calls[3].url.path == "/emails/m1/requeue"
195
+ assert calls[0].url.path == "/api/v1/emails/m 1" # decoded back by httpx (sent percent-encoded)
196
+ assert calls[1].url.path == "/api/v1/emails/m1/events"
197
+ assert calls[2].method == "POST" and calls[2].url.path == "/api/v1/emails/m1/cancel"
198
+ assert calls[3].url.path == "/api/v1/emails/m1/requeue"
199
199
 
200
200
 
201
201
  # --------------------------------------------------------------------------- #
@@ -224,8 +224,8 @@ def test_domains_resource():
224
224
  "provider_id": "p1",
225
225
  "custom_return_path": "bounce",
226
226
  }
227
- assert calls[1].method == "GET" and calls[1].url.path == "/domains"
228
- assert calls[3].url.path == "/domains/d1/verify"
227
+ assert calls[1].method == "GET" and calls[1].url.path == "/api/v1/domains"
228
+ assert calls[3].url.path == "/api/v1/domains/d1/verify"
229
229
 
230
230
 
231
231
  def test_templates_resource_including_delete_returns_none():
@@ -247,7 +247,7 @@ def test_templates_resource_including_delete_returns_none():
247
247
  finally:
248
248
  http.close()
249
249
  assert removed is None
250
- assert calls[3].method == "PATCH" and calls[3].url.path == "/templates/t1"
250
+ assert calls[3].method == "PATCH" and calls[3].url.path == "/api/v1/templates/t1"
251
251
  assert calls[4].method == "DELETE"
252
252
 
253
253
 
@@ -268,8 +268,8 @@ def test_webhooks_resource_event_types_alias():
268
268
  finally:
269
269
  http.close()
270
270
  assert json.loads(calls[0].content) == {"url": "https://e.com", "event_types": ["email.sent"]}
271
- assert calls[0].url.path == "/webhook-endpoints"
272
- assert calls[3].method == "DELETE" and calls[3].url.path == "/webhook-endpoints/wh"
271
+ assert calls[0].url.path == "/api/v1/webhook-endpoints"
272
+ assert calls[3].method == "DELETE" and calls[3].url.path == "/api/v1/webhook-endpoints/wh"
273
273
 
274
274
 
275
275
  def test_webhook_events_retry_alias_attribute():
@@ -280,7 +280,7 @@ def test_webhook_events_retry_alias_attribute():
280
280
  finally:
281
281
  http.close()
282
282
  assert result["webhook_status"] == "queued"
283
- assert calls[0].method == "POST" and calls[0].url.path == "/events/ev_1/retry"
283
+ assert calls[0].method == "POST" and calls[0].url.path == "/api/v1/events/ev_1/retry"
284
284
 
285
285
 
286
286
  def test_suppressions_resource():
@@ -297,8 +297,8 @@ def test_suppressions_resource():
297
297
  client.suppressions.remove("bad@x.com")
298
298
  finally:
299
299
  http.close()
300
- assert calls[0].url.path == "/suppressions"
301
- assert calls[2].method == "DELETE" and calls[2].url.path == "/suppressions/bad@x.com"
300
+ assert calls[0].url.path == "/api/v1/suppressions"
301
+ assert calls[2].method == "DELETE" and calls[2].url.path == "/api/v1/suppressions/bad@x.com"
302
302
 
303
303
 
304
304
  def test_attachments_upload_alias():
@@ -646,7 +646,7 @@ def test_close_owns_vs_injected_and_context_manager():
646
646
  def test_default_base_url_when_unspecified():
647
647
  client = Sendstack("tok")
648
648
  try:
649
- assert client.base_url == "https://mailer.norialabs.com"
649
+ assert client.base_url == "https://sendstack.norialabs.com/api/v1"
650
650
  finally:
651
651
  client.close()
652
652
 
File without changes
File without changes