agentletter 0.1.0__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.
@@ -0,0 +1,41 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agent Letter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentletter
3
+ Version: 0.1.0
4
+ Summary: The physical-mail API for AI agents. Send a real, tracked, compliant letter with one call.
5
+ Project-URL: Homepage, https://agentletter.dev
6
+ Project-URL: Documentation, https://agentletter.dev
7
+ Project-URL: Source, https://github.com/noetiq/agentletter
8
+ Author-email: Falco Schneider <falco@noetiq.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,api,direct-mail,letters,mail,mcp,usps
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Communications
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.8
26
+ Requires-Dist: httpx<1,>=0.23
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Agent Letter — Python SDK
30
+
31
+ The physical-mail API for AI agents. Send a real, tracked, compliant letter with one call — the postal equivalent of giving an agent an email address.
32
+
33
+ > Private beta. Request access at [agentletter.dev](https://agentletter.dev).
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install agentletter
39
+ ```
40
+
41
+ ## Quickstart
42
+
43
+ ```python
44
+ from agentletter import AgentLetter
45
+
46
+ client = AgentLetter() # reads AGENTLETTER_API_KEY from the environment
47
+
48
+ letter = client.send(
49
+ to={
50
+ "name": "Jane Doe",
51
+ "line1": "1 Market St",
52
+ "city": "San Francisco",
53
+ "state": "CA",
54
+ "zip": "94105",
55
+ },
56
+ body=pdf_bytes, # PDF bytes — or html="..." / template="..."
57
+ certified=True, # proof of delivery
58
+ )
59
+
60
+ print(letter.id, letter.status) # "ltr_9f2a" "in_transit"
61
+ ```
62
+
63
+ Set your key once:
64
+
65
+ ```bash
66
+ export AGENTLETTER_API_KEY="sk_live_..."
67
+ ```
68
+
69
+ ## Sending content
70
+
71
+ Provide exactly one of:
72
+
73
+ ```python
74
+ client.send(to=addr, body=pdf_bytes) # a PDF
75
+ client.send(to=addr, html="<h1>Notice</h1>...") # rendered HTML
76
+ client.send(to=addr, template="tmpl_123",
77
+ variables={"name": "Jane", "amount": "$420"}) # a saved template
78
+ ```
79
+
80
+ Typed addresses are supported too:
81
+
82
+ ```python
83
+ from agentletter import Address
84
+
85
+ client.send(
86
+ to=Address(name="Jane Doe", line1="1 Market St",
87
+ city="San Francisco", state="CA", zip="94105"),
88
+ body=pdf_bytes,
89
+ )
90
+ ```
91
+
92
+ ## Tracking
93
+
94
+ ```python
95
+ letter = client.get("ltr_9f2a")
96
+ print(letter.status, letter.tracking_number, letter.expected_delivery)
97
+ ```
98
+
99
+ ## Async
100
+
101
+ ```python
102
+ import asyncio
103
+ from agentletter import AsyncAgentLetter
104
+
105
+ async def main():
106
+ async with AsyncAgentLetter() as client:
107
+ letter = await client.send(to=addr, body=pdf_bytes, certified=True)
108
+ print(letter.id)
109
+
110
+ asyncio.run(main())
111
+ ```
112
+
113
+ ## Errors
114
+
115
+ All errors subclass `AgentLetterError`:
116
+
117
+ ```python
118
+ from agentletter import AgentLetterError, AuthenticationError, RateLimitError
119
+
120
+ try:
121
+ client.send(to=addr, body=pdf_bytes)
122
+ except AuthenticationError:
123
+ ... # bad / missing key
124
+ except RateLimitError:
125
+ ... # back off and retry
126
+ except AgentLetterError as e:
127
+ print(e.status_code, e.message)
128
+ ```
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,104 @@
1
+ # Agent Letter — Python SDK
2
+
3
+ The physical-mail API for AI agents. Send a real, tracked, compliant letter with one call — the postal equivalent of giving an agent an email address.
4
+
5
+ > Private beta. Request access at [agentletter.dev](https://agentletter.dev).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install agentletter
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```python
16
+ from agentletter import AgentLetter
17
+
18
+ client = AgentLetter() # reads AGENTLETTER_API_KEY from the environment
19
+
20
+ letter = client.send(
21
+ to={
22
+ "name": "Jane Doe",
23
+ "line1": "1 Market St",
24
+ "city": "San Francisco",
25
+ "state": "CA",
26
+ "zip": "94105",
27
+ },
28
+ body=pdf_bytes, # PDF bytes — or html="..." / template="..."
29
+ certified=True, # proof of delivery
30
+ )
31
+
32
+ print(letter.id, letter.status) # "ltr_9f2a" "in_transit"
33
+ ```
34
+
35
+ Set your key once:
36
+
37
+ ```bash
38
+ export AGENTLETTER_API_KEY="sk_live_..."
39
+ ```
40
+
41
+ ## Sending content
42
+
43
+ Provide exactly one of:
44
+
45
+ ```python
46
+ client.send(to=addr, body=pdf_bytes) # a PDF
47
+ client.send(to=addr, html="<h1>Notice</h1>...") # rendered HTML
48
+ client.send(to=addr, template="tmpl_123",
49
+ variables={"name": "Jane", "amount": "$420"}) # a saved template
50
+ ```
51
+
52
+ Typed addresses are supported too:
53
+
54
+ ```python
55
+ from agentletter import Address
56
+
57
+ client.send(
58
+ to=Address(name="Jane Doe", line1="1 Market St",
59
+ city="San Francisco", state="CA", zip="94105"),
60
+ body=pdf_bytes,
61
+ )
62
+ ```
63
+
64
+ ## Tracking
65
+
66
+ ```python
67
+ letter = client.get("ltr_9f2a")
68
+ print(letter.status, letter.tracking_number, letter.expected_delivery)
69
+ ```
70
+
71
+ ## Async
72
+
73
+ ```python
74
+ import asyncio
75
+ from agentletter import AsyncAgentLetter
76
+
77
+ async def main():
78
+ async with AsyncAgentLetter() as client:
79
+ letter = await client.send(to=addr, body=pdf_bytes, certified=True)
80
+ print(letter.id)
81
+
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ## Errors
86
+
87
+ All errors subclass `AgentLetterError`:
88
+
89
+ ```python
90
+ from agentletter import AgentLetterError, AuthenticationError, RateLimitError
91
+
92
+ try:
93
+ client.send(to=addr, body=pdf_bytes)
94
+ except AuthenticationError:
95
+ ... # bad / missing key
96
+ except RateLimitError:
97
+ ... # back off and retry
98
+ except AgentLetterError as e:
99
+ print(e.status_code, e.message)
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agentletter"
7
+ version = "0.1.0"
8
+ description = "The physical-mail API for AI agents. Send a real, tracked, compliant letter with one call."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Falco Schneider", email = "falco@noetiq.com" }]
13
+ keywords = ["ai", "agents", "mail", "direct-mail", "letters", "api", "mcp", "usps"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: Communications",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = ["httpx>=0.23,<1"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://agentletter.dev"
33
+ Documentation = "https://agentletter.dev"
34
+ Source = "https://github.com/noetiq/agentletter"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/agentletter"]
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src/agentletter", "README.md", "LICENSE"]
@@ -0,0 +1,41 @@
1
+ """Agent Letter — the physical-mail API for AI agents.
2
+
3
+ Send a real, tracked, compliant letter with one call.
4
+
5
+ from agentletter import AgentLetter
6
+
7
+ client = AgentLetter() # reads AGENTLETTER_API_KEY
8
+ letter = client.send(
9
+ to={"name": "Jane Doe", "line1": "1 Market St",
10
+ "city": "San Francisco", "state": "CA", "zip": "94105"},
11
+ body=pdf_bytes,
12
+ certified=True,
13
+ )
14
+ print(letter.id, letter.status)
15
+ """
16
+
17
+ from ._version import __version__
18
+ from .client import AgentLetter, AsyncAgentLetter
19
+ from .errors import (
20
+ AgentLetterError,
21
+ APIError,
22
+ AuthenticationError,
23
+ InvalidRequestError,
24
+ NotFoundError,
25
+ RateLimitError,
26
+ )
27
+ from .types import Address, Letter
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "AgentLetter",
32
+ "AsyncAgentLetter",
33
+ "Address",
34
+ "Letter",
35
+ "AgentLetterError",
36
+ "APIError",
37
+ "AuthenticationError",
38
+ "InvalidRequestError",
39
+ "NotFoundError",
40
+ "RateLimitError",
41
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,262 @@
1
+ """HTTP client for the Agent Letter API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import os
7
+ import time
8
+ from typing import Any, Dict, Mapping, Optional, Union
9
+
10
+ import httpx
11
+
12
+ from ._version import __version__
13
+ from .errors import APIError, AgentLetterError, error_from_response
14
+ from .types import Address, Letter
15
+
16
+ DEFAULT_BASE_URL = "https://api.agentletter.dev/v1"
17
+ DEFAULT_TIMEOUT = 30.0
18
+ DEFAULT_MAX_RETRIES = 2
19
+
20
+ AddressInput = Union[Address, Mapping[str, Any]]
21
+
22
+
23
+ def _resolve_api_key(api_key: Optional[str]) -> str:
24
+ key = api_key or os.environ.get("AGENTLETTER_API_KEY")
25
+ if not key:
26
+ raise AgentLetterError(
27
+ "No API key provided. Pass api_key=... or set the "
28
+ "AGENTLETTER_API_KEY environment variable."
29
+ )
30
+ return key
31
+
32
+
33
+ def _address_dict(value: AddressInput) -> Dict[str, Any]:
34
+ if isinstance(value, Address):
35
+ return value.to_dict()
36
+ return dict(value)
37
+
38
+
39
+ def _build_payload(
40
+ *,
41
+ to: AddressInput,
42
+ body: Optional[bytes],
43
+ html: Optional[str],
44
+ template: Optional[str],
45
+ variables: Optional[Mapping[str, Any]],
46
+ from_: Optional[AddressInput],
47
+ certified: bool,
48
+ color: bool,
49
+ double_sided: bool,
50
+ metadata: Optional[Mapping[str, Any]],
51
+ ) -> Dict[str, Any]:
52
+ provided = [name for name, v in (("body", body), ("html", html), ("template", template)) if v is not None]
53
+ if len(provided) != 1:
54
+ raise AgentLetterError(
55
+ "Provide exactly one of: body (PDF bytes), html (str), or template (str). "
56
+ f"Got: {provided or 'none'}."
57
+ )
58
+
59
+ if body is not None:
60
+ content: Dict[str, Any] = {"pdf_base64": base64.b64encode(body).decode("ascii")}
61
+ elif html is not None:
62
+ content = {"html": html}
63
+ else:
64
+ content = {"template": template}
65
+ if variables:
66
+ content["variables"] = dict(variables)
67
+
68
+ payload: Dict[str, Any] = {
69
+ "to": _address_dict(to),
70
+ "body": content,
71
+ "certified": certified,
72
+ "color": color,
73
+ "double_sided": double_sided,
74
+ }
75
+ if from_ is not None:
76
+ payload["from"] = _address_dict(from_)
77
+ if metadata:
78
+ payload["metadata"] = dict(metadata)
79
+ return payload
80
+
81
+
82
+ class _BaseClient:
83
+ def __init__(
84
+ self,
85
+ api_key: Optional[str] = None,
86
+ *,
87
+ base_url: str = DEFAULT_BASE_URL,
88
+ timeout: float = DEFAULT_TIMEOUT,
89
+ max_retries: int = DEFAULT_MAX_RETRIES,
90
+ ) -> None:
91
+ self._api_key = _resolve_api_key(api_key)
92
+ self._base_url = base_url.rstrip("/")
93
+ self._timeout = timeout
94
+ self._max_retries = max_retries
95
+
96
+ @property
97
+ def _headers(self) -> Dict[str, str]:
98
+ return {
99
+ "Authorization": f"Bearer {self._api_key}",
100
+ "Content-Type": "application/json",
101
+ "User-Agent": f"agentletter-python/{__version__}",
102
+ }
103
+
104
+ def _parse(self, response: httpx.Response) -> Any:
105
+ try:
106
+ data = response.json()
107
+ except ValueError:
108
+ data = response.text
109
+ if response.status_code >= 400:
110
+ raise error_from_response(response.status_code, data)
111
+ return data
112
+
113
+
114
+ class AgentLetter(_BaseClient):
115
+ """Synchronous Agent Letter client.
116
+
117
+ Example::
118
+
119
+ from agentletter import AgentLetter
120
+
121
+ client = AgentLetter() # reads AGENTLETTER_API_KEY
122
+ letter = client.send(
123
+ to={"name": "Jane Doe", "line1": "1 Market St",
124
+ "city": "San Francisco", "state": "CA", "zip": "94105"},
125
+ body=pdf_bytes,
126
+ certified=True,
127
+ )
128
+ print(letter.id, letter.status)
129
+ """
130
+
131
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
132
+ super().__init__(*args, **kwargs)
133
+ self._client = httpx.Client(base_url=self._base_url, timeout=self._timeout)
134
+
135
+ def _request(self, method: str, path: str, *, json: Optional[dict] = None) -> Any:
136
+ last_exc: Optional[Exception] = None
137
+ for attempt in range(self._max_retries + 1):
138
+ try:
139
+ response = self._client.request(
140
+ method, path, headers=self._headers, json=json
141
+ )
142
+ except httpx.HTTPError as exc:
143
+ last_exc = APIError(f"Network error talking to Agent Letter: {exc}")
144
+ if attempt < self._max_retries:
145
+ time.sleep(0.5 * (2**attempt))
146
+ continue
147
+ raise last_exc from exc
148
+ if response.status_code in (429, 500, 502, 503, 504) and attempt < self._max_retries:
149
+ time.sleep(0.5 * (2**attempt))
150
+ continue
151
+ return self._parse(response)
152
+ raise last_exc or APIError("Request failed.")
153
+
154
+ def send(
155
+ self,
156
+ *,
157
+ to: AddressInput,
158
+ body: Optional[bytes] = None,
159
+ html: Optional[str] = None,
160
+ template: Optional[str] = None,
161
+ variables: Optional[Mapping[str, Any]] = None,
162
+ from_: Optional[AddressInput] = None,
163
+ certified: bool = False,
164
+ color: bool = False,
165
+ double_sided: bool = True,
166
+ metadata: Optional[Mapping[str, Any]] = None,
167
+ ) -> Letter:
168
+ """Send a physical letter. Provide one of body / html / template."""
169
+ payload = _build_payload(
170
+ to=to, body=body, html=html, template=template, variables=variables,
171
+ from_=from_, certified=certified, color=color,
172
+ double_sided=double_sided, metadata=metadata,
173
+ )
174
+ data = self._request("POST", "/letters", json=payload)
175
+ return Letter.from_dict(data)
176
+
177
+ def get(self, letter_id: str) -> Letter:
178
+ """Retrieve a letter by id."""
179
+ data = self._request("GET", f"/letters/{letter_id}")
180
+ return Letter.from_dict(data)
181
+
182
+ def cancel(self, letter_id: str) -> Letter:
183
+ """Cancel a letter that has not yet been printed."""
184
+ data = self._request("POST", f"/letters/{letter_id}/cancel")
185
+ return Letter.from_dict(data)
186
+
187
+ def close(self) -> None:
188
+ self._client.close()
189
+
190
+ def __enter__(self) -> "AgentLetter":
191
+ return self
192
+
193
+ def __exit__(self, *args: Any) -> None:
194
+ self.close()
195
+
196
+
197
+ class AsyncAgentLetter(_BaseClient):
198
+ """Asynchronous Agent Letter client (same API as :class:`AgentLetter`)."""
199
+
200
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
201
+ super().__init__(*args, **kwargs)
202
+ self._client = httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout)
203
+
204
+ async def _request(self, method: str, path: str, *, json: Optional[dict] = None) -> Any:
205
+ import asyncio
206
+
207
+ last_exc: Optional[Exception] = None
208
+ for attempt in range(self._max_retries + 1):
209
+ try:
210
+ response = await self._client.request(
211
+ method, path, headers=self._headers, json=json
212
+ )
213
+ except httpx.HTTPError as exc:
214
+ last_exc = APIError(f"Network error talking to Agent Letter: {exc}")
215
+ if attempt < self._max_retries:
216
+ await asyncio.sleep(0.5 * (2**attempt))
217
+ continue
218
+ raise last_exc from exc
219
+ if response.status_code in (429, 500, 502, 503, 504) and attempt < self._max_retries:
220
+ await asyncio.sleep(0.5 * (2**attempt))
221
+ continue
222
+ return self._parse(response)
223
+ raise last_exc or APIError("Request failed.")
224
+
225
+ async def send(
226
+ self,
227
+ *,
228
+ to: AddressInput,
229
+ body: Optional[bytes] = None,
230
+ html: Optional[str] = None,
231
+ template: Optional[str] = None,
232
+ variables: Optional[Mapping[str, Any]] = None,
233
+ from_: Optional[AddressInput] = None,
234
+ certified: bool = False,
235
+ color: bool = False,
236
+ double_sided: bool = True,
237
+ metadata: Optional[Mapping[str, Any]] = None,
238
+ ) -> Letter:
239
+ payload = _build_payload(
240
+ to=to, body=body, html=html, template=template, variables=variables,
241
+ from_=from_, certified=certified, color=color,
242
+ double_sided=double_sided, metadata=metadata,
243
+ )
244
+ data = await self._request("POST", "/letters", json=payload)
245
+ return Letter.from_dict(data)
246
+
247
+ async def get(self, letter_id: str) -> Letter:
248
+ data = await self._request("GET", f"/letters/{letter_id}")
249
+ return Letter.from_dict(data)
250
+
251
+ async def cancel(self, letter_id: str) -> Letter:
252
+ data = await self._request("POST", f"/letters/{letter_id}/cancel")
253
+ return Letter.from_dict(data)
254
+
255
+ async def aclose(self) -> None:
256
+ await self._client.aclose()
257
+
258
+ async def __aenter__(self) -> "AsyncAgentLetter":
259
+ return self
260
+
261
+ async def __aexit__(self, *args: Any) -> None:
262
+ await self.aclose()
@@ -0,0 +1,67 @@
1
+ """Exception types raised by the Agent Letter SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class AgentLetterError(Exception):
9
+ """Base class for all Agent Letter errors."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ status_code: Optional[int] = None,
16
+ body: Any = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.message = message
20
+ self.status_code = status_code
21
+ self.body = body
22
+
23
+
24
+ class AuthenticationError(AgentLetterError):
25
+ """Raised when the API key is missing or invalid (HTTP 401/403)."""
26
+
27
+
28
+ class InvalidRequestError(AgentLetterError):
29
+ """Raised when the request is malformed or fails validation (HTTP 400/422)."""
30
+
31
+
32
+ class NotFoundError(AgentLetterError):
33
+ """Raised when a resource does not exist (HTTP 404)."""
34
+
35
+
36
+ class RateLimitError(AgentLetterError):
37
+ """Raised when the rate limit is exceeded (HTTP 429)."""
38
+
39
+
40
+ class APIError(AgentLetterError):
41
+ """Raised for unexpected server errors (HTTP 5xx) or transport failures."""
42
+
43
+
44
+ def error_from_response(status_code: int, body: Any) -> AgentLetterError:
45
+ """Map an HTTP status code + parsed body to the right exception type."""
46
+ message = _extract_message(body) or f"Agent Letter request failed ({status_code})."
47
+ if status_code in (401, 403):
48
+ return AuthenticationError(message, status_code=status_code, body=body)
49
+ if status_code == 404:
50
+ return NotFoundError(message, status_code=status_code, body=body)
51
+ if status_code == 429:
52
+ return RateLimitError(message, status_code=status_code, body=body)
53
+ if status_code in (400, 422):
54
+ return InvalidRequestError(message, status_code=status_code, body=body)
55
+ return APIError(message, status_code=status_code, body=body)
56
+
57
+
58
+ def _extract_message(body: Any) -> Optional[str]:
59
+ if isinstance(body, dict):
60
+ err = body.get("error")
61
+ if isinstance(err, dict):
62
+ return err.get("message")
63
+ if isinstance(err, str):
64
+ return err
65
+ if isinstance(body.get("message"), str):
66
+ return body["message"]
67
+ return None
File without changes
@@ -0,0 +1,59 @@
1
+ """Data types for the Agent Letter SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, Optional
7
+
8
+
9
+ @dataclass
10
+ class Address:
11
+ """A postal recipient or sender address."""
12
+
13
+ name: str
14
+ line1: str
15
+ city: str
16
+ state: str
17
+ zip: str
18
+ line2: Optional[str] = None
19
+ country: str = "US"
20
+
21
+ def to_dict(self) -> Dict[str, Any]:
22
+ data = {
23
+ "name": self.name,
24
+ "line1": self.line1,
25
+ "city": self.city,
26
+ "state": self.state,
27
+ "zip": self.zip,
28
+ "country": self.country,
29
+ }
30
+ if self.line2:
31
+ data["line2"] = self.line2
32
+ return data
33
+
34
+
35
+ @dataclass
36
+ class Letter:
37
+ """A letter created through the API."""
38
+
39
+ id: str
40
+ status: str
41
+ to: Dict[str, Any] = field(default_factory=dict)
42
+ certified: bool = False
43
+ tracking_number: Optional[str] = None
44
+ expected_delivery: Optional[str] = None
45
+ created_at: Optional[str] = None
46
+ raw: Dict[str, Any] = field(default_factory=dict)
47
+
48
+ @classmethod
49
+ def from_dict(cls, data: Dict[str, Any]) -> "Letter":
50
+ return cls(
51
+ id=data.get("id", ""),
52
+ status=data.get("status", "unknown"),
53
+ to=data.get("to", {}) or {},
54
+ certified=bool(data.get("certified", False)),
55
+ tracking_number=data.get("tracking_number"),
56
+ expected_delivery=data.get("expected_delivery"),
57
+ created_at=data.get("created_at"),
58
+ raw=data,
59
+ )