poststack 0.4.0__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.
- poststack/__init__.py +159 -0
- poststack/_client.py +333 -0
- poststack/_errors.py +33 -0
- poststack/resources/__init__.py +59 -0
- poststack/resources/api_keys.py +63 -0
- poststack/resources/broadcasts.py +160 -0
- poststack/resources/contact_properties.py +73 -0
- poststack/resources/contacts.py +130 -0
- poststack/resources/domains.py +199 -0
- poststack/resources/email_validations.py +53 -0
- poststack/resources/emails.py +124 -0
- poststack/resources/mailboxes.py +157 -0
- poststack/resources/segments.py +156 -0
- poststack/resources/signup_forms.py +124 -0
- poststack/resources/subscription_topics.py +119 -0
- poststack/resources/suppressions.py +60 -0
- poststack/resources/templates.py +160 -0
- poststack/resources/webhooks.py +139 -0
- poststack/resources/workflows.py +192 -0
- poststack/types.py +511 -0
- poststack-0.4.0.dist-info/METADATA +155 -0
- poststack-0.4.0.dist-info/RECORD +24 -0
- poststack-0.4.0.dist-info/WHEEL +4 -0
- poststack-0.4.0.dist-info/licenses/LICENSE +21 -0
poststack/__init__.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""PostStack Python SDK.
|
|
2
|
+
|
|
3
|
+
Public API::
|
|
4
|
+
|
|
5
|
+
from poststack import PostStack, AsyncPostStack, PostStackError
|
|
6
|
+
|
|
7
|
+
client = PostStack(api_key="sk_live_...")
|
|
8
|
+
client.emails.send({"from": "a@b.com", "to": ["c@d.com"], "subject": "hi"})
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from ._client import (
|
|
16
|
+
DEFAULT_BASE_URL,
|
|
17
|
+
AsyncPostStackClient,
|
|
18
|
+
PostStackClient,
|
|
19
|
+
)
|
|
20
|
+
from ._errors import APIError, PostStackError
|
|
21
|
+
from .resources import (
|
|
22
|
+
ApiKeysResource,
|
|
23
|
+
AsyncApiKeysResource,
|
|
24
|
+
AsyncBroadcastsResource,
|
|
25
|
+
AsyncContactPropertiesResource,
|
|
26
|
+
AsyncContactsResource,
|
|
27
|
+
AsyncDomainsResource,
|
|
28
|
+
AsyncEmailsResource,
|
|
29
|
+
AsyncEmailValidationsResource,
|
|
30
|
+
AsyncMailboxesResource,
|
|
31
|
+
AsyncSegmentsResource,
|
|
32
|
+
AsyncSignupFormsResource,
|
|
33
|
+
AsyncSubscriptionTopicsResource,
|
|
34
|
+
AsyncSuppressionsResource,
|
|
35
|
+
AsyncTemplatesResource,
|
|
36
|
+
AsyncWebhooksResource,
|
|
37
|
+
AsyncWorkflowsResource,
|
|
38
|
+
BroadcastsResource,
|
|
39
|
+
ContactPropertiesResource,
|
|
40
|
+
ContactsResource,
|
|
41
|
+
DomainsResource,
|
|
42
|
+
EmailsResource,
|
|
43
|
+
EmailValidationsResource,
|
|
44
|
+
MailboxesResource,
|
|
45
|
+
SegmentsResource,
|
|
46
|
+
SignupFormsResource,
|
|
47
|
+
SubscriptionTopicsResource,
|
|
48
|
+
SuppressionsResource,
|
|
49
|
+
TemplatesResource,
|
|
50
|
+
WebhooksResource,
|
|
51
|
+
WorkflowsResource,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__version__ = "0.4.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"PostStack",
|
|
58
|
+
"AsyncPostStack",
|
|
59
|
+
"PostStackError",
|
|
60
|
+
"APIError",
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PostStack:
|
|
66
|
+
"""Synchronous PostStack client.
|
|
67
|
+
|
|
68
|
+
Example::
|
|
69
|
+
|
|
70
|
+
client = PostStack(api_key="sk_live_...")
|
|
71
|
+
client.emails.send({
|
|
72
|
+
"from": "hello@example.com",
|
|
73
|
+
"to": ["user@example.com"],
|
|
74
|
+
"subject": "Hello",
|
|
75
|
+
"html": "<p>Hi</p>",
|
|
76
|
+
})
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
api_key: str,
|
|
82
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
83
|
+
timeout: float = 30.0,
|
|
84
|
+
) -> None:
|
|
85
|
+
self._client = PostStackClient(
|
|
86
|
+
api_key=api_key, base_url=base_url, timeout=timeout
|
|
87
|
+
)
|
|
88
|
+
self.emails = EmailsResource(self._client)
|
|
89
|
+
self.domains = DomainsResource(self._client)
|
|
90
|
+
self.contacts = ContactsResource(self._client)
|
|
91
|
+
self.contact_properties = ContactPropertiesResource(self._client)
|
|
92
|
+
self.segments = SegmentsResource(self._client)
|
|
93
|
+
self.templates = TemplatesResource(self._client)
|
|
94
|
+
self.webhooks = WebhooksResource(self._client)
|
|
95
|
+
self.broadcasts = BroadcastsResource(self._client)
|
|
96
|
+
self.suppressions = SuppressionsResource(self._client)
|
|
97
|
+
self.api_keys = ApiKeysResource(self._client)
|
|
98
|
+
self.mailboxes = MailboxesResource(self._client)
|
|
99
|
+
self.subscription_topics = SubscriptionTopicsResource(self._client)
|
|
100
|
+
self.workflows = WorkflowsResource(self._client)
|
|
101
|
+
self.signup_forms = SignupFormsResource(self._client)
|
|
102
|
+
self.email_validations = EmailValidationsResource(self._client)
|
|
103
|
+
|
|
104
|
+
def close(self) -> None:
|
|
105
|
+
self._client.close()
|
|
106
|
+
|
|
107
|
+
def __enter__(self) -> "PostStack":
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
def __exit__(self, *args: object) -> None:
|
|
111
|
+
self.close()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AsyncPostStack:
|
|
115
|
+
"""Asynchronous PostStack client.
|
|
116
|
+
|
|
117
|
+
Example::
|
|
118
|
+
|
|
119
|
+
async with AsyncPostStack(api_key="sk_live_...") as client:
|
|
120
|
+
await client.emails.send({...})
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
api_key: str,
|
|
126
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
127
|
+
timeout: float = 30.0,
|
|
128
|
+
) -> None:
|
|
129
|
+
self._client = AsyncPostStackClient(
|
|
130
|
+
api_key=api_key, base_url=base_url, timeout=timeout
|
|
131
|
+
)
|
|
132
|
+
self.emails = AsyncEmailsResource(self._client)
|
|
133
|
+
self.domains = AsyncDomainsResource(self._client)
|
|
134
|
+
self.contacts = AsyncContactsResource(self._client)
|
|
135
|
+
self.contact_properties = AsyncContactPropertiesResource(self._client)
|
|
136
|
+
self.segments = AsyncSegmentsResource(self._client)
|
|
137
|
+
self.templates = AsyncTemplatesResource(self._client)
|
|
138
|
+
self.webhooks = AsyncWebhooksResource(self._client)
|
|
139
|
+
self.broadcasts = AsyncBroadcastsResource(self._client)
|
|
140
|
+
self.suppressions = AsyncSuppressionsResource(self._client)
|
|
141
|
+
self.api_keys = AsyncApiKeysResource(self._client)
|
|
142
|
+
self.mailboxes = AsyncMailboxesResource(self._client)
|
|
143
|
+
self.subscription_topics = AsyncSubscriptionTopicsResource(self._client)
|
|
144
|
+
self.workflows = AsyncWorkflowsResource(self._client)
|
|
145
|
+
self.signup_forms = AsyncSignupFormsResource(self._client)
|
|
146
|
+
self.email_validations = AsyncEmailValidationsResource(self._client)
|
|
147
|
+
|
|
148
|
+
async def aclose(self) -> None:
|
|
149
|
+
await self._client.aclose()
|
|
150
|
+
|
|
151
|
+
async def __aenter__(self) -> "AsyncPostStack":
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
async def __aexit__(self, *args: object) -> None:
|
|
155
|
+
await self.aclose()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Re-export for advanced callers.
|
|
159
|
+
_: Optional[type] = None # noqa: E305 - keeps ruff/mypy happy with Optional import
|
poststack/_client.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""HTTP client used by every resource module.
|
|
2
|
+
|
|
3
|
+
Implements two parallel clients (sync + async) with identical interfaces so
|
|
4
|
+
resources can pick the transport they need. Auth is a simple Bearer token.
|
|
5
|
+
|
|
6
|
+
Both clients apply:
|
|
7
|
+
- a per-attempt timeout (default 30s, override per call)
|
|
8
|
+
- exponential-backoff retries on transient failures (network errors, 408,
|
|
9
|
+
429, 5xx) up to ``max_retries`` (default 3)
|
|
10
|
+
- automatic ``Idempotency-Key`` injection on POSTs so that a retry after a
|
|
11
|
+
network blip does not create a duplicate resource
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import random
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from typing import Any, Mapping, Optional
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from ._errors import PostStackError
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://api.poststack.dev"
|
|
27
|
+
DEFAULT_TIMEOUT = 30.0
|
|
28
|
+
DEFAULT_MAX_RETRIES = 3
|
|
29
|
+
BASE_RETRY_DELAY = 0.25
|
|
30
|
+
SDK_VERSION = "0.5.0"
|
|
31
|
+
USER_AGENT = f"PostStack-Python-SDK/{SDK_VERSION}"
|
|
32
|
+
|
|
33
|
+
RETRYABLE_STATUSES = {408, 429, 500, 502, 503, 504}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_headers(api_key: str) -> dict[str, str]:
|
|
37
|
+
return {
|
|
38
|
+
"Authorization": f"Bearer {api_key}",
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
"User-Agent": USER_AGENT,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _clean_params(
|
|
45
|
+
params: Optional[Mapping[str, Any]],
|
|
46
|
+
) -> Optional[dict[str, str]]:
|
|
47
|
+
if not params:
|
|
48
|
+
return None
|
|
49
|
+
out: dict[str, str] = {}
|
|
50
|
+
for key, value in params.items():
|
|
51
|
+
if value is None:
|
|
52
|
+
continue
|
|
53
|
+
out[key] = str(value)
|
|
54
|
+
return out or None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
58
|
+
if response.is_success:
|
|
59
|
+
return
|
|
60
|
+
request_id = response.headers.get("x-request-id")
|
|
61
|
+
try:
|
|
62
|
+
body = response.json()
|
|
63
|
+
except ValueError:
|
|
64
|
+
body = {}
|
|
65
|
+
error_message = (
|
|
66
|
+
body.get("error") if isinstance(body, dict) else None
|
|
67
|
+
) or response.reason_phrase or "Unknown error"
|
|
68
|
+
code = body.get("code") if isinstance(body, dict) else None
|
|
69
|
+
raise PostStackError(
|
|
70
|
+
status_code=response.status_code,
|
|
71
|
+
error=error_message,
|
|
72
|
+
code=code,
|
|
73
|
+
request_id=request_id,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_body(response: httpx.Response) -> Any:
|
|
78
|
+
# Some endpoints (e.g. CSV export) return non-JSON. Fall back to text.
|
|
79
|
+
content_type = response.headers.get("content-type", "")
|
|
80
|
+
if "application/json" in content_type:
|
|
81
|
+
return response.json()
|
|
82
|
+
text = response.text
|
|
83
|
+
if not text:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return response.json()
|
|
87
|
+
except ValueError:
|
|
88
|
+
return text
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _retry_delay(attempt: int) -> float:
|
|
92
|
+
"""Full-jitter exponential backoff — pick uniformly in [0, base*2**attempt)."""
|
|
93
|
+
return random.random() * (BASE_RETRY_DELAY * (2**attempt))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _idempotency_headers() -> dict[str, str]:
|
|
97
|
+
return {"Idempotency-Key": str(uuid.uuid4())}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PostStackClient:
|
|
101
|
+
"""Synchronous HTTP client."""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
api_key: str,
|
|
106
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
107
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
108
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
109
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
self._base_url = base_url.rstrip("/")
|
|
112
|
+
self._timeout = timeout
|
|
113
|
+
self._max_retries = max_retries
|
|
114
|
+
self._httpx = httpx.Client(
|
|
115
|
+
base_url=self._base_url,
|
|
116
|
+
headers=_build_headers(api_key),
|
|
117
|
+
timeout=timeout,
|
|
118
|
+
transport=transport,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Lifecycle ------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def close(self) -> None:
|
|
124
|
+
self._httpx.close()
|
|
125
|
+
|
|
126
|
+
def __enter__(self) -> "PostStackClient":
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
def __exit__(self, *args: Any) -> None:
|
|
130
|
+
self.close()
|
|
131
|
+
|
|
132
|
+
# HTTP methods ---------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def _request(
|
|
135
|
+
self,
|
|
136
|
+
method: str,
|
|
137
|
+
path: str,
|
|
138
|
+
*,
|
|
139
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
140
|
+
body: Any = None,
|
|
141
|
+
extra_headers: Optional[Mapping[str, str]] = None,
|
|
142
|
+
timeout: Optional[float] = None,
|
|
143
|
+
) -> Any:
|
|
144
|
+
request_timeout = timeout if timeout is not None else self._timeout
|
|
145
|
+
|
|
146
|
+
last_exc: Optional[BaseException] = None
|
|
147
|
+
for attempt in range(self._max_retries + 1):
|
|
148
|
+
try:
|
|
149
|
+
response = self._httpx.request(
|
|
150
|
+
method,
|
|
151
|
+
path,
|
|
152
|
+
params=_clean_params(params),
|
|
153
|
+
json=body if body is not None else None,
|
|
154
|
+
headers=dict(extra_headers) if extra_headers else None,
|
|
155
|
+
timeout=request_timeout,
|
|
156
|
+
)
|
|
157
|
+
except httpx.TransportError as exc:
|
|
158
|
+
last_exc = exc
|
|
159
|
+
if attempt < self._max_retries:
|
|
160
|
+
time.sleep(_retry_delay(attempt))
|
|
161
|
+
continue
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
response.status_code in RETRYABLE_STATUSES
|
|
166
|
+
and attempt < self._max_retries
|
|
167
|
+
):
|
|
168
|
+
time.sleep(_retry_delay(attempt))
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
_raise_for_status(response)
|
|
172
|
+
return _parse_body(response)
|
|
173
|
+
|
|
174
|
+
# If we exit the loop via `continue` on the final attempt, last_exc
|
|
175
|
+
# holds the most recent error.
|
|
176
|
+
assert last_exc is not None # noqa: S101
|
|
177
|
+
raise last_exc
|
|
178
|
+
|
|
179
|
+
def get(
|
|
180
|
+
self,
|
|
181
|
+
path: str,
|
|
182
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
183
|
+
*,
|
|
184
|
+
timeout: Optional[float] = None,
|
|
185
|
+
) -> Any:
|
|
186
|
+
return self._request("GET", path, params=params, timeout=timeout)
|
|
187
|
+
|
|
188
|
+
def post(
|
|
189
|
+
self,
|
|
190
|
+
path: str,
|
|
191
|
+
body: Any = None,
|
|
192
|
+
*,
|
|
193
|
+
timeout: Optional[float] = None,
|
|
194
|
+
) -> Any:
|
|
195
|
+
return self._request(
|
|
196
|
+
"POST",
|
|
197
|
+
path,
|
|
198
|
+
body=body,
|
|
199
|
+
extra_headers=_idempotency_headers(),
|
|
200
|
+
timeout=timeout,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def patch(
|
|
204
|
+
self,
|
|
205
|
+
path: str,
|
|
206
|
+
body: Any,
|
|
207
|
+
*,
|
|
208
|
+
timeout: Optional[float] = None,
|
|
209
|
+
) -> Any:
|
|
210
|
+
return self._request("PATCH", path, body=body, timeout=timeout)
|
|
211
|
+
|
|
212
|
+
def delete(
|
|
213
|
+
self,
|
|
214
|
+
path: str,
|
|
215
|
+
*,
|
|
216
|
+
timeout: Optional[float] = None,
|
|
217
|
+
) -> Any:
|
|
218
|
+
return self._request("DELETE", path, timeout=timeout)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AsyncPostStackClient:
|
|
222
|
+
"""Asynchronous HTTP client."""
|
|
223
|
+
|
|
224
|
+
def __init__(
|
|
225
|
+
self,
|
|
226
|
+
api_key: str,
|
|
227
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
228
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
229
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
230
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
self._base_url = base_url.rstrip("/")
|
|
233
|
+
self._timeout = timeout
|
|
234
|
+
self._max_retries = max_retries
|
|
235
|
+
self._httpx = httpx.AsyncClient(
|
|
236
|
+
base_url=self._base_url,
|
|
237
|
+
headers=_build_headers(api_key),
|
|
238
|
+
timeout=timeout,
|
|
239
|
+
transport=transport,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def aclose(self) -> None:
|
|
243
|
+
await self._httpx.aclose()
|
|
244
|
+
|
|
245
|
+
async def __aenter__(self) -> "AsyncPostStackClient":
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
249
|
+
await self.aclose()
|
|
250
|
+
|
|
251
|
+
async def _request(
|
|
252
|
+
self,
|
|
253
|
+
method: str,
|
|
254
|
+
path: str,
|
|
255
|
+
*,
|
|
256
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
257
|
+
body: Any = None,
|
|
258
|
+
extra_headers: Optional[Mapping[str, str]] = None,
|
|
259
|
+
timeout: Optional[float] = None,
|
|
260
|
+
) -> Any:
|
|
261
|
+
request_timeout = timeout if timeout is not None else self._timeout
|
|
262
|
+
|
|
263
|
+
last_exc: Optional[BaseException] = None
|
|
264
|
+
for attempt in range(self._max_retries + 1):
|
|
265
|
+
try:
|
|
266
|
+
response = await self._httpx.request(
|
|
267
|
+
method,
|
|
268
|
+
path,
|
|
269
|
+
params=_clean_params(params),
|
|
270
|
+
json=body if body is not None else None,
|
|
271
|
+
headers=dict(extra_headers) if extra_headers else None,
|
|
272
|
+
timeout=request_timeout,
|
|
273
|
+
)
|
|
274
|
+
except httpx.TransportError as exc:
|
|
275
|
+
last_exc = exc
|
|
276
|
+
if attempt < self._max_retries:
|
|
277
|
+
await asyncio.sleep(_retry_delay(attempt))
|
|
278
|
+
continue
|
|
279
|
+
raise
|
|
280
|
+
|
|
281
|
+
if (
|
|
282
|
+
response.status_code in RETRYABLE_STATUSES
|
|
283
|
+
and attempt < self._max_retries
|
|
284
|
+
):
|
|
285
|
+
await asyncio.sleep(_retry_delay(attempt))
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
_raise_for_status(response)
|
|
289
|
+
return _parse_body(response)
|
|
290
|
+
|
|
291
|
+
assert last_exc is not None # noqa: S101
|
|
292
|
+
raise last_exc
|
|
293
|
+
|
|
294
|
+
async def get(
|
|
295
|
+
self,
|
|
296
|
+
path: str,
|
|
297
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
298
|
+
*,
|
|
299
|
+
timeout: Optional[float] = None,
|
|
300
|
+
) -> Any:
|
|
301
|
+
return await self._request("GET", path, params=params, timeout=timeout)
|
|
302
|
+
|
|
303
|
+
async def post(
|
|
304
|
+
self,
|
|
305
|
+
path: str,
|
|
306
|
+
body: Any = None,
|
|
307
|
+
*,
|
|
308
|
+
timeout: Optional[float] = None,
|
|
309
|
+
) -> Any:
|
|
310
|
+
return await self._request(
|
|
311
|
+
"POST",
|
|
312
|
+
path,
|
|
313
|
+
body=body,
|
|
314
|
+
extra_headers=_idempotency_headers(),
|
|
315
|
+
timeout=timeout,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def patch(
|
|
319
|
+
self,
|
|
320
|
+
path: str,
|
|
321
|
+
body: Any,
|
|
322
|
+
*,
|
|
323
|
+
timeout: Optional[float] = None,
|
|
324
|
+
) -> Any:
|
|
325
|
+
return await self._request("PATCH", path, body=body, timeout=timeout)
|
|
326
|
+
|
|
327
|
+
async def delete(
|
|
328
|
+
self,
|
|
329
|
+
path: str,
|
|
330
|
+
*,
|
|
331
|
+
timeout: Optional[float] = None,
|
|
332
|
+
) -> Any:
|
|
333
|
+
return await self._request("DELETE", path, timeout=timeout)
|
poststack/_errors.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Errors raised by the PostStack SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PostStackError(Exception):
|
|
9
|
+
"""Raised when the PostStack API returns a non-2xx response."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
status_code: int,
|
|
14
|
+
error: str,
|
|
15
|
+
code: Optional[str] = None,
|
|
16
|
+
request_id: Optional[str] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(error)
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.error = error
|
|
21
|
+
self.code = code
|
|
22
|
+
self.request_id = request_id
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str: # pragma: no cover - cosmetic
|
|
25
|
+
return (
|
|
26
|
+
f"PostStackError(status_code={self.status_code!r}, "
|
|
27
|
+
f"error={self.error!r}, code={self.code!r}, "
|
|
28
|
+
f"request_id={self.request_id!r})"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Alias retained for callers expecting a generic APIError type.
|
|
33
|
+
APIError = PostStackError
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Resource classes mirroring the PostStack TypeScript SDK."""
|
|
2
|
+
|
|
3
|
+
from .api_keys import ApiKeysResource, AsyncApiKeysResource
|
|
4
|
+
from .broadcasts import AsyncBroadcastsResource, BroadcastsResource
|
|
5
|
+
from .contact_properties import (
|
|
6
|
+
AsyncContactPropertiesResource,
|
|
7
|
+
ContactPropertiesResource,
|
|
8
|
+
)
|
|
9
|
+
from .contacts import AsyncContactsResource, ContactsResource
|
|
10
|
+
from .domains import AsyncDomainsResource, DomainsResource
|
|
11
|
+
from .email_validations import (
|
|
12
|
+
AsyncEmailValidationsResource,
|
|
13
|
+
EmailValidationsResource,
|
|
14
|
+
)
|
|
15
|
+
from .emails import AsyncEmailsResource, EmailsResource
|
|
16
|
+
from .mailboxes import AsyncMailboxesResource, MailboxesResource
|
|
17
|
+
from .segments import AsyncSegmentsResource, SegmentsResource
|
|
18
|
+
from .signup_forms import AsyncSignupFormsResource, SignupFormsResource
|
|
19
|
+
from .subscription_topics import (
|
|
20
|
+
AsyncSubscriptionTopicsResource,
|
|
21
|
+
SubscriptionTopicsResource,
|
|
22
|
+
)
|
|
23
|
+
from .suppressions import AsyncSuppressionsResource, SuppressionsResource
|
|
24
|
+
from .templates import AsyncTemplatesResource, TemplatesResource
|
|
25
|
+
from .webhooks import AsyncWebhooksResource, WebhooksResource
|
|
26
|
+
from .workflows import AsyncWorkflowsResource, WorkflowsResource
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ApiKeysResource",
|
|
30
|
+
"AsyncApiKeysResource",
|
|
31
|
+
"BroadcastsResource",
|
|
32
|
+
"AsyncBroadcastsResource",
|
|
33
|
+
"ContactPropertiesResource",
|
|
34
|
+
"AsyncContactPropertiesResource",
|
|
35
|
+
"ContactsResource",
|
|
36
|
+
"AsyncContactsResource",
|
|
37
|
+
"DomainsResource",
|
|
38
|
+
"AsyncDomainsResource",
|
|
39
|
+
"EmailValidationsResource",
|
|
40
|
+
"AsyncEmailValidationsResource",
|
|
41
|
+
"EmailsResource",
|
|
42
|
+
"AsyncEmailsResource",
|
|
43
|
+
"MailboxesResource",
|
|
44
|
+
"AsyncMailboxesResource",
|
|
45
|
+
"SegmentsResource",
|
|
46
|
+
"AsyncSegmentsResource",
|
|
47
|
+
"SignupFormsResource",
|
|
48
|
+
"AsyncSignupFormsResource",
|
|
49
|
+
"SubscriptionTopicsResource",
|
|
50
|
+
"AsyncSubscriptionTopicsResource",
|
|
51
|
+
"SuppressionsResource",
|
|
52
|
+
"AsyncSuppressionsResource",
|
|
53
|
+
"TemplatesResource",
|
|
54
|
+
"AsyncTemplatesResource",
|
|
55
|
+
"WebhooksResource",
|
|
56
|
+
"AsyncWebhooksResource",
|
|
57
|
+
"WorkflowsResource",
|
|
58
|
+
"AsyncWorkflowsResource",
|
|
59
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""API keys resource — mirrors packages/sdk/src/resources/api-keys.ts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from .._client import AsyncPostStackClient, PostStackClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiKeysResource:
|
|
11
|
+
def __init__(self, client: PostStackClient) -> None:
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def create(
|
|
15
|
+
self, input: Dict[str, Any], *, timeout: Optional[float] = None
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
return self._client.post("/api-keys", input, timeout=timeout)
|
|
18
|
+
|
|
19
|
+
def list(
|
|
20
|
+
self,
|
|
21
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
22
|
+
*,
|
|
23
|
+
timeout: Optional[float] = None,
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
return self._client.get("/api-keys", params, timeout=timeout)
|
|
26
|
+
|
|
27
|
+
def get(
|
|
28
|
+
self, id: int, *, timeout: Optional[float] = None
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
return self._client.get(f"/api-keys/{id}", timeout=timeout)
|
|
31
|
+
|
|
32
|
+
def revoke(
|
|
33
|
+
self, id: int, *, timeout: Optional[float] = None
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
return self._client.delete(f"/api-keys/{id}", timeout=timeout)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncApiKeysResource:
|
|
39
|
+
def __init__(self, client: AsyncPostStackClient) -> None:
|
|
40
|
+
self._client = client
|
|
41
|
+
|
|
42
|
+
async def create(
|
|
43
|
+
self, input: Dict[str, Any], *, timeout: Optional[float] = None
|
|
44
|
+
) -> Dict[str, Any]:
|
|
45
|
+
return await self._client.post("/api-keys", input, timeout=timeout)
|
|
46
|
+
|
|
47
|
+
async def list(
|
|
48
|
+
self,
|
|
49
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
50
|
+
*,
|
|
51
|
+
timeout: Optional[float] = None,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
return await self._client.get("/api-keys", params, timeout=timeout)
|
|
54
|
+
|
|
55
|
+
async def get(
|
|
56
|
+
self, id: int, *, timeout: Optional[float] = None
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
return await self._client.get(f"/api-keys/{id}", timeout=timeout)
|
|
59
|
+
|
|
60
|
+
async def revoke(
|
|
61
|
+
self, id: int, *, timeout: Optional[float] = None
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
return await self._client.delete(f"/api-keys/{id}", timeout=timeout)
|