mailrith-sdk 0.1.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.
- mailrith_sdk/__init__.py +3 -0
- mailrith_sdk/client.py +273 -0
- mailrith_sdk/contract.json +3 -0
- mailrith_sdk/manifest.json +3932 -0
- mailrith_sdk/py.typed +1 -0
- mailrith_sdk-0.1.0.dist-info/METADATA +35 -0
- mailrith_sdk-0.1.0.dist-info/RECORD +10 -0
- mailrith_sdk-0.1.0.dist-info/WHEEL +5 -0
- mailrith_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- mailrith_sdk-0.1.0.dist-info/top_level.txt +1 -0
mailrith_sdk/__init__.py
ADDED
mailrith_sdk/client.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from typing import Any, Callable, Mapping
|
|
8
|
+
from urllib import error, parse, request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TransportResponse = tuple[int, Mapping[str, str], Any]
|
|
12
|
+
Transport = Callable[[str, str, Mapping[str, str], str | None], TransportResponse]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_manifest() -> list[dict[str, Any]]:
|
|
16
|
+
with resources.files("mailrith_sdk").joinpath("manifest.json").open(
|
|
17
|
+
"r", encoding="utf-8"
|
|
18
|
+
) as handle:
|
|
19
|
+
return json.load(handle)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_SDK_MANIFEST = _load_manifest()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MailrithApiError(RuntimeError):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
status: int,
|
|
30
|
+
message: str,
|
|
31
|
+
error_type: str | None = None,
|
|
32
|
+
code: str | None = None,
|
|
33
|
+
response_body: Any = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status = status
|
|
37
|
+
self.type = error_type
|
|
38
|
+
self.code = code
|
|
39
|
+
self.response_body = response_body
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class _Operation:
|
|
44
|
+
namespace: str
|
|
45
|
+
method_name: str
|
|
46
|
+
operation_id: str
|
|
47
|
+
method: str
|
|
48
|
+
path: str
|
|
49
|
+
auth_required: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _default_transport(
|
|
53
|
+
method: str,
|
|
54
|
+
url: str,
|
|
55
|
+
headers: Mapping[str, str],
|
|
56
|
+
body: str | None,
|
|
57
|
+
) -> TransportResponse:
|
|
58
|
+
http_request = request.Request(
|
|
59
|
+
url=url,
|
|
60
|
+
method=method,
|
|
61
|
+
headers=dict(headers),
|
|
62
|
+
data=body.encode("utf-8") if body is not None else None,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with request.urlopen(http_request) as response:
|
|
67
|
+
payload = response.read().decode("utf-8")
|
|
68
|
+
content_type = response.headers.get("Content-Type", "")
|
|
69
|
+
parsed = json.loads(payload) if payload and "application/json" in content_type else payload or None
|
|
70
|
+
return response.status, dict(response.headers.items()), parsed
|
|
71
|
+
except error.HTTPError as exc:
|
|
72
|
+
payload = exc.read().decode("utf-8")
|
|
73
|
+
content_type = exc.headers.get("Content-Type", "")
|
|
74
|
+
parsed = json.loads(payload) if payload and "application/json" in content_type else payload or None
|
|
75
|
+
return exc.code, dict(exc.headers.items()), parsed
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _normalize_base_url(value: str | None) -> str:
|
|
79
|
+
return (value or "https://api.mailrith.com").rstrip("/")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _encode_path(pathname: str, path_params: Mapping[str, str | int] | None) -> str:
|
|
83
|
+
encoded = pathname
|
|
84
|
+
for segment in _extract_path_params(pathname):
|
|
85
|
+
if path_params is None or segment not in path_params:
|
|
86
|
+
raise ValueError(f"Missing required path parameter: {segment}.")
|
|
87
|
+
encoded = encoded.replace(
|
|
88
|
+
"{" + segment + "}",
|
|
89
|
+
parse.quote(str(path_params[segment]), safe=""),
|
|
90
|
+
)
|
|
91
|
+
return encoded
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _extract_path_params(pathname: str) -> list[str]:
|
|
95
|
+
parts: list[str] = []
|
|
96
|
+
cursor = 0
|
|
97
|
+
while True:
|
|
98
|
+
start = pathname.find("{", cursor)
|
|
99
|
+
if start < 0:
|
|
100
|
+
return parts
|
|
101
|
+
end = pathname.find("}", start)
|
|
102
|
+
if end < 0:
|
|
103
|
+
return parts
|
|
104
|
+
parts.append(pathname[start + 1 : end])
|
|
105
|
+
cursor = end + 1
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _append_query(url: str, query: Mapping[str, Any] | None) -> str:
|
|
109
|
+
if not query:
|
|
110
|
+
return url
|
|
111
|
+
|
|
112
|
+
items: list[tuple[str, str]] = []
|
|
113
|
+
for key, value in query.items():
|
|
114
|
+
if value is None:
|
|
115
|
+
continue
|
|
116
|
+
if isinstance(value, list):
|
|
117
|
+
for item in value:
|
|
118
|
+
if item is not None:
|
|
119
|
+
items.append((key, _stringify_query_value(item)))
|
|
120
|
+
continue
|
|
121
|
+
items.append((key, _stringify_query_value(value)))
|
|
122
|
+
|
|
123
|
+
if not items:
|
|
124
|
+
return url
|
|
125
|
+
separator = "&" if "?" in url else "?"
|
|
126
|
+
return f"{url}{separator}{parse.urlencode(items)}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _stringify_query_value(value: Any) -> str:
|
|
130
|
+
if isinstance(value, bool):
|
|
131
|
+
return "true" if value else "false"
|
|
132
|
+
return str(value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _ResourceNamespace:
|
|
136
|
+
def __init__(self, client: "MailrithClient", resource: dict[str, Any]) -> None:
|
|
137
|
+
self._client = client
|
|
138
|
+
self._operations = {
|
|
139
|
+
operation["method_name"]: _Operation(
|
|
140
|
+
namespace=resource["namespace"],
|
|
141
|
+
method_name=operation["method_name"],
|
|
142
|
+
operation_id=operation["operation_id"],
|
|
143
|
+
method=operation["method"],
|
|
144
|
+
path=operation["path"],
|
|
145
|
+
auth_required=bool(operation["auth_required"]),
|
|
146
|
+
)
|
|
147
|
+
for operation in resource["operations"]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def __getattr__(self, method_name: str):
|
|
151
|
+
operation = self._operations.get(method_name)
|
|
152
|
+
if operation is None:
|
|
153
|
+
raise AttributeError(method_name)
|
|
154
|
+
|
|
155
|
+
def invoke(
|
|
156
|
+
*,
|
|
157
|
+
path: Mapping[str, str | int] | None = None,
|
|
158
|
+
query: Mapping[str, Any] | None = None,
|
|
159
|
+
body: Any = None,
|
|
160
|
+
headers: Mapping[str, str] | None = None,
|
|
161
|
+
idempotency_key: str | None = None,
|
|
162
|
+
api_key: str | None = None,
|
|
163
|
+
) -> Any:
|
|
164
|
+
return self._client.request(
|
|
165
|
+
operation,
|
|
166
|
+
path=path,
|
|
167
|
+
query=query,
|
|
168
|
+
body=body,
|
|
169
|
+
headers=headers,
|
|
170
|
+
idempotency_key=idempotency_key,
|
|
171
|
+
api_key=api_key,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return invoke
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class MailrithClient:
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
base_url: str | None = None,
|
|
182
|
+
api_key: str | None = None,
|
|
183
|
+
transport: Transport | None = None,
|
|
184
|
+
default_headers: Mapping[str, str] | None = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
self.base_url = _normalize_base_url(base_url)
|
|
187
|
+
self.api_key = api_key
|
|
188
|
+
self.transport = transport or _default_transport
|
|
189
|
+
self.default_headers = dict(default_headers or {})
|
|
190
|
+
self.operations = _SDK_MANIFEST
|
|
191
|
+
|
|
192
|
+
for resource in _SDK_MANIFEST:
|
|
193
|
+
setattr(self, resource["namespace"], _ResourceNamespace(self, resource))
|
|
194
|
+
|
|
195
|
+
def with_api_key(self, api_key: str) -> "MailrithClient":
|
|
196
|
+
return MailrithClient(
|
|
197
|
+
base_url=self.base_url,
|
|
198
|
+
api_key=api_key,
|
|
199
|
+
transport=self.transport,
|
|
200
|
+
default_headers=self.default_headers,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_operation(self, namespace: str, method_name: str) -> dict[str, Any] | None:
|
|
204
|
+
for resource in _SDK_MANIFEST:
|
|
205
|
+
if resource["namespace"] != namespace:
|
|
206
|
+
continue
|
|
207
|
+
for operation in resource["operations"]:
|
|
208
|
+
if operation["method_name"] == method_name:
|
|
209
|
+
return operation
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def request(
|
|
213
|
+
self,
|
|
214
|
+
operation: _Operation,
|
|
215
|
+
*,
|
|
216
|
+
path: Mapping[str, str | int] | None = None,
|
|
217
|
+
query: Mapping[str, Any] | None = None,
|
|
218
|
+
body: Any = None,
|
|
219
|
+
headers: Mapping[str, str] | None = None,
|
|
220
|
+
idempotency_key: str | None = None,
|
|
221
|
+
api_key: str | None = None,
|
|
222
|
+
) -> Any:
|
|
223
|
+
resolved_api_key = api_key or self.api_key
|
|
224
|
+
if operation.auth_required and not resolved_api_key:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Mailrith operation {operation.operation_id} requires a bearer credential."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
url = self.base_url + _encode_path(operation.path, path)
|
|
230
|
+
url = _append_query(url, query)
|
|
231
|
+
|
|
232
|
+
request_headers: dict[str, str] = {
|
|
233
|
+
"accept": "application/json",
|
|
234
|
+
"x-mailrith-client": "python_sdk/dev",
|
|
235
|
+
"x-mailrith-request-id": f"req_{uuid.uuid4()}",
|
|
236
|
+
**self.default_headers,
|
|
237
|
+
**dict(headers or {}),
|
|
238
|
+
}
|
|
239
|
+
if resolved_api_key:
|
|
240
|
+
request_headers["authorization"] = f"Bearer {resolved_api_key}"
|
|
241
|
+
if idempotency_key:
|
|
242
|
+
request_headers["idempotency-key"] = idempotency_key
|
|
243
|
+
|
|
244
|
+
serialized_body: str | None = None
|
|
245
|
+
if body is not None:
|
|
246
|
+
request_headers["content-type"] = "application/json"
|
|
247
|
+
serialized_body = body if isinstance(body, str) else json.dumps(body)
|
|
248
|
+
|
|
249
|
+
status, response_headers, response_body = self.transport(
|
|
250
|
+
operation.method,
|
|
251
|
+
url,
|
|
252
|
+
request_headers,
|
|
253
|
+
serialized_body,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if status < 200 or status >= 300:
|
|
257
|
+
error_payload = (
|
|
258
|
+
response_body.get("error")
|
|
259
|
+
if isinstance(response_body, dict) and isinstance(response_body.get("error"), dict)
|
|
260
|
+
else {}
|
|
261
|
+
)
|
|
262
|
+
raise MailrithApiError(
|
|
263
|
+
status=status,
|
|
264
|
+
message=error_payload.get(
|
|
265
|
+
"message",
|
|
266
|
+
f"Mailrith request failed with status {status}.",
|
|
267
|
+
),
|
|
268
|
+
error_type=error_payload.get("type"),
|
|
269
|
+
code=error_payload.get("code"),
|
|
270
|
+
response_body=response_body,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return response_body
|